using System; using UnityEngine; using static AffectingForcesManager; using ShipHandling; using Managers; using Unity.Mathematics; using FORGE3D; using PrimeTween; using log4net; using System.Reflection; using System.Collections.Generic; using static ShipSound; public class Ship : MonoBehaviour, IHUDOwner { private static ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); public int InstanceID { get; private set; } public BoostCapacityUI BoostUI { get; set; } public ShipProperties Props; public ShipState State; public ShipInput Input; // Private variables public CameraOperator CameraOperator; public ParticleSystem BoostEffect; public ParticleSystem GravityEffect; public ParticleSystem JetFlameEffect; public ParticleSystem SmokeTrailEffect; public MeshRenderer BodyMeshRenderer; private F3DFXController _fireController; private AffectingForcesManager _forceManager; private Rigidbody _body; // Saves the current input value for thrust private bool _canBoost = true; private TackleDetection[] _tackleDetectors; private bool _isCriticalTackle = false; private bool _isTackled = false; private float _tackledTime = 0f; private Tween _tackleIgnoreTween = new(); // Upcoming zone change private Zone newZone = Zone.NimbleZone; private Dictionary sounds = new(); void Awake() { if (_forceManager == null) { _forceManager = GameObject.FindGameObjectWithTag("ForceManager"). GetComponent(); } _body = GetComponent(); _fireController = GetComponent(); } void Start() { InstanceID = gameObject.GetInstanceID(); State.boostCapacity = Props.maxBoostCapacity; // Get manageable audio instances for the ships sounds foreach (ShipSoundToName stn in Props.audio.shipSounds) { ManageableAudio ma = AudioManager.G.GetLocalSound(stn.soundName, 1, gameObject.transform); sounds[stn.sound] = ma; } // Register the ship with the camera CameraOperator.AddCharacter(gameObject); // Connect the tackling/tackled logic to the ships detection components _tackleDetectors = GetComponentsInChildren(); foreach (TackleDetection td in _tackleDetectors) { td.TackledResponse += TackledResponse; td.TacklingResponse += TacklingResponse; } } void OnDestroy() { foreach (TackleDetection td in _tackleDetectors) { td.TackledResponse = null; td.TacklingResponse = null; } } // Update is called once per frame void FixedUpdate() { newZone = _forceManager.GetZoneOfInstance(InstanceID); // TODO: This could be more elegant maybe? if (MatchManager.G.matchState != MatchState.Match || State.IsFrozen) { _body.constraints = RigidbodyConstraints.FreezeAll; UpdateSounds(); State.Zone = newZone; return; } _body.constraints = RigidbodyConstraints.None; UpdateSounds(); if (State.Zone != newZone) { State.Zone = newZone; } UpdateMovement(); BoostStateUpdate(Time.deltaTime); UpdateTackleResponse(_isCriticalTackle); if (!State.IsFiring && Input.shootInput == 1) { State.IsFiring = true; _fireController.Fire(); } // Stop firing if (State.IsFiring && Input.shootInput < 1) { State.IsFiring = false; _fireController.Stop(); } } /// /// Movement logic and simulation of the ship. /// void UpdateMovement() { //Debug.Log("inupdatemove " + currentThrustInput); // Player rotation is always possible and same speed transform.Rotate(0, 0, -Props.steerVelocity * Input.steerInput * Time.deltaTime); // Get and apply the current Gravity Transform gravitySource = _forceManager.GetGravitySourceForInstance(InstanceID); State.currentGravity = _forceManager.GetGravityForInstance(InstanceID)(gravitySource, transform); _body.AddForce(State.currentGravity, ForceMode.Acceleration); float stunFactor = _isCriticalTackle ? Props.stunLooseControlFactor : 1f; float thrust = IsBoosting() ? 1f : Input.thrustInput; Vector3 acceleration = Props.thrustAcceleration * thrust * Time.deltaTime * transform.up * stunFactor; Vector3 currentVelocity = _body.velocity; Vector3 boostedAcceleration = BoostAcceleration(acceleration, State.currentGravity); if (!_isCriticalTackle) { // Add drag if (State.Zone == Zone.NimbleZone) { Vector3 dragDecceleration = DragDecceleration(currentVelocity, State.Zone); _body.AddForce(dragDecceleration, ForceMode.Acceleration); if (!_isTackled) { // Add anti drift acceleration Vector3 driftDampeningAcceleration = DriftDampeningAcceleration(currentVelocity, State.Zone); _body.AddForce(driftDampeningAcceleration, ForceMode.Acceleration); } } if (currentVelocity.magnitude <= Props.normalMaxVelocity || IsBoosting() || State.Zone != Zone.NimbleZone) { _body.AddForce(boostedAcceleration, ForceMode.Acceleration); } if (currentVelocity.magnitude >= Props.absolutMaxVelocity && State.Zone == Zone.NimbleZone) { _body.velocity = _body.velocity.normalized * Props.absolutMaxVelocity; } } // Default torque drag _body.AddRelativeTorque(_body.angularVelocity * -Props.torqueDrag, ForceMode.Acceleration); Debug.DrawRay(transform.position, transform.up * (currentVelocity.magnitude + 3) * 0.5f, Color.black); // Fix the ship to the virtual 2D plane of the game transform.localEulerAngles = new Vector3(0, 0, transform.localEulerAngles.z); _body.transform.localPosition = new Vector3(_body.transform.localPosition.x, _body.transform.localPosition.y, 0); } /// /// Calculates a vector to mitigate the ship drifting when it's changing direction. /// /// Current velocity of the ship /// Zone which the ship is in /// Vector3 DriftDampeningAcceleration(Vector3 currentVelocity, Zone zone) { Vector3 antiDriftVelocity; float antiDriftFactor; // Cancel out inertia/drifting Vector3 up = transform.up; Vector3 driftVelocity = currentVelocity - Vector3.Project(currentVelocity, up); if (driftVelocity.magnitude < 0.1) { return Vector3.zero; } antiDriftVelocity = Vector3.Reflect(-driftVelocity, up) - driftVelocity; antiDriftFactor = Mathf.InverseLerp(Props.absolutMaxVelocity, Props.normalMaxVelocity, currentVelocity.magnitude); antiDriftFactor = Mathf.Max(antiDriftFactor, Props.minAntiDriftFactor); Debug.DrawRay(transform.position, currentVelocity.normalized * currentVelocity.magnitude * 2, Color.cyan); Debug.DrawRay(transform.position, driftVelocity.normalized * 5, Color.red); Debug.DrawRay(transform.position, antiDriftVelocity.normalized * 5, Color.green); return antiDriftVelocity * Props.antiDriftAmount * antiDriftFactor; } /// /// Calculates drag on the ship depending on it's velocity and inhabited zone. /// /// Velocity of the ship /// Zone which the ship is in /// Vector3 DragDecceleration(Vector3 currentVelocity, Zone zone) { Vector3 drag = new Vector3(); float minDragFactor = Mathf.InverseLerp(Props.absolutMaxVelocity, Props.normalMaxVelocity, currentVelocity.magnitude); float normalDragFactor = Mathf.InverseLerp(Props.normalMaxVelocity, 0, currentVelocity.magnitude); if (!IsBoosting() && zone == Zone.NimbleZone) { drag -= currentVelocity.normalized * Props.normalDrag; } if (currentVelocity.magnitude >= Props.normalMaxVelocity && zone == Zone.NimbleZone) { drag -= currentVelocity.normalized * Props.maximumDrag; } return drag; } /// /// Is the boost input pressed and boosting possible? /// /// Boosting state bool IsBoosting() { return Input.boostInput > 0 && _canBoost; } /// /// Applies boost to an acceleration vector. /// This includes increasing acceleration and mitigating /// the gravity. /// /// Current acceleration vector /// Gravity vector which is in force /// Vector3 BoostAcceleration(Vector3 acceleration, Vector3 currentGravity) { if (IsBoosting()) { acceleration *= Props.boostMagnitude; acceleration -= currentGravity * Props.boostAntiGravityFactor; } return acceleration; } /// /// Logic which depletes boost capacity when boost conditions are met. /// /// Time delta of the current frame void BoostStateUpdate(float deltaTime) { BoostUI.UpdateFill(Math.Min(State.boostCapacity / Props.maxBoostCapacity, 1)); if (IsBoosting()) { State.boostCapacity -= deltaTime; } if (_canBoost && State.Zone == Zone.OutsideZone) { State.boostCapacity -= deltaTime * Props.outsideBoostRate; } if (State.boostCapacity <= 0) { _canBoost = false; } if ((Input.boostInput <= 0 || !_canBoost) && State.Zone == Zone.NimbleZone && State.boostCapacity <= Props.maxBoostCapacity) { State.boostCapacity += deltaTime; } // When your boost capacity is still critical, you can't start boosting immediately again. // TODO: This is not tested well enough with players. if (_canBoost == false && State.boostCapacity >= Props.minBoostCapacity) { _canBoost = true; } } /// /// Logic which sets the tackled member variables and /// updates them over time. /// State logic depends on these variables and is responsible /// for certain tackle behavior. /// /// Use true to process a tackle hit void UpdateTackleResponse(bool gotTackled = false) { if (gotTackled && !_isTackled) { _isTackled = true; _tackledTime = _isCriticalTackle ? Props.tackledCriticalStunTime : Props.tackledBodyStunTime; return; } _tackledTime -= Time.deltaTime; if (_tackledTime <= 0) { _isTackled = false; _isCriticalTackle = false; _tackledTime = 0; } } /// /// Disable tackle responeses for a given time /// async void TemporarilyIgnoreTackles(float duration) { if (_tackleIgnoreTween.isAlive) return; _tackleIgnoreTween = Tween.Delay(duration); await _tackleIgnoreTween; } private bool IgnoreTackle() { return _tackleIgnoreTween.isAlive; } /// /// Response logic if the ship is tackling an opponend. /// void TacklingResponse() { if (IgnoreTackle()) return; Log.Debug($"{Props.shipName} is tackling."); TemporarilyIgnoreTackles(Props.tacklingGraceTime); } /// /// Called by the collision regions of the ship being tackled by an opponent. /// Adds resulting forces to the ship and intiates the tackle response. /// /// Kind of the tackle. Depends on collision region. /// Object which has collided with the collision region. void TackledResponse(TackleKind tackleKind, Collider collider) { if (IgnoreTackle()) return; TemporarilyIgnoreTackles(Props.tackledGraceTime); float tacklePowerFactor = Props.criticalTacklePowerFactor; if (tackleKind == TackleKind.IncomingCritical) { _isCriticalTackle = true; Log.Debug($"{Props.shipName} has been tackled critically."); } else if (tackleKind == TackleKind.IncomingNormal) { _isCriticalTackle = false; tacklePowerFactor = Props.normalTacklePowerFactor; Log.Debug($"{Props.shipName} has been tackled."); } Vector3 colliderVelocity = collider.attachedRigidbody.velocity; //Log.Debug("velocity " + colliderVelocity); //Log.Debug("angle " + angle); //Log.Debug("outvector " + outVector); Vector3 force = colliderVelocity * tacklePowerFactor; Vector3 resultForce = force / Math.Max(force.magnitude / 4000, 1); resultForce = resultForce / Math.Max(0.001f, Math.Min(resultForce.magnitude / 500, 1)); Log.Debug(resultForce.magnitude); _body.AddForce(resultForce, ForceMode.Acceleration); UpdateTackleResponse(true); } void UpdateSounds() { if (MatchManager.G.matchState != MatchState.Match || State.IsFrozen) { if (newZone != State.Zone && newZone == Zone.NimbleZone) { AudioManager.G.BroadcastAudioEffect(AudioEffects.LowPass, transform, false); } sounds[Thruster].StopAudio(); GravityEffect.Clear(); GravityEffect.Stop(); return; } float velocityFactor = math.smoothstep(0, Props.absolutMaxVelocity, _body.velocity.magnitude); if (math.abs(Input.thrustInput) > 0 || IsBoosting()) { sounds[Thruster].PlayAudio(true); sounds[Thruster].ChangePitch(velocityFactor); if (!JetFlameEffect.isPlaying) JetFlameEffect.Play(); } else { sounds[Thruster].FadeOutAudio(0.3f); JetFlameEffect.Stop(); } if (IsBoosting()) { if (!BoostEffect.isPlaying) BoostEffect.Play(); if (!SmokeTrailEffect.isPlaying) SmokeTrailEffect.Play(); if (JetFlameEffect.isPlaying) JetFlameEffect.transform.localScale = new Vector3(1.3f, 2, 1); sounds[Booster].PlayAudio(false, true); } else { sounds[Booster].ResetOneShot(); SmokeTrailEffect.Stop(); JetFlameEffect.transform.localScale = new Vector3(1.3f, 1, 1); } if (_isTackled && !_isCriticalTackle) { sounds[Tackling].PlayAudio(false, true); CameraOperator.ShakeCam(0.2f); } if (_isCriticalTackle) { sounds[TacklingCritical].PlayAudio(false, true); CameraOperator.ShakeCam(0.4f); } if (!_isTackled) { sounds[TacklingCritical].ResetOneShot(); sounds[Tackling].ResetOneShot(); } if (newZone != State.Zone && State.Zone != Zone.UninitializedZone && newZone != Zone.UninitializedZone) { if (newZone != Zone.NimbleZone) { sounds[LeaveZone].ChangePitch(velocityFactor); sounds[LeaveZone].PlayAudio(false); AudioManager.G.BroadcastAudioEffect(AudioEffects.LowPass, transform, true); } else { sounds[EnterZone].ChangePitch(velocityFactor); sounds[EnterZone].PlayAudio(false); AudioManager.G.BroadcastAudioEffect(AudioEffects.LowPass, transform, false); } } if (GravityEffect == null) { return; } if (!GravityEffect.isPlaying && State.currentGravity != Vector3.zero) { GravityEffect.Play(); } else if (State.currentGravity == Vector3.zero) { GravityEffect.Stop(); } if (GravityEffect.isPlaying) { float gravityAngle = Vector3.SignedAngle(transform.parent.up, State.currentGravity, transform.forward); GravityEffect.gameObject.transform.localEulerAngles = new Vector3(0, 0, gravityAngle - transform.localEulerAngles.z); } } }