Space-Smash-Out/Assets/Scripts/Characters/Ship.cs

507 lines
14 KiB
C#

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<ShipSound, ManageableAudio> sounds = new();
void Awake()
{
if (_forceManager == null)
{
_forceManager = GameObject.FindGameObjectWithTag("ForceManager").
GetComponent<AffectingForcesManager>();
}
_body = GetComponent<Rigidbody>();
_fireController = GetComponent<F3DFXController>();
}
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<TackleDetection>();
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();
}
}
/// <summary>
/// Movement logic and simulation of the ship.
/// </summary>
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) * Props.GravitStrength;
_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);
}
/// <summary>
/// Calculates a vector to mitigate the ship drifting when it's changing direction.
/// </summary>
/// <param name="currentVelocity">Current velocity of the ship</param>
/// <param name="zone">Zone which the ship is in</param>
/// <returns></returns>
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;
}
/// <summary>
/// Calculates drag on the ship depending on it's velocity and inhabited zone.
/// </summary>
/// <param name="currentVelocity">Velocity of the ship</param>
/// <param name="zone">Zone which the ship is in</param>
/// <returns></returns>
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;
}
/// <summary>
/// Is the boost input pressed and boosting possible?
/// </summary>
/// <returns>Boosting state</returns>
bool IsBoosting()
{
return Input.boostInput > 0 && _canBoost;
}
/// <summary>
/// Applies boost to an acceleration vector.
/// This includes increasing acceleration and mitigating
/// the gravity.
/// </summary>
/// <param name="acceleration">Current acceleration vector</param>
/// <param name="currentGravity">Gravity vector which is in force</param>
/// <returns></returns>
Vector3 BoostAcceleration(Vector3 acceleration, Vector3 currentGravity)
{
if (IsBoosting())
{
acceleration *= Props.BoostMagnitude;
acceleration -= currentGravity * Props.BoostAntiGravityFactor;
}
return acceleration;
}
/// <summary>
/// Logic which depletes boost capacity when boost conditions are met.
/// </summary>
/// <param name="deltaTime">Time delta of the current frame</param>
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;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="gotTackled">Use true to process a tackle hit</param>
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;
}
}
/// <summary>
/// Disable tackle responeses for a given time
/// </summary>
async void TemporarilyIgnoreTackles(float duration)
{
if (_tackleIgnoreTween.isAlive)
return;
_tackleIgnoreTween = Tween.Delay(duration);
await _tackleIgnoreTween;
}
private bool IgnoreTackle()
{
return _tackleIgnoreTween.isAlive;
}
/// <summary>
/// Response logic if the ship is tackling an opponend.
/// </summary>
void TacklingResponse()
{
if (IgnoreTackle())
return;
Log.Debug($"{Props.ShipName} is tackling.");
TemporarilyIgnoreTackles(Props.TacklingGraceTime);
}
/// <summary>
/// Called by the collision regions of the ship being tackled by an opponent.
/// Adds resulting forces to the ship and intiates the tackle response.
/// </summary>
/// <param name="tackleKind">Kind of the tackle. Depends on collision region.</param>
/// <param name="collider">Object which has collided with the collision region.</param>
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);
}
}
}