using System; using UnityEngine; using static AffectingForcesManager; using ShipHandling; using Managers; using Unity.Mathematics; using FishNet.Object; using FORGE3D; using PrimeTween; using log4net; using System.Reflection; using UnityEngine.Rendering.PostProcessing; using FishNet.Object.Prediction; using GameKit.Dependencies.Utilities; using log4net.Filter; using FishNet.Transporting; public class PredictedShip : NetworkBehaviour, IHUDOwner { private static ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); public event Action BoostUpdated; public event Action LifeUpdated; public int InstanceID { get; private set; } public ShipProperties props; public ShipState state; public ShipInput input; public BoostCapacityUI BoostUI { get; set; } // Private variables public CameraOperator cameraOperator; public ParticleSystem boostEffect; public ParticleSystem gravityEffect; public ParticleSystem jetFlameEffect; public ParticleSystem smokeTrailEffect; public F3DFXController fireController; public MeshRenderer bodyMeshRenderer; private AffectingForcesManager forceManager; // Saves the current input value for thrust private bool canBoost = true; private HitDetection[] tackleDetectors; private bool isCriticalTackle = false; private bool isTackled = false; private float tackledTime = 0f; private Tween tackleIgnoreTween = new(); private bool isFiring = false; // Current Zone the player occupies private Zone zone = Zone.NimbleZone; // Upcoming zone change private Zone newZone = Zone.NimbleZone; private ManageableAudio ThrusterSound; private ManageableAudio BoosterSound; private ManageableAudio LeaveZoneSound; private ManageableAudio EnterZoneSound; private ManageableAudio TackleOpponentSound; private ManageableAudio CriticalTackleOpponentSound; private ManageableAudio BeingTackledSound; private ManageableAudio BeingCriticallyTackledSound; private ManageableAudio CrashOutOfBoundsSound; public PredictionRigidbody PredictionRigidbody; public GameObject smoothedRepresentation; private Rigidbody body; void Awake() { if (forceManager == null) { forceManager = GameObject.FindGameObjectWithTag("ForceManager"). GetComponent(); } ThrusterSound = AudioManager.G.GetLocalSound("thruster", 1, gameObject.transform); BoosterSound = AudioManager.G.GetLocalSound("booster", 1, gameObject.transform); BeingTackledSound = AudioManager.G.GetLocalSound("normal_tackle", 1, gameObject.transform); BeingCriticallyTackledSound = AudioManager.G.GetLocalSound("critical_tackle", 1, gameObject.transform); LeaveZoneSound = AudioManager.G.GetLocalSound("zone_change_out", 1, gameObject.transform); EnterZoneSound = AudioManager.G.GetLocalSound("zone_change_in", 1, gameObject.transform); PredictionRigidbody = ObjectCaches.Retrieve(); PredictionRigidbody.Initialize(GetComponent()); body = GetComponent(); } // Start is called before the first frame update void Start() { InstanceID = gameObject.GetInstanceID(); state.BoostCapacity = props.MaxBoostCapacity; // boostUI.SetMinBoostRatio(props.minBoostCapacity / props.maxBoostCapacity); // GameManager.GM.RegisterPlayer(this); cameraOperator.AddCharacter(smoothedRepresentation); tackleDetectors = GetComponentsInChildren(); foreach (HitDetection td in tackleDetectors) { td.TackledResponse += TackledResponse; td.TacklingResponse += TacklingResponse; } } public override void OnStartNetwork() { base.TimeManager.OnTick += TimeManager_OnTick; base.TimeManager.OnPostTick += TimeManager_OnPostTick; } public override void OnStopNetwork() { base.TimeManager.OnTick -= TimeManager_OnTick; base.TimeManager.OnPostTick -= TimeManager_OnPostTick; } void OnDestroy() { ObjectCaches.StoreAndDefault(ref PredictionRigidbody); foreach (HitDetection td in tackleDetectors) { td.TackledResponse = null; td.TacklingResponse = null; } LifeUpdated = null; BoostUpdated = null; } private void TimeManager_OnTick() { RunInputs(CreateReplicateData()); } private ReplicateData CreateReplicateData() { //if (!base.IsOwner) //return default; ReplicateData rd = new ReplicateData(input.thrustInput, input.steerInput, input.boostInput); return rd; } [Replicate] private void RunInputs(ReplicateData rd, ReplicateState rstate = ReplicateState.Invalid, Channel channel = Channel.Reliable) { //Debug.Log("inupdatemove " + currentThrustInput); // Player rotation is always possible and same speed transform.Rotate(0, 0, (float)(-props.SteerVelocity * rd.Steer * 2 * TimeManager.TickDelta)); //PredictionRigidbody.AddTorque(new Vector3(0, 0, -props.steerVelocity * rd.Steer * Time.deltaTime)); // // Get and apply the current Gravity Transform gravitySource = forceManager.GetGravitySourceForInstance(InstanceID); state.CurrentGravity = forceManager.GetGravityForInstance(InstanceID)(gravitySource, transform); PredictionRigidbody.AddForce(state.CurrentGravity, ForceMode.Acceleration); float stunFactor = isCriticalTackle ? props.StunLooseControlFactor : 1f; float thrust = IsBoosting(rd.Boost) ? 1f : rd.Thrust; Vector3 acceleration = props.ThrustAcceleration * thrust * (float)TimeManager.TickDelta * transform.up * stunFactor * 2; Vector3 currentVelocity = body.velocity; Vector3 boostedAcceleration = BoostAcceleration(acceleration, state.CurrentGravity); if (!isCriticalTackle) { // Add drag if (zone == Zone.NimbleZone) { Vector3 dragDecceleration = DragDecceleration(currentVelocity, zone); PredictionRigidbody.AddForce(dragDecceleration, ForceMode.Acceleration); if (!isTackled) { // Add anti drift acceleration Vector3 driftDampeningAcceleration = DriftDampeningAcceleration(currentVelocity, zone); PredictionRigidbody.AddForce(driftDampeningAcceleration, ForceMode.Acceleration); } } if (currentVelocity.magnitude <= props.NormalMaxVelocity || IsBoosting(rd.Boost) || zone != Zone.NimbleZone) { PredictionRigidbody.AddForce(boostedAcceleration, ForceMode.Acceleration); } if (currentVelocity.magnitude >= props.AbsolutMaxVelocity && zone == Zone.NimbleZone) { body.velocity = body.velocity.normalized * props.AbsolutMaxVelocity; } } // Default torque drag PredictionRigidbody.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(0, 0, transform.localPosition.z); PredictionRigidbody.Simulate(); } private void TimeManager_OnPostTick() { CreateReconcile(); } public override void CreateReconcile() { ReconcileData md = new ReconcileData(PredictionRigidbody); ReconcileState(md); } [Reconcile] private void ReconcileState(ReconcileData rst, Channel channel = Channel.Unreliable) { PredictionRigidbody.Reconcile(rst.PredictionRigidbody); } // Update is called once per frame void FixedUpdate() { // TODO: This belongs in the state object newZone = forceManager.GetZoneOfInstance(InstanceID); // TODO: This could be more elegant maybe? if (MatchManager.G.matchState != MatchState.Match || state.IsFrozen) { body.constraints = RigidbodyConstraints.FreezeAll; UpdateSounds(); zone = newZone; return; } body.constraints = RigidbodyConstraints.None; UpdateSounds(); if (zone != newZone) { zone = newZone; } //UpdateMovement(); BoostStateUpdate(Time.deltaTime); UpdateTackleResponse(isCriticalTackle); if (!isFiring && input.shootInput == 1) { isFiring = true; fireController.Fire(); } // Stop firing if (isFiring && input.shootInput < 1) { 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(input.boostInput) ? 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 (zone == Zone.NimbleZone) { Vector3 dragDecceleration = DragDecceleration(currentVelocity, zone); body.AddForce(dragDecceleration, ForceMode.Acceleration); if (!isTackled) { // Add anti drift acceleration Vector3 driftDampeningAcceleration = DriftDampeningAcceleration(currentVelocity, zone); body.AddForce(driftDampeningAcceleration, ForceMode.Acceleration); } } if (currentVelocity.magnitude <= props.NormalMaxVelocity || IsBoosting(input.boostInput) || zone != Zone.NimbleZone) { body.AddForce(boostedAcceleration, ForceMode.Acceleration); } if (currentVelocity.magnitude >= props.AbsolutMaxVelocity && 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(0, 0, transform.localPosition.z); } /// /// 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(input.boostInput) && 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(float input) { return input > 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(input.boostInput)) { 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(input.boostInput)) { state.BoostCapacity -= deltaTime; } if (canBoost && zone == Zone.OutsideZone) { state.BoostCapacity -= deltaTime * props.OutsideBoostRate; } if (state.BoostCapacity <= 0) { canBoost = false; } if ((input.boostInput <= 0 || !canBoost) && 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 != zone && newZone == Zone.NimbleZone) { AudioManager.G.BroadcastAudioEffect(AudioEffects.LowPass, transform, false); } ThrusterSound.StopAudio(); gravityEffect.Clear(); gravityEffect.Stop(); return; } float velocityFactor = math.smoothstep(0, props.AbsolutMaxVelocity, body.velocity.magnitude); if (math.abs(input.thrustInput) > 0 || IsBoosting(input.boostInput)) { ThrusterSound.PlayAudio(true); ThrusterSound.ChangePitch(velocityFactor); if (!jetFlameEffect.isPlaying) jetFlameEffect.Play(); } else { ThrusterSound.FadeOutAudio(0.3f); jetFlameEffect.Stop(); } if (IsBoosting(input.boostInput)) { if (!boostEffect.isPlaying) boostEffect.Play(); if (!smokeTrailEffect.isPlaying) smokeTrailEffect.Play(); if (jetFlameEffect.isPlaying) jetFlameEffect.transform.localScale = new Vector3(1.3f, 2, 1); BoosterSound.PlayAudio(false, 0.1f, true); } else { BoosterSound.ResetOneShot(); smokeTrailEffect.Stop(); jetFlameEffect.transform.localScale = new Vector3(1.3f, 1, 1); } if (isTackled && !isCriticalTackle) { BeingTackledSound.PlayAudio(false, 0, true); cameraOperator.ShakeCam(0.2f); } if (isCriticalTackle) { BeingCriticallyTackledSound.PlayAudio(false, 0, true); cameraOperator.ShakeCam(0.4f); } if (!isTackled) { BeingCriticallyTackledSound.ResetOneShot(); BeingTackledSound.ResetOneShot(); } if (newZone != zone && zone != Zone.UninitializedZone && newZone != Zone.UninitializedZone) { if (newZone != Zone.NimbleZone) { LeaveZoneSound.ChangePitch(velocityFactor); LeaveZoneSound.PlayAudio(false); AudioManager.G.BroadcastAudioEffect(AudioEffects.LowPass, transform, true); } else { EnterZoneSound.ChangePitch(velocityFactor); EnterZoneSound.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); } } }