diff --git a/Assets/Src/PowerUnit.cs b/Assets/Src/PowerUnit.cs index 0f23194..a0533da 100644 --- a/Assets/Src/PowerUnit.cs +++ b/Assets/Src/PowerUnit.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using UnityEngine; public enum Output { @@ -9,6 +11,10 @@ public enum Output { } public class PowerUnit { + static float stablisation = 75f; + public float Stabilisation => stablisation; + + // Outputs static float baseOutput = 25.0f; static float initialAcceleration = 2f * baseOutput; static float initialDeceleration = 4f * baseOutput; @@ -18,94 +24,71 @@ public class PowerUnit { float deceleration = initialDeceleration; float strafing = initialStrafing; + // public float Acceleration => acceleration; + // public float Deceleration => deceleration; + // public float Strafing => strafing; + + // Shields static float initialShieldMax = 5000f; float shieldCurrent = initialShieldMax; float shieldMax = initialShieldMax; float shieldRegen = 10f; // * 100 ticks per second = 5s fully recharge - int boostMax = 2; - int boostCharges = 0; - - static float stablisation = 75f; - - public float Acceleration => acceleration; - public float Deceleration => deceleration; - public float Strafing => strafing; public float Shield => shieldCurrent; public float ShieldMax => shieldMax; - public float Stabilisation => stablisation; - public string ShieldPct => $"{((shieldCurrent / shieldMax) * 100f):0}"; - public string BoostsAvailable => $"{boostCharges}"; - public bool BoostRecharging => boostCharges < boostMax; + float ShieldRegen => (shieldMax / 5f) * Time.fixedDeltaTime; - public void Init() { - boostCharges = 0; + // Boost + List<(Output output, double expiresAt)> boostActive = new(); + static double boostFullChargeSeconds = 10f; + static double boostDurationSeconds = 2f; + double boostMax = 2f * boostDurationSeconds; + double boostAvailable = 0f; + + // public string BoostAvailable => $"{boostAvailable}"; + public string BoostAvailable => $"{((boostAvailable / boostMax) * 100f):0}"; + + public float Acceleration => + acceleration + boostActive.Count(b => b.output == Output.Acceleration) * baseOutput; + + public float Deceleration => + deceleration + boostActive.Count(b => b.output == Output.Deceleration) * baseOutput; + + public float Strafing => + strafing + boostActive.Count(b => b.output == Output.Strafing) * baseOutput; + + double BoostRegen => (boostMax / boostFullChargeSeconds) * Time.fixedDeltaTime; + + public void Reset() { shieldCurrent = initialShieldMax; acceleration = initialAcceleration; deceleration = initialDeceleration; strafing = initialStrafing; + + boostActive = new(); } - // Regeneration of boosts is tracked by the ShipControls - // it has to handle tracking the coroutines etc - public void BoostGenerate() { - boostCharges = Math.Min(boostCharges + 1, boostMax); - Debug.Log($"Boosts available {boostCharges}"); + public void Deploy(Output output) { + boostActive.Add((output, Time.timeAsDouble + boostDurationSeconds)); } - public bool BoostDeploy(Output output) { - if (boostCharges < 1) - return false; + public void Tick() { + // remove expired boosts + boostActive = boostActive.Where(b => b.expiresAt > Time.timeAsDouble).ToList(); - float newOutput = output switch { - Output.Acceleration => BoostDeployAcceleration(), - Output.Deceleration => BoostDeployDeceleration(), - Output.Strafing => BoostDeployStrafing(), - Output.Shield => BoostDeployShields(), - _ => 0f, + double delta = boostActive.Count switch { + // if there are no boosts active we can increase the store + 0 => BoostRegen, + // if there are active boosts we deplete it + int count => count * Time.fixedDeltaTime, }; - // Boosts are immediately used and tracked here - boostCharges--; + double next = Math.Min(boostAvailable + delta, boostMax); + boostAvailable = Math.Max(0f, next); - Debug.Log($"Set {output} to {newOutput}"); - - return true; - } - - float BoostDeployAcceleration() => acceleration += baseOutput; - float BoostDeployDeceleration() => deceleration += baseOutput; - float BoostDeployStrafing() => strafing += baseOutput; - float BoostDeployShields() { - float shieldFrac = shieldMax / shieldCurrent; - shieldMax = 7000f; - shieldCurrent = shieldMax * shieldFrac; - ShieldRegenRecalculate(); - return shieldMax; - } - - public void BoostEnd(Output output) { - float newOutput = output switch { - Output.Acceleration => BoostEndAcceleration(), - Output.Deceleration => BoostEndDeceleration(), - Output.Strafing => BoostEndStrafing(), - Output.Shield => BoostEndShields(), - _ => 0f, - }; - - Debug.Log($"Set {output} to {newOutput}"); - } - - float BoostEndAcceleration() => acceleration -= baseOutput; - float BoostEndDeceleration() => deceleration -= baseOutput; - float BoostEndStrafing() => strafing -= baseOutput; - float BoostEndShields() { - float shieldFrac = shieldMax / shieldCurrent; - shieldMax = 5000f; - shieldCurrent = shieldMax * shieldFrac; - ShieldRegenRecalculate(); - return shieldMax; + shieldMax = initialShieldMax + boostActive.Count(b => b.output == Output.Shield) * 1000f; + shieldCurrent = Math.Min(shieldCurrent + ShieldRegen, shieldMax); } // returns whether shield depleted @@ -113,7 +96,4 @@ public class PowerUnit { shieldCurrent = shieldCurrent - amt; return shieldCurrent < 0f; } - - public void ShieldRegenerate() => shieldCurrent = Math.Min(shieldCurrent + shieldRegen, shieldMax); - void ShieldRegenRecalculate() => shieldRegen = (shieldMax / 5f) * Time.fixedDeltaTime; } \ No newline at end of file diff --git a/Assets/Src/ShipControls.cs b/Assets/Src/ShipControls.cs index 6b23aba..06a2df2 100755 --- a/Assets/Src/ShipControls.cs +++ b/Assets/Src/ShipControls.cs @@ -1,270 +1,236 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -using UnityEngine; -using UnityEngine.InputSystem; - -public class ShipControls : MonoBehaviour { - public Camera aim; - public Timing timing; - - SpaceRaceInput input; - - Rigidbody rigidBody; - - Vector3 initialPosition; - Vector3 initialForce; - Quaternion initialAim; - - ForceMode forceMode; - Vector3 thrust; - Vector3 strafe; - - // Camera Controls - float camPitch; - float camYaw; - float clampAngle = 90.0f; - - public PowerUnit powerUnit = new(); - IEnumerator boostDuration; - IEnumerator boostCooldown; - int boostDurationSeconds = 2; - int boostCooldownSeconds = 5; - - void Start() { - Cursor.lockState = CursorLockMode.Locked; - rigidBody = GetComponent(); - - // just stop the body from being affected by rotation for now - // until the 3p camera is in to explain why - rigidBody.freezeRotation = true; - - initialPosition = rigidBody.transform.position; - initialAim = rigidBody.transform.rotation; - initialForce = Vector3.forward; - - thrust = Vector3.zero; - strafe = Vector3.zero; - - camPitch = initialAim.eulerAngles.x; - camYaw = initialAim.eulerAngles.y; - forceMode = ForceMode.Acceleration; - - SetupActions(); - Respawn(); - } - - // to minimise input lag we poll for the thrust vector every frame and set it - // same with the camera, we always look around - void Update() { - UpdateCamera(); - thrust = getThrust(); - } - - void FixedUpdate() { - // when you hit the trigger... GO - // needs to be done here in fixedUpdate because otherwise the input is checked on every frame - // so they will have already hit the button before this tick and they actually start moving - // aka ripoff - bool shouldBegin = !timing.timerEnabled && (thrust != Vector3.zero || strafe != Vector3.zero); - if (shouldBegin) Begin(); - - // the ship being propelled forward - // this is in world space because of the xform from the camera pos - // - rigidBody.AddForce(thrust, forceMode); - - // moving side to side - // this is relative because it's easy to just multiply by the strafe vector - rigidBody.AddRelativeForce(strafe, forceMode); - - // rotating towards the camera's orientation - // not being done by a force because of strange behaviour - // needs investigation - float step = powerUnit.Stabilisation * Time.fixedDeltaTime; - - // stop the ship jiggling because of micro camera movement - if (Quaternion.Angle(aim.transform.rotation, rigidBody.rotation) > 1f) - rigidBody.rotation = Quaternion.RotateTowards(transform.rotation, aim.transform.rotation, step); - - powerUnit.ShieldRegenerate(); - } - - void UpdateCamera() { - // there's some hokey scaling that went on with the old input - // we were originally using - - // in the action mapping the mouse is scaled by 0.05 - // delta *= 0.5f; // Account for scaling applied directly in Windows code by old input system. - // delta *= 0.1f; // Account for sensitivity setting on old Mouse X and Y axes. - // def needs to be configurable - - Vector2 lookDelta = input.ship.look.ReadValue(); - camYaw += lookDelta.x; - camPitch -= lookDelta.y; - camPitch = Mathf.Clamp(camPitch, -clampAngle, clampAngle); - aim.transform.eulerAngles = new Vector3(camPitch, camYaw, 0.0f); - } - - // if you are decelerating you can't accelerate as well - // otherwise you can flip the ship around and - // decelerate + accelerate against the velocity vector - // aka cheating? - Vector3 getThrust() { - float accelerateInput = input.ship.accelerate.ReadValue(); - float decelerateInput = input.ship.decelerate.ReadValue(); - - // decelerates the ship against the direction it is moving - if (decelerateInput > 0f) { - // the velocity is already in world space so we just invert it - return rigidBody.velocity.normalized * -1f * powerUnit.Deceleration * decelerateInput; - } - - // thrust in the direction of the camera - if (accelerateInput > 0f) - // transform from the camera's forward vector into world space - return aim.transform.TransformDirection(Vector3.forward) * powerUnit.Acceleration * accelerateInput; - - return Vector3.zero; - } - - public void Respawn() { - // IMPORTANT - // stop the timer before we move the ship or anything - // otherwise it might generate a checkpoint time - // for the final sector - timing.StopTimer(); - - thrust = new Vector3(0.0f, 0.0f, 0.0f); - transform.position = initialPosition; - rigidBody.transform.forward = initialForce; - rigidBody.velocity = new Vector3(0f, 0f, 0f); - rigidBody.angularVelocity = new Vector3(0f, 0f, 0f); - rigidBody.transform.rotation = initialAim; - aim.transform.rotation = initialAim; - - camPitch = initialAim.eulerAngles.x; - camYaw = initialAim.eulerAngles.y; - - if (boostCooldown != null) StopCoroutine(boostCooldown); - if (boostDuration != null) StopCoroutine(boostDuration); - - powerUnit.Init(); - } - - // stuff we have to do right as they come off the line - void Begin() { - timing.StartTimer(); - - // start with 0 cooldowns and regen them - boostCooldown = BoostCooldown(); - StartCoroutine(boostCooldown); - } - - public float Speed() { - return rigidBody.velocity.magnitude; - } - - public void Damage(float amt, string src) { - Debug.Log($"Damage {amt} from {src}"); - - bool destroyed = powerUnit.ShieldReduce(amt); - if (destroyed) { - Debug.Log($"Ddddestroyed by {src}"); - Respawn(); - } - } - - void BoostDeploy(Output output) { - bool used = powerUnit.BoostDeploy(output); - if (!used) return; - - // stops the cooldown until after our boost finishes - if (boostCooldown != null) StopCoroutine(boostCooldown); - boostDuration = BoostDuration(output); - StartCoroutine(boostDuration); - } - - IEnumerator BoostDuration(Output output) { - Debug.Log($"BoostDuration {output}..."); - yield return new WaitForSeconds(boostDurationSeconds); - BoostEnd(output); - } - - void BoostEnd(Output output) { - Debug.Log($"BoostEnd {output}..."); - powerUnit.BoostEnd(output); - - if (boostCooldown != null) StopCoroutine(boostCooldown); - boostCooldown = BoostCooldown(); - StartCoroutine(boostCooldown); - } - - IEnumerator BoostCooldown() { - while (powerUnit.BoostRecharging) { - yield return new WaitForSeconds(boostCooldownSeconds); - powerUnit.BoostGenerate(); - } - } - - // I prefer doing it this way than registering the callbacks etc - // the control of the actions is more granular - // plus the callbacks don't have to be public, bonus - void SetupActions() { - if (input == null) - input = new SpaceRaceInput(); - - input.ship.Enable(); - - // we have to update strafe the whole time it's being pressed - // including setting it back to zero when released - input.ship.strafe.started += OnStrafe; - input.ship.strafe.performed += OnStrafe; - input.ship.strafe.canceled += OnStrafe; - - // these we only care about the initial press - input.ship.respawn.started += OnRespawn; - input.ship.quit.started += OnQuit; - input.ship.powerUnit1.started += OnPowerUnit1; - input.ship.powerUnit2.started += OnPowerUnit2; - input.ship.powerUnit3.started += OnPowerUnit3; - input.ship.powerUnit4.started += OnPowerUnit4; - } - - void OnStrafe(InputAction.CallbackContext ctx) { - Vector2 str = ctx.ReadValue(); - strafe = new Vector3(str.x, str.y, 0f) * powerUnit.Strafing; - } - - void OnQuit(InputAction.CallbackContext ctx) => Application.Quit(); - void OnRespawn(InputAction.CallbackContext ctx) => Respawn(); - void OnPowerUnit1 (InputAction.CallbackContext ctx) => BoostDeploy(Output.Acceleration); - void OnPowerUnit2 (InputAction.CallbackContext ctx) => BoostDeploy(Output.Deceleration); - void OnPowerUnit3 (InputAction.CallbackContext ctx) => BoostDeploy(Output.Strafing); - void OnPowerUnit4 (InputAction.CallbackContext ctx) => BoostDeploy(Output.Shield); - - void OnDrawGizmosSelected() { - if (Application.isPlaying == false) - return; - - Gizmos.color = Color.red; - Vector3 right = aim.transform.TransformDirection(Vector3.right) * 10; - Gizmos.DrawRay(transform.position, right); - - Gizmos.color = Color.green; - Vector3 direction = aim.transform.TransformDirection(Vector3.up) * 10; - Gizmos.DrawRay(transform.position, direction); - - Gizmos.color = Color.blue; - Vector3 forward = aim.transform.TransformDirection(Vector3.forward) * 10; - Gizmos.DrawRay(transform.position, forward); - - Gizmos.color = Color.yellow; - Vector3 velocity = rigidBody.velocity; - Gizmos.DrawRay(transform.position, velocity); - - Gizmos.color = Color.cyan; - Gizmos.DrawRay(transform.position, thrust); - } +using System; +using System.Collections; +using System.Collections.Generic; + +using UnityEngine; +using UnityEngine.InputSystem; + +public class ShipControls : MonoBehaviour { + public Camera aim; + public Timing timing; + + SpaceRaceInput input; + + Rigidbody rigidBody; + + Vector3 initialPosition; + Vector3 initialForce; + Quaternion initialAim; + + ForceMode forceMode; + Vector3 thrust; + Vector3 strafe; + + // Camera Controls + float camPitch; + float camYaw; + float clampAngle = 90.0f; + + public PowerUnit powerUnit = new(); + int boostDurationSeconds = 2; + int boostCooldownSeconds = 5; + + void Start() { + Cursor.lockState = CursorLockMode.Locked; + rigidBody = GetComponent(); + + // just stop the body from being affected by rotation for now + // until the 3p camera is in to explain why + rigidBody.freezeRotation = true; + + initialPosition = rigidBody.transform.position; + initialAim = rigidBody.transform.rotation; + initialForce = Vector3.forward; + + thrust = Vector3.zero; + strafe = Vector3.zero; + + camPitch = initialAim.eulerAngles.x; + camYaw = initialAim.eulerAngles.y; + forceMode = ForceMode.Acceleration; + + SetupActions(); + Respawn(); + } + + // to minimise input lag we poll for the thrust vector every frame and set it + // same with the camera, we always look around + void Update() { + UpdateCamera(); + thrust = getThrust(); + } + + void FixedUpdate() { + // when you hit the trigger... GO + // needs to be done here in fixedUpdate because otherwise the input is checked on every frame + // so they will have already hit the button before this tick and they actually start moving + // aka ripoff + bool shouldBegin = !timing.timerEnabled && (thrust != Vector3.zero || strafe != Vector3.zero); + if (shouldBegin) Begin(); + + // the ship being propelled forward + // this is in world space because of the xform from the camera pos + // + rigidBody.AddForce(thrust, forceMode); + + // moving side to side + // this is relative because it's easy to just multiply by the strafe vector + rigidBody.AddRelativeForce(strafe, forceMode); + + // rotating towards the camera's orientation + // not being done by a force because of strange behaviour + // needs investigation + float step = powerUnit.Stabilisation * Time.fixedDeltaTime; + + // stop the ship jiggling because of micro camera movement + if (Quaternion.Angle(aim.transform.rotation, rigidBody.rotation) > 1f) + rigidBody.rotation = Quaternion.RotateTowards(transform.rotation, aim.transform.rotation, step); + + if (timing.timerEnabled) { + powerUnit.Tick(); + } + } + + void UpdateCamera() { + // there's some hokey scaling that went on with the old input + // we were originally using + + // in the action mapping the mouse is scaled by 0.05 + // delta *= 0.5f; // Account for scaling applied directly in Windows code by old input system. + // delta *= 0.1f; // Account for sensitivity setting on old Mouse X and Y axes. + // def needs to be configurable + + Vector2 lookDelta = input.ship.look.ReadValue(); + camYaw += lookDelta.x; + camPitch -= lookDelta.y; + camPitch = Mathf.Clamp(camPitch, -clampAngle, clampAngle); + aim.transform.eulerAngles = new Vector3(camPitch, camYaw, 0.0f); + } + + // if you are decelerating you can't accelerate as well + // otherwise you can flip the ship around and + // decelerate + accelerate against the velocity vector + // aka cheating? + Vector3 getThrust() { + float accelerateInput = input.ship.accelerate.ReadValue(); + float decelerateInput = input.ship.decelerate.ReadValue(); + + // decelerates the ship against the direction it is moving + if (decelerateInput > 0f) { + // the velocity is already in world space so we just invert it + return rigidBody.velocity.normalized * -1f * powerUnit.Deceleration * decelerateInput; + } + + // thrust in the direction of the camera + if (accelerateInput > 0f) + // transform from the camera's forward vector into world space + return aim.transform.TransformDirection(Vector3.forward) * powerUnit.Acceleration * accelerateInput; + + return Vector3.zero; + } + + public void Respawn() { + // IMPORTANT + // stop the timer before we move the ship or anything + // otherwise it might generate a checkpoint time + // for the final sector + timing.StopTimer(); + + thrust = new Vector3(0.0f, 0.0f, 0.0f); + transform.position = initialPosition; + rigidBody.transform.forward = initialForce; + rigidBody.velocity = new Vector3(0f, 0f, 0f); + rigidBody.angularVelocity = new Vector3(0f, 0f, 0f); + rigidBody.transform.rotation = initialAim; + aim.transform.rotation = initialAim; + + camPitch = initialAim.eulerAngles.x; + camYaw = initialAim.eulerAngles.y; + + powerUnit.Reset(); + } + + // stuff we have to do right as they come off the line + void Begin() { + timing.StartTimer(); + } + + public float Speed() { + return rigidBody.velocity.magnitude; + } + + public void Damage(float amt, string src) { + Debug.Log($"Damage {amt} from {src}"); + + bool destroyed = powerUnit.ShieldReduce(amt); + if (destroyed) { + Debug.Log($"Ddddestroyed by {src}"); + Respawn(); + } + } + + void Deploy(Output output) { + powerUnit.Deploy(output); + } + + // I prefer doing it this way than registering the callbacks etc + // the control of the actions is more granular + // plus the callbacks don't have to be public, bonus + void SetupActions() { + if (input == null) + input = new SpaceRaceInput(); + + input.ship.Enable(); + + // we have to update strafe the whole time it's being pressed + // including setting it back to zero when released + input.ship.strafe.started += OnStrafe; + input.ship.strafe.performed += OnStrafe; + input.ship.strafe.canceled += OnStrafe; + + // these we only care about the initial press + input.ship.respawn.started += OnRespawn; + input.ship.quit.started += OnQuit; + input.ship.powerUnit1.started += OnPowerUnit1; + input.ship.powerUnit2.started += OnPowerUnit2; + input.ship.powerUnit3.started += OnPowerUnit3; + input.ship.powerUnit4.started += OnPowerUnit4; + } + + void OnStrafe(InputAction.CallbackContext ctx) { + Vector2 str = ctx.ReadValue(); + strafe = new Vector3(str.x, str.y, 0f) * powerUnit.Strafing; + } + + void OnQuit(InputAction.CallbackContext ctx) => Application.Quit(); + void OnRespawn(InputAction.CallbackContext ctx) => Respawn(); + + void OnPowerUnit1(InputAction.CallbackContext ctx) => Deploy(Output.Acceleration); + void OnPowerUnit2(InputAction.CallbackContext ctx) => Deploy(Output.Deceleration); + void OnPowerUnit3(InputAction.CallbackContext ctx) => Deploy(Output.Strafing); + void OnPowerUnit4(InputAction.CallbackContext ctx) => Deploy(Output.Shield); + + void OnDrawGizmosSelected() { + if (Application.isPlaying == false) + return; + + Gizmos.color = Color.red; + Vector3 right = aim.transform.TransformDirection(Vector3.right) * 10; + Gizmos.DrawRay(transform.position, right); + + Gizmos.color = Color.green; + Vector3 direction = aim.transform.TransformDirection(Vector3.up) * 10; + Gizmos.DrawRay(transform.position, direction); + + Gizmos.color = Color.blue; + Vector3 forward = aim.transform.TransformDirection(Vector3.forward) * 10; + Gizmos.DrawRay(transform.position, forward); + + Gizmos.color = Color.yellow; + Vector3 velocity = rigidBody.velocity; + Gizmos.DrawRay(transform.position, velocity); + + Gizmos.color = Color.cyan; + Gizmos.DrawRay(transform.position, thrust); + } } \ No newline at end of file diff --git a/Assets/Src/UI/ControlsUI.cs b/Assets/Src/UI/ControlsUI.cs index b1d2b74..ae81994 100644 --- a/Assets/Src/UI/ControlsUI.cs +++ b/Assets/Src/UI/ControlsUI.cs @@ -45,7 +45,7 @@ public class ControlsUI : MonoBehaviour { speed.text = $"{ship.Speed():0}"; shields.text = ship.powerUnit.ShieldPct; - boosts.text = ship.powerUnit.BoostsAvailable; + boosts.text = ship.powerUnit.BoostAvailable; puAccel.text = ship.powerUnit.Acceleration.ToString(); puDecel.text = ship.powerUnit.Deceleration.ToString(); puStrafe.text = ship.powerUnit.Strafing.ToString();