All ships now have their own properties, which are modifiers for the base ship properties. The base behaviour of all ships can now be easily changed, while the individual differences are still respected. Ship states now trigger particle emissions & effects. This is still messy and mixed with sounds. Will be overhauled once ship state machine is introduced. To calculate gravity you now need to specify the gravity source transform.
442 lines
13 KiB
C#
442 lines
13 KiB
C#
using System;
|
|
using UnityEngine;
|
|
using static AffectingForcesManager;
|
|
using ShipHandling;
|
|
using Managers;
|
|
using Unity.Mathematics;
|
|
using FishNet.Object;
|
|
using FishNet.Connection;
|
|
|
|
public class Ship : NetworkBehaviour
|
|
{
|
|
|
|
public int InstanceID { get; private set; }
|
|
public ShipProperties props;
|
|
public ShipState state;
|
|
public BoostCapacityUI boostUI;
|
|
// Private variables
|
|
public CameraOperator cameraOperator;
|
|
public ParticleSystem boostEffect;
|
|
public ParticleSystem gravityEffect;
|
|
public ParticleSystem jetFlameEffect;
|
|
public ParticleSystem smokeTrailEffect;
|
|
|
|
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;
|
|
// 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;
|
|
|
|
void Awake()
|
|
{
|
|
if (forceManager == null)
|
|
{
|
|
forceManager = GameObject.FindGameObjectWithTag("ForceManager").
|
|
GetComponent<AffectingForcesManager>();
|
|
}
|
|
body = GetComponent<Rigidbody>();
|
|
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);
|
|
}
|
|
|
|
// 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(gameObject);
|
|
|
|
tackleDetectors = GetComponentsInChildren<TackleDetection>();
|
|
foreach (TackleDetection td in tackleDetectors)
|
|
{
|
|
td.TackleResponse.AddListener(StartTackleResponse);
|
|
}
|
|
}
|
|
|
|
void OnDestroy()
|
|
{
|
|
foreach (TackleDetection td in tackleDetectors)
|
|
{
|
|
td.TackleResponse.RemoveAllListeners();
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
/// <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 * state.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 : state.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()
|
|
|| 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);
|
|
}
|
|
|
|
/// <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 state.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 && zone == Zone.OutsideZone)
|
|
{
|
|
state.boostCapacity -= deltaTime * props.outsideBoostRate;
|
|
}
|
|
if (state.boostCapacity <= 0)
|
|
{
|
|
canBoost = false;
|
|
}
|
|
|
|
if ((state.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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logic which sets the isTackled state.
|
|
/// </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.tackleCriticalStunTime :
|
|
props.tackleBodyStunTime;
|
|
return;
|
|
}
|
|
tackledTime -= Time.deltaTime;
|
|
if (tackledTime <= 0)
|
|
{
|
|
isTackled = false;
|
|
isCriticalTackle = false;
|
|
tackledTime = 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by the collision regions which detect tackling.
|
|
/// 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 StartTackleResponse(TackleKind tackleKind, Collider collider)
|
|
{
|
|
// TODO: The one who tackled first should be exempt from knockback in some way
|
|
float tacklePowerFactor = props.criticalTacklePowerFactor;
|
|
if (tackleKind == TackleKind.Critical)
|
|
{
|
|
isCriticalTackle = true;
|
|
}
|
|
else
|
|
{
|
|
isCriticalTackle = false;
|
|
tacklePowerFactor = props.normalTacklePowerFactor;
|
|
}
|
|
Vector3 colliderVelocity = collider.attachedRigidbody.velocity;
|
|
Vector3 tackleDirection = transform.position - collider.transform.position;
|
|
|
|
body.AddForce(colliderVelocity.magnitude * tackleDirection * tacklePowerFactor,
|
|
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(state.thrustInput) > 0 || IsBoosting())
|
|
{
|
|
ThrusterSound.PlayAudio(true);
|
|
|
|
ThrusterSound.ChangePitch(velocityFactor);
|
|
if (!jetFlameEffect.isPlaying)
|
|
jetFlameEffect.Play();
|
|
}
|
|
else
|
|
{
|
|
ThrusterSound.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);
|
|
BoosterSound.PlayAudio(false, true);
|
|
}
|
|
else
|
|
{
|
|
BoosterSound.ResetOneShot();
|
|
smokeTrailEffect.Stop();
|
|
jetFlameEffect.transform.localScale = new Vector3(1.3f, 1, 1);
|
|
}
|
|
if (isTackled && !isCriticalTackle)
|
|
{
|
|
BeingTackledSound.PlayAudio(false, true);
|
|
}
|
|
if (isCriticalTackle)
|
|
{
|
|
BeingCriticallyTackledSound.PlayAudio(false, true);
|
|
}
|
|
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);
|
|
|
|
}
|
|
}
|
|
|
|
}
|