Skip to content

Instantly share code, notes, and snippets.

@CoffeepotCoder
Last active March 10, 2022 03:31
Show Gist options
  • Save CoffeepotCoder/84e30114f522579bd27ddbbfd8cb7365 to your computer and use it in GitHub Desktop.
Save CoffeepotCoder/84e30114f522579bd27ddbbfd8cb7365 to your computer and use it in GitHub Desktop.
RoadRAGE: Under the Hood
using UnityEngine;
using UnityEngine.InputSystem;
/// <Summary>
/// Brainstem of the player "Runner"
/// </Summary>
public class RunnerEntity : MonoBehaviour
{
[Header("Setup")]
[SerializeField] PlayerInput _player;
[SerializeField] Animator _animator;
[SerializeField] SpriteRenderer _renderer;
[SerializeField] Rigidbody2D _rigidbody;
[SerializeField] Collider2D _collider;
[SerializeField] ContactFilter2D _filter;
[Header("Control")]
[SerializeField] float _moveSpeed = 250f;
[SerializeField] float _jumpForce = 125f;
[SerializeField] float _hangFactor = 6f;
[SerializeField] float _gravFactor = 3f;
[Header("Config")]
[SerializeField, Range(0f, 10f)] float _clampForce = 5f;
[SerializeField, Range(1f, 10f)] float _assistRange = 5f;
[SerializeField, Range(0f, 1f)] float _assistForce = 0f;
[SerializeField, Range(0f, 1f)] float _sensorRange = 0.1f;
internal PlayerInput Player => _player;
internal Animator Animator => _animator;
internal SpriteRenderer Renderer => _renderer;
internal Rigidbody2D Rigidbody => _rigidbody;
internal Collider2D Collider => _collider;
internal ContactFilter2D Filter => _filter;
internal float MoveSpeed => _moveSpeed;
internal float JumpForce => _jumpForce;
internal float HangFactor => _hangFactor;
internal float GravFactor => _gravFactor;
internal float ClampForce => _clampForce;
internal float AssistForce => _assistForce;
internal float AssistRange => _assistRange;
internal float SensorRange => _sensorRange;
internal RunnerInput Input { get; private set; }
internal RunnerFSM State { get; private set; }
internal RunnerSensor Sensor { get; private set; }
internal RunnerController Controller { get; private set; }
void Awake()
{
Input = new RunnerInput(this);
Sensor = new RunnerSensor(this);
State = new RunnerFSM(this);
Controller = new RunnerController(this);
}
void Update()
{
State.UpdateState();
Controller.PollForJumpAction();
Controller.EvaluateColliderToggle();
State.UpdateState();
}
void FixedUpdate()
{
Controller.Simulate();
if (!State.IsReaching && Sensor.DetectsEdge)
Controller.ClampBoundsToSurface();
}
void OnTriggerStay2D(Collider2D collision)
=> Controller.EvaluateCollisionIgnorance(collision);
void OnCollisionEnter2D(Collision2D collision)
{
if (collision.GetContact(0).normal != Vector2.up)
Controller.CorrectGlancingError(collision);
Sensor.Contacts.Add(collision.collider);
}
void OnCollisionStay2D(Collision2D collision)
{
Controller.SyncVelocityToSurface(collision);
}
void OnCollisionExit2D(Collision2D collision)
{
Sensor.Contacts.Remove(collision.collider);
}
}
using System;
using UnityEngine;
/// <Summary>
/// Finite State Machine for the Runner
/// </Summary>
public class RunnerFSM
{
internal Action<RunnerEntity> WipedOut;
enum State { Idle, Running, Reaching, Jumping, KnockedOut, WipedOut }
State _state = State.Idle;
internal bool IsIdle => _state == State.Idle;
internal bool IsRunning => _state == State.Running;
internal bool IsReaching => _state == State.Reaching;
internal bool IsJumping => _state == State.Jumping;
internal bool IsKnockedOut => _state == State.KnockedOut;
internal bool IsWipedOut => _state == State.WipedOut;
readonly RunnerEntity _entity;
SpriteRenderer Renderer => _entity.Renderer;
RunnerSensor Sensor => _entity.Sensor;
RunnerInput Input => _entity.Input;
Animator Animator => _entity.Animator;
internal RunnerFSM(RunnerEntity entity)
=> _entity = entity;
internal void UpdateState()
{
var stateLast = _state;
float moveX = Input.MoveVector.x;
float absX = Mathf.Abs((float)moveX);
if (Sensor.DetectsGround)
_state = State.WipedOut;
else if (!IsWipedOut)
if (Sensor.DetectsSurface)
if (absX > 0.05f)
if (Sensor.DetectsEdge)
_state = State.Reaching;
else _state = State.Running;
else _state = State.Idle;
else _state = State.Jumping;
else if (IsWipedOut && !Sensor.DetectsGround)
_state = State.KnockedOut;
else _state = State.WipedOut;
Animator.SetFloat("Speed", absX);
if (!IsWipedOut && moveX < 0)
Renderer.flipX = true;
else Renderer.flipX = false;
if (_state != stateLast)
Animator.SetTrigger($"{_state}");
if (_state != stateLast && IsWipedOut)
WipedOut?.Invoke(_entity);
}
}
using System.Collections.Generic;
using UnityEngine;
/// <Summary>
/// Edge and Surface detection
/// </Summary>
public class RunnerSensor
{
internal float Range => _entity.SensorRange;
readonly RunnerEntity _entity;
ContactFilter2D Filter => _entity.Filter;
Collider2D Collider => _entity.Collider;
internal List<Collider2D> Contacts => _contacts;
readonly List<Collider2D> _contacts = new List<Collider2D>();
Vector3 Mid => Collider.bounds.center;
Vector3 Min => Collider.bounds.min;
Vector3 Max => Collider.bounds.max;
//Vector3 TopL => new Vector3(Min.x, Max.y, Mid.z);
//Vector3 TopM => new Vector3(Mid.x, Max.y, Mid.z);
//Vector3 TopR => new Vector3(Max.x, Max.y, Mid.z);
//Vector3 MidL => new Vector3(Min.x, Mid.y, Mid.z);
//Vector3 MidM => new Vector3(Mid.x, Mid.y, Mid.z);
//Vector3 MidR => new Vector3(Max.x, Mid.y, Mid.z);
Vector3 BotL => new Vector3(Min.x, Min.y, Mid.z);
//Vector3 BotM => new Vector3(Mid.x, Min.y, Mid.z);
Vector3 BotR => new Vector3(Max.x, Min.y, Mid.z);
internal RunnerSensor(RunnerEntity entity)
=> _entity = entity;
internal bool DetectsSurface
=> DetectLayer(29);
internal bool DetectsVehicle
=> DetectLayer(30);
internal bool DetectsGround
=> DetectLayer(31);
internal bool DetectsEdgeL
=> DetectEdge(BotL, Vector2.down, Range);
internal bool DetectsEdgeR
=> DetectEdge(BotR, Vector2.down, Range);
internal bool DetectsEdge
=> DetectsEdgeL || DetectsEdgeR;
private bool DetectLayer(int layer)
{
if (_contacts.Count == 0)
return false;
foreach (var collider in _contacts)
if (collider.gameObject.layer == layer)
return true;
else continue;
return false;
}
private bool DetectEdge(Vector3 position, Vector2 direction, float range)
=> DetectsSurface && Physics2D.Raycast(position, direction, range, Filter.layerMask).collider == null;
}
using UnityEngine;
/// <Summary>
/// Physics-based character controller
/// </Summary>
internal class RunnerController
{
float MoveSpeed => _entity.MoveSpeed;
float JumpForce => _entity.JumpForce;
float HangFactor => _entity.HangFactor;
float GravFactor => _entity.GravFactor;
float ClampForce => _entity.ClampForce;
float AssistForce => _entity.AssistForce;
float AssistRange => _entity.AssistRange;
readonly RunnerEntity _entity;
RunnerInput Input => _entity.Input;
Rigidbody2D Rigidbody => _entity.Rigidbody;
RunnerFSM State => _entity.State;
RunnerSensor Sensor => _entity.Sensor;
Collider2D Collider => _entity.Collider;
ContactFilter2D Filter => _entity.Filter;
Vector2 InputVelocity => new Vector2
(
_defaultVelocity.x + (Input.MoveVector.x * MoveSpeed * Time.fixedDeltaTime),
Rigidbody.velocity.y
);
Vector2 _defaultVelocity = Vector2.zero;
internal RunnerController(RunnerEntity entity)
=> _entity = entity;
internal void PollForJumpAction()
{
if (Input.JumpActionPush && !State.IsJumping && !State.IsWipedOut)
Rigidbody.AddForce(Vector2.up * JumpForce);
}
internal void Simulate()
{
if (State.IsWipedOut)
return;
float moveX = Input.MoveVector.x;
var absThrow = Mathf.Abs((float)moveX);
var WalkingOffEdgeL = Sensor.DetectsEdgeL && moveX < 0f;
var WalkingOffEdgeR = Sensor.DetectsEdgeR && moveX > 0f;
if (absThrow > 0.05f && (WalkingOffEdgeL || WalkingOffEdgeR))
Rigidbody.velocity = _defaultVelocity;
else Rigidbody.velocity = InputVelocity;
if (State.IsJumping)
{
if (AssistForce > 0 && Rigidbody.velocity.y < 0 && Input.MoveVector.x >= 0)
JumpAssist();
Rigidbody.gravityScale += Time.fixedDeltaTime * GravFactor;
if (Input.JumpActionHold)
Rigidbody.AddForce(HangFactor * JumpForce * Time.fixedDeltaTime * Vector2.up);
}
else Rigidbody.gravityScale = 1;
}
private void JumpAssist()
{
var hits = Physics2D.RaycastAll(Rigidbody.position, Vector2.down, AssistRange, Filter.layerMask);
if (hits.Length != 0)
{
Rigidbody2D target = null;
float closestHit = AssistRange;
for (int i = 0; i < hits.Length; i++)
{
if (hits[i].distance < closestHit)
{
closestHit = hits[i].distance;
target = hits[i].rigidbody;
}
}
if (target == null) return;
if (closestHit <= 0f)
closestHit = Mathf.Epsilon;
var step = (1f - closestHit / AssistRange) * AssistForce;
var targetVelocityX = Mathf.Lerp(Rigidbody.velocity.x, target.velocity.x, step);
var targetVelocityY = Rigidbody.velocity.y;
Rigidbody.velocity = new Vector2(targetVelocityX, targetVelocityY);
}
}
internal void CorrectGlancingError(Collision2D collision)
{
Rigidbody.position = collision.GetContact(0).point + Vector2.up;
Debug.LogError($"Glance Error - Normal: {collision.GetContact(0).normal}, Setting to Point: {collision.GetContact(0).point}");
}
internal void ClampBoundsToSurface()
{
if (!Sensor.DetectsEdgeL && Sensor.DetectsEdgeR)
Rigidbody.velocity = _defaultVelocity + Vector2.left * ClampForce;
if (!Sensor.DetectsEdgeR && Sensor.DetectsEdgeL)
Rigidbody.velocity = _defaultVelocity + Vector2.right * ClampForce;
}
internal void SyncVelocityToSurface(Collision2D collision)
{
if (Rigidbody.velocity.y <= 0)
if (collision.rigidbody != null)
_defaultVelocity = collision.rigidbody.velocity;
}
internal void EvaluateCollisionIgnorance(Collider2D collision)
=> Physics2D.IgnoreCollision
(Collider, collision, collision.bounds.max.y > Collider.bounds.min.y);
internal void EvaluateColliderToggle()
=> Collider.enabled = Rigidbody.velocity.y <= 0;
}
using UnityEngine;
using UnityEngine.InputSystem;
/// <Summary>
/// Translates player input for Runner actions
/// </Summary>
internal class RunnerInput
{
readonly InputAction _moveAction;
readonly InputAction _jumpAction;
public Vector2 MoveVector => _moveAction.ReadValue<Vector2>();
public bool JumpActionPush => _jumpAction.WasPressedThisFrame();
public bool JumpActionHold => _jumpAction.IsPressed();
internal RunnerInput(RunnerEntity entity)
{
_moveAction = entity.Player.actions.FindAction("Move");
_jumpAction = entity.Player.actions.FindAction("Jump");
}
}
public static class InputActionExtensions
{
public static bool IsPressed(this InputAction inputAction)
=> inputAction.ReadValue<float>() > 0f;
public static bool WasPressedThisFrame(this InputAction inputAction)
=> inputAction.triggered && inputAction.ReadValue<float>() > 0f;
public static bool WasReleasedThisFrame(this InputAction inputAction)
=> inputAction.triggered && inputAction.ReadValue<float>() == 0f;
}
using UnityEngine;
/// <Summary>
/// Collects information about player performance for use by the ScoreKeeper
/// </Summary>
[RequireComponent(typeof(RunnerEntity))]
public class ScoreTracker : MonoBehaviour
{
internal float Velocity => Mathf.Abs(_rigidbody.velocity.x);
internal float VelocityHI => _velocityHI;
float _velocityHI;
internal float Distance => transform.position.x;
internal float DistanceLO => _distanceLO;
float _distanceLO;
internal float DistanceHI => _distanceHI;
float _distanceHI;
internal float StyleHI => _styleHI;
float _styleHI;
RunnerEntity _runner;
Rigidbody2D _rigidbody;
Vector3 _entryPoint, _exitPoint;
private void Start()
{
_runner = GetComponent<RunnerEntity>();
_runner.State.WipedOut += SubmitHiScore;
_rigidbody = _runner.Rigidbody;
}
void OnDestroy()
=> _runner.State.WipedOut -= SubmitHiScore;
private void Update()
{
if (!TimeKeeper.IsRoundOngoing) return;
if (Velocity > _velocityHI) _velocityHI = Velocity;
if (Distance > _distanceHI) _distanceHI = Distance;
if (Distance < _distanceLO) _distanceLO = Distance;
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (!TimeKeeper.IsRoundOngoing) return;
_entryPoint = transform.position;
_styleHI += Vector3.Magnitude(_exitPoint - _entryPoint);
}
private void OnCollisionExit2D(Collision2D collision)
=> _exitPoint = transform.position;
internal void Submit1KScore()
=> ScoreKeeper.EvaluateScores1K(new ScoreEntry(this));
void SubmitHiScore(RunnerEntity entity)
=> ScoreKeeper.EvaluateScoresHI(new ScoreEntry(this));
}
internal struct ScoreEntry
{
internal float distanceLO;
internal float distanceHI;
internal float velocityHI;
internal float styleHI;
internal ScoreEntry(ScoreTracker tracker)
{
distanceLO = tracker.DistanceLO;
distanceHI = tracker.DistanceHI;
velocityHI = tracker.VelocityHI;
styleHI = tracker.StyleHI;
}
}
using System;
using UnityEngine;
/// <summary>
/// Saves and loads Runner score data from PlayerPrefs
/// </summary>
internal static class ScoreKeeper
{
public static event Action UpdatedScores1K;
public static event Action UpdatedScoresHI;
readonly static string _vHI = "VelocityHI";
readonly static string _v1K = "Velocity1K";
readonly static string _dHI = "DistanceHI";
readonly static string _dLO = "DistanceLO";
readonly static string _sHI = "StyleHI";
readonly static string _s1K = "Style1K";
readonly static string _tHI = "TimeHI";
readonly static string _t1K = "Time1K";
static float _velocityHI, _velocity1K;
static float _distanceHI, _distanceLO;
static float _styleHI, _style1K;
static float _timeHI, _time1K;
static internal float VelocityHI
{
get => _velocityHI != 0 ? _velocityHI : _velocityHI = PlayerPrefs.GetFloat(_vHI);
private set => _velocityHI = value;
}
static internal float Velocity1K
{
get => _velocity1K != 0 ? _velocity1K : _velocity1K = PlayerPrefs.GetFloat(_v1K);
private set => _velocity1K = value;
}
static internal float DistanceHI
{
get => _distanceHI != 0 ? _distanceHI : _distanceHI = PlayerPrefs.GetFloat(_dHI);
private set => _distanceHI = value;
}
static internal float DistanceLO
{
get => _distanceLO != 0 ? _distanceLO : _distanceLO = PlayerPrefs.GetFloat(_dLO);
private set => _distanceLO = value;
}
static internal float StyleHI
{
get => _styleHI != 0 ? _styleHI : _styleHI = PlayerPrefs.GetFloat(_sHI);
private set => _styleHI = value;
}
static internal float Style1K
{
get => _style1K != 0 ? _style1K : _style1K = PlayerPrefs.GetFloat(_s1K);
private set => _style1K = value;
}
static internal float TimeHI
{
get => _timeHI != 0 ? _timeHI : _timeHI = PlayerPrefs.GetFloat(_tHI);
private set => _timeHI = value;
}
static internal float Time1K
{
get => _time1K != 0 ? _time1K : _time1K = PlayerPrefs.GetFloat(_t1K, Mathf.Infinity);
private set => _time1K = value;
}
static internal void SerializeScores()
{
PlayerPrefs.SetFloat(_vHI, _velocityHI);
PlayerPrefs.SetFloat(_v1K, _velocity1K);
PlayerPrefs.SetFloat(_dHI, _distanceHI);
PlayerPrefs.SetFloat(_dLO, _distanceLO);
PlayerPrefs.SetFloat(_sHI, _styleHI);
PlayerPrefs.SetFloat(_s1K, _style1K);
PlayerPrefs.SetFloat(_tHI, _timeHI);
PlayerPrefs.SetFloat(_t1K, _time1K);
}
static internal void EvaluateScoresHI(ScoreEntry score)
{
if (score.distanceHI > DistanceHI)
DistanceHI = score.distanceHI;
if (score.distanceLO < DistanceLO)
DistanceLO = score.distanceLO;
if (score.velocityHI > VelocityHI)
VelocityHI = score.velocityHI;
if (score.styleHI > StyleHI)
StyleHI = score.styleHI;
if (TimeKeeper.RoundCurrentTime > TimeHI)
TimeHI = TimeKeeper.RoundCurrentTime;
UpdatedScoresHI?.Invoke();
}
static internal void EvaluateScores1K(ScoreEntry score)
{
if (score.velocityHI > Velocity1K)
Velocity1K = score.velocityHI;
if (score.styleHI > Style1K)
Style1K = score.styleHI;
if (TimeKeeper.RoundCurrentTime < Time1K)
Time1K = TimeKeeper.RoundCurrentTime;
UpdatedScores1K?.Invoke();
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
/// <Summary>
/// Updates UI to reflect player performance
/// </Summary>
public class ScoreDisplay : MonoBehaviour
{
[SerializeField] float _tickRate = 0.01f;
[SerializeField] List<ScoreTracker> _trackers;
[SerializeField] Text _textVelocity;
[SerializeField] Text _textVelocityHI;
[SerializeField] Text _textVelocity1K;
float _velocityHI, _velocityLast;
[SerializeField] Text _textDistance;
[SerializeField] Text _textDistanceHI;
[SerializeField] Text _textDistanceLO;
float _distanceHI, _distanceLO;
[SerializeField] Text _textStyle;
[SerializeField] Text _textStyleHI;
[SerializeField] Text _textStyle1K;
float _styleLast;
[SerializeField] Text _textTime;
[SerializeField] Text _textTimeHI;
[SerializeField] Text _textTime1K;
void Start()
{
FetchScoresHI();
FetchScores1K();
ScoreKeeper.UpdatedScores1K += FetchScores1K;
ScoreKeeper.UpdatedScoresHI += FetchScoresHI;
StartCoroutine(TickerRoutine());
}
void OnDestroy()
{
ScoreKeeper.UpdatedScores1K -= FetchScores1K;
ScoreKeeper.UpdatedScoresHI -= FetchScoresHI;
}
IEnumerator TickerRoutine()
{
var interval = new WaitForSeconds(_tickRate);
while (true)
{
yield return interval;
if (!TimeKeeper.IsRoundOngoing)
continue;
var timeCurrent = TimeKeeper.RoundCurrentTime;
var leadVelocity = 0f;
var leadDistance = 0f;
var leadStyle = 0f;
foreach (var tracker in _trackers)
{
if (tracker.Velocity > leadVelocity)
leadVelocity = tracker.Velocity;
if (tracker.VelocityHI > _velocityHI)
_velocityHI = tracker.VelocityHI;
if (tracker.Distance > leadDistance)
leadDistance = tracker.Distance;
if (tracker.DistanceHI > _distanceHI)
_distanceHI = tracker.DistanceHI;
if (tracker.DistanceLO < _distanceLO)
_distanceLO = tracker.DistanceLO;
if (tracker.StyleHI > leadStyle)
leadStyle = tracker.StyleHI;
}
_textTime.text = $"{timeCurrent:0.00} t";
_textDistance.text = $"{leadDistance:0.00} m";
_textVelocity.text = $"{leadVelocity:0.00} s";
var style = Mathf.Lerp(_styleLast, leadStyle, 0.1f);
_textStyle.text = $"{style:0.00} p";
_styleLast = style;
if (timeCurrent > ScoreKeeper.TimeHI)
_textTimeHI.text = $"[ HI: {timeCurrent:0.00} t ]";
if (_distanceHI > ScoreKeeper.DistanceHI)
_textDistanceHI.text = $"[ HI: {leadDistance:0.00} m ]";
if (_distanceLO < ScoreKeeper.DistanceLO)
_textDistanceLO.text = $"[ HI: {leadDistance:0.00} m ]";
if (_velocityHI > ScoreKeeper.VelocityHI && _velocityHI > _velocityLast)
_textVelocityHI.text = $"[ HI: {leadVelocity:0.00} s ]";
_velocityLast = _velocityHI;
if (leadStyle > ScoreKeeper.StyleHI)
_textStyleHI.text = $"[ HI: {style:0.00} p ]";
}
}
void FetchScoresHI()
{
_textVelocityHI.text
= $"[ HI: {ScoreKeeper.VelocityHI:0.00} s ]";
_textTimeHI.text
= $"[ HI: {ScoreKeeper.TimeHI:0.00} t ]";
_textStyleHI.text
= $"[ HI: {ScoreKeeper.StyleHI:0.00} p ]";
_textDistanceHI.text
= $"[ HI: {ScoreKeeper.DistanceHI:0.00} m ]";
_textDistanceLO.text
= $"[ LO: {ScoreKeeper.DistanceLO:0.00} m ]";
}
void FetchScores1K()
{
_textVelocity1K.text
= $"[ 1K: {ScoreKeeper.Velocity1K:0.00} s ]";
_textTime1K.text
= $"[ 1K: {ScoreKeeper.Time1K:0.00} t ]";
_textStyle1K.text
= $"[ 1K: {ScoreKeeper.Style1K:0.00} p ]";
}
}
using System;
using UnityEngine;
/// <summary>
/// Keeps track of time in the context of the current run
/// </summary>
public static class TimeKeeper
{
static internal float RoundCurrentTime => _isRoundOngoing ? Time.time - _timeStarted : _timeStopped - _timeStarted;
static float _timeStopped, _timeStarted;
static internal bool IsRoundOngoing => _isRoundOngoing;
static bool _isRoundOngoing;
static internal Action RoundStarted;
static internal Action RoundStopped;
static internal void StartRound()
{
StopRound();
_timeStarted = Time.time;
_isRoundOngoing = true;
RoundStarted?.Invoke();
}
static internal void StopRound()
{
_timeStopped = Time.time;
_isRoundOngoing = false;
RoundStopped?.Invoke();
}
}
using UnityEngine;
/// <Summary>
/// Brainstem of Vehicle GameObjects
/// </Summary>
public class VehicleBehaviour : MonoBehaviour
{
[SerializeField] VehicleController _controller;
[SerializeField] VehicleSensor _sensor;
[Tooltip("The time in seconds after an encounter before the driver will accelerate")]
[SerializeField] float _startupDelay = 1f;
internal float _startupTimer = 0f;
private bool _isDistractedDriver;
private void OnEnable()
=> _isDistractedDriver = Random.Range(0, 10) < 1;
private void FixedUpdate()
=> Drive();
private void OnCollisionEnter2D(Collision2D collision)
=> _controller.BurstBrake();
private void OnCollisionStay2D(Collision2D collision)
{
_controller.PassiveBrake();
_startupTimer = 0f;
}
private void Drive()
{
if (!_isDistractedDriver && _sensor.DetectsTraffic())
{
_controller.TrafficBrake();
return;
}
if (_startupTimer < _startupDelay)
{
_startupTimer += Time.fixedDeltaTime;
return;
}
_controller.Drive();
}
}
using UnityEngine;
/// <Summary>
/// Kinematic controller for Vehicles
/// </Summary>
public class VehicleController : MonoBehaviour
{
[SerializeField] Rigidbody2D _vehicle;
[SerializeField] VehicleSensor _sensor;
[Tooltip("The interpolation rate when Target Velocity > Current Velocity")]
[SerializeField, Range(0.01f, 1f)] float _acceleration = 0.25f;
[Tooltip("The interpolation rate when Target Velocity < Current Velocity")]
[SerializeField, Range(0.01f, 1f)] float _braking = 0.25f;
[Tooltip("The starting velocity from which TargetVelocity is derived")]
[SerializeField] float _baseVelocity;
internal Vector2 TargetVelocity
=> new Vector2(_baseVelocity * Random.Range(0.75f, 1.25f) + ((TimeKeeper.RoundCurrentTime + transform.position.x) * 0.01f), 0f);
private void OnEnable()
=> _vehicle.velocity = TargetVelocity;
internal void Drive()
=> _vehicle.velocity = Vector2.Lerp
(_vehicle.velocity, TargetVelocity, _acceleration * Time.fixedDeltaTime);
internal void PassiveBrake()
=> _vehicle.velocity = Vector2.Lerp
(_vehicle.velocity, Vector2.zero, _braking * Time.fixedDeltaTime);
internal void BurstBrake()
=> _vehicle.velocity *= 0.9f;
internal void TrafficBrake()
{
float currentVelocity = _vehicle.velocity.x;
float targetVelocity = _sensor.TrafficScanner[0].rigidbody.velocity.x;
Keyframe travelSpeed = new Keyframe(0f, currentVelocity);
Keyframe targetSpeed = new Keyframe(_sensor.ScanRange / _braking, targetVelocity);
Keyframe[] brakeFrames = new Keyframe[2] { travelSpeed, targetSpeed };
AnimationCurve brakeCurve = new AnimationCurve(brakeFrames);
_vehicle.velocity = new Vector2(brakeCurve.Evaluate(_sensor.Proximity), 0f);
}
}
using System;
using UnityEngine;
/// <Summary>
/// Collects down-lane traffic data for use by Vehicele behaviours
/// </Summary>
public class VehicleSensor : MonoBehaviour
{
[SerializeField] CapsuleCollider2D _bumper;
[SerializeField] float _scanRange = 10f;
[SerializeField] ContactFilter2D _scanFilter;
RaycastHit2D[] _trafficScanner = new RaycastHit2D[1];
Vector2 ScanOrigin => new Vector2(_bumper.bounds.max.x + 0.1f, _bumper.bounds.center.y);
internal float ScanRange => _scanRange;
internal float Urgency => Proximity / _scanRange;
internal float Proximity => _scanRange - _trafficScanner[0].distance;
internal RaycastHit2D[] TrafficScanner => _trafficScanner;
internal bool DetectsTraffic()
{
Array.Clear(_trafficScanner, 0, _trafficScanner.Length);
Physics2D.RaycastNonAlloc(ScanOrigin, Vector2.right, _trafficScanner, _scanRange, _scanFilter.layerMask);
return _trafficScanner[0].collider != null;
}
#if UNITY_EDITOR
private void OnDrawGizmos()
=> Gizmos.DrawLine
(
new Vector3(_bumper.bounds.max.x + 0.1f, _bumper.bounds.center.y, 0f),
new Vector3(_bumper.bounds.max.x + _scanRange, _bumper.bounds.center.y, 0f)
);
#endif
}
using UnityEngine;
/// <Summary>
/// Soundbank and randomizer for Vehicle audio effects
/// </Summary>
public class VehicleSound : MonoBehaviour
{
[Header("Engine")]
[SerializeField] AudioSource _engineSource;
[SerializeField] SoundKit _engineKit;
[Header("Horn")]
[SerializeField] AudioSource _hornSource;
[SerializeField] SoundKit _hornKit;
Rigidbody2D _rigidbody;
void Start()
=> _rigidbody = GetComponent<Rigidbody2D>();
void OnEnable()
{
var clip = _engineKit.clips[Random.Range(0, _engineKit.clips.Length)];
if (clip != null)
_engineSource.clip = clip;
_engineSource.loop = true;
_engineSource.Play();
_hornSource.pitch = Random.Range(0.85f, 1.15f);
_hornSource.loop = false;
}
private void FixedUpdate()
=> _engineSource.pitch = 1 + _rigidbody.velocity.x * 0.1f;
internal float HonkHorn()
{
var clip = _hornKit.clips[Random.Range(0, _hornKit.clips.Length)];
_hornSource.PlayOneShot(clip);
return clip.length / _hornSource.pitch;
}
[ContextMenu("Validate")]
private void OnValidate()
{
_engineSource = transform.GetChild(2).GetComponent<AudioSource>();
if (_engineSource != null)
_hornSource = _engineSource.transform.GetChild(0).GetComponent<AudioSource>();
}
}
using System.Collections;
using UnityEngine;
/// <Summary>
/// Better than therapy; regulates Driver effects & vehicle explosions
/// </Summary>
internal class AngerManagement : MonoBehaviour
{
[Header("Config")]
[SerializeField, Range(0f, 1.0f)] float _roadRage;
[SerializeField, Range(0f, 0.1f)] float _rageInterval = 0.1f;
[SerializeField, Range(0f, 0.1f)] float _rageCooldown = 0.03f;
[SerializeField, Range(0f, 0.1f)] float _rageOnCollision = 0.1f;
[SerializeField, Range(0f, 0.1f)] float _rageWhenBoarded = 0.02f;
[SerializeField, Range(0f, 0.1f)] float _rageWhenTraffic = 0.01f;
WaitForSeconds _interval;
[Header("Fury FX")]
[Tooltip("The minimum Rage required for Fury effects to be triggered")]
[SerializeField, Range(0f, 1f)] float _furyRageThreshold = 0.20f;
[Tooltip("The emission rate of particles at maximum Rage")]
[SerializeField] float _furyEmmisionMax = 2.5f;
ParticleSystem _furyParticles;
ParticleSystem.MainModule _furyMain;
ParticleSystem.EmissionModule _furyEmission;
[Header("Cuss FX")]
[Tooltip("The minimum required Rage required for Cuss effects to be triggered")]
[SerializeField, Range(0f, 1f)] float _cussRageThreshold = 0.45f;
[SerializeField] AudioSource _cussSource;
[Header("Horn FX")]
[Tooltip("The minimum required Rage required for Horn effects to be triggered")]
[SerializeField, Range(0f, 1f)] float _hornRageThreshold = 0.70f;
VehicleSound _sound;
[Header("Boom FX")]
[Tooltip("The minimum required Rage required for Boom effects to be triggered")]
[SerializeField, Range(0f, 1f)] float _boomRageThreshold = 0.95f;
[SerializeField] GameObject _explosionPrefab;
[SerializeField] Transform _explosionOrigin;
[SerializeField] float _explosionDelay = 1f;
VehicleController _controller;
Rigidbody2D _rigidbody;
bool _isBoardedByRunner;
#region Unity Lifecycle Methods
private void Awake()
{
_interval = new WaitForSeconds(_rageInterval);
_controller = GetComponent<VehicleController>();
_rigidbody = GetComponent<Rigidbody2D>();
_furyParticles = GetComponentInChildren<ParticleSystem>();
_furyEmission = _furyParticles.emission;
_furyMain = _furyParticles.main;
_sound = GetComponentInChildren<VehicleSound>();
}
private void OnEnable()
{
_furyMain.startColor = Color.white;
_furyParticles.Play();
_roadRage = 0f;
StartCoroutine(RageRoutine());
StartCoroutine(FuryRoutine());
StartCoroutine(CussRoutine());
StartCoroutine(HonkRoutine());
StartCoroutine(BoomRoutine());
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.CompareTag("Vehicle"))
Explode();
else _roadRage += _rageOnCollision;
if (collision.gameObject.CompareTag("Runner"))
{
if (_roadRage > _boomRageThreshold)
Invoke(nameof(Explode), 0.1f);
_isBoardedByRunner = true;
_roadRage = Mathf.Clamp01(_roadRage * 2);
}
}
private void OnCollisionExit2D(Collision2D collision)
{
if (collision.gameObject.CompareTag("Runner"))
_isBoardedByRunner = false;
}
#endregion
private IEnumerator RageRoutine()
{
while (true)
{
yield return _interval;
if (_isBoardedByRunner)
{
_roadRage += _rageWhenBoarded;
continue;
}
if (_roadRage < 0.9f && _rigidbody.velocity.x < _controller.TargetVelocity.x)
{
_roadRage += _rageWhenTraffic;
continue;
}
if (_roadRage > 0)
_roadRage -= _rageCooldown;
}
}
private IEnumerator FuryRoutine()
{
while (true)
{
if (_roadRage >= _furyRageThreshold)
_furyEmission.rateOverTime = _roadRage * _furyEmmisionMax;
else _furyEmission.rateOverTime = 0f;
yield return _interval;
}
}
private IEnumerator CussRoutine()
{
_cussSource.pitch = Random.Range(0.5f, 1f);
while (true)
{
_cussSource.volume = _roadRage >= _cussRageThreshold ? Mathf.Clamp01(_roadRage) : 0f;
yield return _interval;
}
}
private IEnumerator HonkRoutine()
{
while (true)
{
if (_roadRage >= _hornRageThreshold)
{
var delay = _sound.HonkHorn();
yield return new WaitForSeconds
(Random.Range(delay, delay * 2f));
}
else yield return _interval;
}
}
private IEnumerator BoomRoutine()
{
while (true)
{
if (_roadRage >= _boomRageThreshold)
{
_furyMain.startColor = Color.red;
yield return new WaitForSeconds(_explosionDelay);
Explode();
}
else yield return _interval;
}
}
internal void Explode()
{
var instance = Instantiate(_explosionPrefab, _explosionOrigin.position, Quaternion.identity);
instance.GetComponent<Explosion>().SyncRenderer(transform.GetChild(1).GetComponent<SpriteRenderer>());
gameObject.SetActive(false);
}
}
using UnityEngine;
/// <Summary>
/// Knocks back the Runner from point of detonation
/// </Summary>
[RequireComponent(typeof(SpriteRenderer))]
public class Explosion : MonoBehaviour
{
[SerializeField] AudioClip[] _explosionClips;
[SerializeField] AudioSource _explosionSource;
[SerializeField] float _explosionForce = 5f;
[SerializeField] float _explosionRadius = 5f;
SpriteRenderer _renderer;
void Awake()
=> _renderer = GetComponent<SpriteRenderer>();
void OnEnable()
{
_explosionSource.clip = _explosionClips[Random.Range(0, _explosionClips.Length)];
_explosionSource.Play();
RaycastHit2D[] hitScan = Physics2D.CircleCastAll
(transform.position, _explosionRadius, Vector2.zero, 0f, LayerMask.GetMask("Runner"));
foreach (var hit in hitScan)
if (hit.rigidbody != null)
{
Vector2 direction = hit.transform.position - transform.position;
float force = _explosionForce * ((_explosionRadius - direction.magnitude) / _explosionRadius);
hit.rigidbody.AddForce(Vector2.up * force, ForceMode2D.Impulse);
}
Destroy(gameObject, 5f);
}
internal void SyncRenderer(SpriteRenderer renderer)
{
_renderer.sortingLayerName = renderer.sortingLayerName;
_renderer.sortingOrder = renderer.sortingOrder;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <Summary>
/// Manages vehicles via object pool
/// </Summary>
public class TrafficDispatcher : MonoBehaviour
{
internal Transform Parkade => _parkade;
[SerializeField] Transform _parkade;
[SerializeField] Transform _westSpawnPoint;
[SerializeField] Transform _eastSpawnPoint;
[SerializeField] ContactFilter2D _trafficScanFilter;
[SerializeField] int _maxVehiclesInLane = 10;
[SerializeField] float _recallRange = 25f;
[SerializeField] float _dispatchInterval = 0.25f;
[SerializeField] List<VehicleController> _carPool = new List<VehicleController>();
List<TrafficTracker> _trackers = new List<TrafficTracker>();
bool _spawnToggle;
int _startingPoolCount;
void Awake()
{
foreach (var vehicle in _carPool)
{
var tracker = vehicle.gameObject.AddComponent<TrafficTracker>();
tracker.Initialize(this, _recallRange);
_trackers.Add(tracker);
RecallVehicle(vehicle);
}
_startingPoolCount = _carPool.Count;
StartCoroutine(DispatchRoutine());
}
IEnumerator DispatchRoutine()
{
//One-Time yield to stagger routines across LaneSpawners
yield return new WaitForSeconds(Random.Range(0, _dispatchInterval));
var interval = new WaitForSeconds(_dispatchInterval);
while (true)
{
if (_startingPoolCount - _carPool.Count < _maxVehiclesInLane)
{
var vehicle = _carPool[Random.Range(0, _carPool.Count - 1)];
if (_spawnToggle)
{
if (CheckForCongestionAtSpawn(_eastSpawnPoint.position))
DispatchVehicle(vehicle, _eastSpawnPoint.position);
}
else if (CheckForCongestionAtSpawn(_westSpawnPoint.position))
DispatchVehicle(vehicle, _westSpawnPoint.position);
}
_spawnToggle = !_spawnToggle;
yield return interval;
}
}
bool CheckForCongestionAtSpawn(Vector3 spawnPoint)
{
var trafficScanner = new RaycastHit2D[1];
return Physics2D.Raycast(spawnPoint, Vector2.left, _trafficScanFilter, trafficScanner, 10f) == 0
&& Physics2D.Raycast(spawnPoint, Vector2.right, _trafficScanFilter, trafficScanner, 10f) == 0;
}
void DispatchVehicle(VehicleController vehicle, Vector3 spawnPoint)
{
_carPool.Remove(vehicle);
vehicle.transform.position = spawnPoint;
vehicle.gameObject.SetActive(true);
}
internal void RecallVehicle(VehicleController vehicle)
{
if (!_carPool.Contains(vehicle))
{
_carPool.Add(vehicle);
vehicle.transform.position = _parkade.position;
vehicle.gameObject.SetActive(false);
}
}
void OnDestroy()
{
foreach (var tracker in _trackers)
Destroy(tracker);
}
}
using UnityEngine;
/// <Summary>
/// Applied to pooled vehicles at runtime for self-recycling
/// </Summary>
[RequireComponent(typeof(VehicleController))]
public class TrafficTracker : MonoBehaviour
{
TrafficDispatcher _dispatcher;
VehicleController _vehicle;
float _recallRange;
internal void Initialize(TrafficDispatcher dispatcher, float recallRange)
{
_dispatcher = dispatcher;
_vehicle = GetComponent<VehicleController>();
_recallRange = recallRange;
}
void Update()
{
if (Mathf.Abs(transform.position.x - _dispatcher.Parkade.position.x) > _recallRange)
_dispatcher.RecallVehicle(_vehicle);
}
}
using UnityEngine;
/// <Summary>
/// Runs everytime a vehicle spawns to cycle through various colors
/// </Summary>
[RequireComponent(typeof(SpriteRenderer))]
public class SpriteSwapper : MonoBehaviour
{
[SerializeField] SpriteRenderer _renderer;
[SerializeField] Sprite[] _sprites;
internal void SwapSprite()
=> _renderer.sprite =
_sprites[Random.Range(0, _sprites.Length)];
}
using UnityEngine;
/// <Summary>
/// Backbone of the parallax background
/// </Summary>
public class SpriteParallax : MonoBehaviour
{
[SerializeField] Sprite _sprite;
[SerializeField] SpriteRenderer[] _renderers;
[SerializeField] Transform _refTransform;
Vector3 _refPositionLast;
[SerializeField][Range(-1f, 1f)]
float _parallaxFactorX, _parallaxFactorY;
[SerializeField] bool AutoSnapX, AutoSnapY;
float _spriteSizeX, _spriteSizeY;
private void Start()
{
TrackLastPosition();
var texture = _sprite.texture;
_spriteSizeX = texture.width / _sprite.pixelsPerUnit * transform.localScale.x;
_spriteSizeY = texture.height / _sprite.pixelsPerUnit * transform.localScale.y;
HotLoadSprite();
}
private void HotLoadSprite()
{
if (_renderers.Length != 0)
foreach (var renderer in _renderers)
{
renderer.sprite = _sprite;
renderer.sortingOrder = transform.GetSiblingIndex();
}
}
void LateUpdate()
{
Vector3 deltaPos = _refTransform.position - _refPositionLast;
transform.position += new Vector3
(deltaPos.x * _parallaxFactorX, deltaPos.y * _parallaxFactorY, 0f);
if (AutoSnapX)
if (Mathf.Abs(_refTransform.position.x - transform.position.x) >= _spriteSizeX)
transform.position = new Vector3(_refTransform.position.x, transform.position.y, transform.position.z);
if (AutoSnapY)
if (Mathf.Abs(_refTransform.position.y - transform.position.y) >= _spriteSizeY)
transform.position = new Vector3(transform.position.x, _refTransform.position.y, transform.position.z);
TrackLastPosition();
}
void TrackLastPosition()
=> _refPositionLast = _refTransform.position;
private void OnValidate()
{
gameObject.name = $"{transform.GetSiblingIndex()}: {_sprite.name}";
HotLoadSprite();
}
}
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.UI;
/// <Summary>
/// Manages game settings, controls, and reloading
/// </Summary>
[RequireComponent(typeof(SimpleInput))]
public class GameManager : MonoBehaviour
{
[Header("UserInterface")]
[SerializeField] GameObject _wipeoutUI;
[Header("AudioControl")]
[SerializeField] Slider _slider;
[SerializeField] float _volumeIncrement = 0.05f;
List<RunnerEntity> _runners = new List<RunnerEntity>();
SimpleInput _input;
bool isPaused;
private void Awake()
=> Time.fixedDeltaTime = 1f / 60f;
private void Start()
{
_input = GetComponent<SimpleInput>();
_runners = FindObjectsOfType<RunnerEntity>().ToList();
foreach (var runner in _runners)
runner.State.WipedOut += UpdateRunners;
}
private void UpdateRunners(RunnerEntity runner)
{
if (_runners.Contains(runner))
{
runner.State.WipedOut -= UpdateRunners;
_runners.Remove(runner);
}
if (_runners.Count == 0)
{
TimeKeeper.StopRound();
_wipeoutUI.SetActive(true);
}
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.LeftArrow) || _input.WPaddlePush)
Jukebox.PlayPrevious();
if (Input.GetKeyDown(KeyCode.RightArrow) || _input.EPaddlePush)
Jukebox.PlayNext();
if (Input.GetKeyDown(KeyCode.UpArrow) || _input.NPaddlePush)
_slider.value += _volumeIncrement;
if (Input.GetKeyDown(KeyCode.DownArrow) || _input.SPaddlePush)
_slider.value -= _volumeIncrement;
else if (_input.MenuHold && _input.ViewHold)
Application.Quit();
else if (_input.MenuPull || Input.GetKeyDown(KeyCode.Space))
if (_runners.Count == 0)
ResetGame();
if (Input.GetKeyDown(KeyCode.Escape))
{
isPaused = !isPaused;
Time.timeScale = isPaused ? 0 : 1;
}
}
public static void ResetGame()
=> SceneManager.LoadScene(0);
private void OnApplicationQuit()
=> ScoreKeeper.SerializeScores();
}
using System.Collections;
using UnityEngine;
/// <Summary>
/// Crossfades the soundtrack and persists between scene reloads
/// </Summary>
public class Jukebox : MonoBehaviour
{
internal static Jukebox Instance { get; set; }
[SerializeField] bool _autoplayOnStart;
internal enum Side { A, B }
internal Side CurrentSide = Side.A;
[SerializeField] AudioSource _aSide, _bSide;
[SerializeField]
[Range(0.001f, 0.05f)]
float _crossfadeSpeed;
[SerializeField] AudioClip[] _playlist;
int _trackIndex = 0;
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else Destroy(gameObject);
}
void Start()
{
_aSide.clip = _playlist[Random.Range(0, _playlist.Length)];
if (_autoplayOnStart)
QueueTrack(_aSide.clip);
}
internal static void PlayNext()
{
if (Instance._trackIndex == Instance._playlist.Length - 1)
Instance._trackIndex = 0;
else Instance._trackIndex++;
QueueTrack(Instance._playlist[Instance._trackIndex]);
}
internal static void PlayPrevious()
{
if (Instance._trackIndex == 0)
Instance._trackIndex = Instance._playlist.Length - 1;
else Instance._trackIndex--;
QueueTrack(Instance._playlist[Instance._trackIndex]);
}
internal static void QueueTrack(AudioClip clip)
{
switch (Instance.CurrentSide)
{
case Side.A:
Instance._bSide.clip = clip;
Instance._bSide.Play();
Instance.CurrentSide = Side.B;
break;
case Side.B:
Instance._aSide.clip = clip;
Instance._aSide.Play();
Instance.CurrentSide = Side.A;
break;
}
Instance.StartCoroutine(Instance.CrossfadeRoutine());
}
internal IEnumerator CrossfadeRoutine()
{
var interval = new WaitForFixedUpdate();
switch (Instance.CurrentSide)
{
case Side.A:
while (_aSide.volume < 1 && _bSide.volume > 0)
{
_aSide.volume += _crossfadeSpeed;
_bSide.volume -= _crossfadeSpeed;
yield return interval;
}
break;
case Side.B:
while (_bSide.volume < 1 && _aSide.volume > 0)
{
_bSide.volume += _crossfadeSpeed;
_aSide.volume -= _crossfadeSpeed;
yield return interval;
}
break;
}
}
}
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.UI;
/// <Summary>
/// Does what it says on the tin: Controls the volume
/// </Summary>
public class VolumeControl : MonoBehaviour
{
[SerializeField] AudioMixer _mixer;
[SerializeField] Slider _slider;
[SerializeField] string _volumeParameter = "MasterVolume";
[SerializeField] float _volumeMultiplier = 30f;
private void Start()
{
_slider.onValueChanged.AddListener(UpdateMixerVolume);
OnEnable();
}
private void OnDestroy()
=> _slider.onValueChanged.RemoveListener(UpdateMixerVolume);
private void OnEnable()
{
_slider.value = PlayerPrefs.GetFloat(_volumeParameter, _slider.value);
UpdateMixerVolume(_slider.value);
}
private void OnDisable()
=> PlayerPrefs.SetFloat(_volumeParameter, _slider.value);
public void UpdateMixerVolume(float value)
{
if (value == 0f) value = 0.01f;
_mixer.SetFloat(_volumeParameter, Mathf.Log10(value) * _volumeMultiplier);
}
}
using UnityEngine.AI;
using UnityEngine.Events;
using UnityEngine;
/// <Summary>
/// Odds and ends that Unity should already do, but doesn't
/// </Summary>
namespace UnityEngine.Extensions
{
public static class UnityExtensions
{
public static bool Contains(this LayerMask mask, int layer)
=> (mask & 1 << layer) != 0;
public static float GetLength(this NavMeshPath path)
{
float length = 0f;
for (int i = 0; i < path.corners.Length - 1; i++)
length += Vector3.Distance(path.corners[i], path.corners[i + 1]);
return length;
}
public static void ClearChildren(this Transform transform)
{
for (int i = transform.childCount - 1; i >= 0; i--)
GameObject.Destroy(transform.GetChild(i).gameObject);
}
public static void Play(this AudioSource source, AudioClip clip)
{
source.Stop();
source.clip = clip;
source.Play();
}
}
[System.Serializable]
public class DynamicEvent<T> : UnityEvent<T> { }
[System.Serializable]
public class DynamicEvent<T0, T1> : UnityEvent<T0, T1> { }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment