-
-
Save CoffeepotCoder/84e30114f522579bd27ddbbfd8cb7365 to your computer and use it in GitHub Desktop.
RoadRAGE: Under the Hood
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ]"; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)]; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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