projectEli/Assets/NeoFPS/Core/Character/StaminaSystem.cs
2022-11-06 20:28:33 -05:00

394 lines
17 KiB
C#

using System.Collections.Generic;
using UnityEngine;
using NeoFPS.CharacterMotion;
using NeoFPS.CharacterMotion.MotionData;
using System;
using UnityEngine.Events;
using NeoFPS.CharacterMotion.Parameters;
using NeoSaveGames.Serialization;
using NeoSaveGames;
namespace NeoFPS
{
public class StaminaSystem : MonoBehaviour, IMotionGraphDataOverride, IStaminaSystem, IBreathHandler
{
[Header("Stamina")]
[SerializeField, Tooltip("The current stamina of the character. This acts as the starting stamina and changes at runtime.")]
private float m_Stamina = 100f;
[SerializeField, Tooltip("The maximum stamina of the character.")]
private float m_MaxStamina = 100f;
[SerializeField, Tooltip("The rate that stamina increases over time when no drains are applied.")]
private float m_StaminaRefreshRate = 10f;
[Header("Movement Speed")]
[SerializeField, Tooltip("Should the stamina system modify movement speed based on current stamina. The other settings will be hidden if this is false.")]
private bool m_AffectMovementSpeed = true;
[SerializeField, Range(0f, 1f), Tooltip("A multiplier applied to the walking speed at minimum stamina.")]
private float m_MinWalkMultiplier = 0.5f;
[SerializeField, Range(0f, 1f), Tooltip("A multiplier applied to the sprinting speed at minimum stamina.")]
private float m_MinSprintMultiplier = 0.5f;
[SerializeField, Range(0f, 1f), Tooltip("A multiplier applied to the crouching speed at minimum stamina.")]
private float m_MinCrouchMultiplier = 0.5f;
[SerializeField, Tooltip("A curve that defines the character speed based on stamina. The X axis is the normalised stamina (stamina / max), while the Y axis is the min to max lerp value (0 = min, 1 = max).")]
private AnimationCurve m_MoveSpeedCurve = new AnimationCurve(new Keyframe[] {
new Keyframe(0f, 0f), new Keyframe(0.5f, 1f), new Keyframe(1f, 1f)
});
[SerializeField, MotionGraphDataKey(MotionGraphDataType.Float), Tooltip("The name of the motion data property on the motion graph that defines walk speed.")]
private string m_WalkSpeedData = string.Empty;
[SerializeField, MotionGraphDataKey(MotionGraphDataType.Float), Tooltip("The name of the motion data property on the motion graph that defines walk speed when aiming.")]
private string m_AimWalkSpeedData = string.Empty;
[SerializeField, MotionGraphDataKey(MotionGraphDataType.Float), Tooltip("The name of the motion data property on the motion graph that defines sprint speed.")]
private string m_SprintSpeedData = string.Empty;
[SerializeField, MotionGraphDataKey(MotionGraphDataType.Float), Tooltip("The name of the motion data property on the motion graph that defines sprint speed when aiming.")]
private string m_AimSprintSpeedData = string.Empty;
[SerializeField, MotionGraphDataKey(MotionGraphDataType.Float), Tooltip("The name of the motion data property on the motion graph that defines crouch movement speed.")]
private string m_CrouchSpeedData = string.Empty;
[SerializeField, MotionGraphDataKey(MotionGraphDataType.Float), Tooltip("The name of the motion data property on the motion graph that defines crouch movement speed when aiming.")]
private string m_AimCrouchSpeedData = string.Empty;
[Header("Breathing")]
[SerializeField, Tooltip("The time in seconds between breaths (when breathing slow).")]
private float m_BreatheSlowInterval = 5f;
[SerializeField, Tooltip("The time in seconds between breaths (when breathing fast).")]
private float m_BreatheFastInterval = 1f;
[SerializeField, Tooltip("A curve that defines the breathing rate based on stamina. The X-axis is the normalised stamina (stamina / max stamina), and the Y-Axis is a lerp between slow and fast breathing rate (0 = slow, 1 = fast).")]
private AnimationCurve m_BreathingRateCurve = new AnimationCurve(new Keyframe[] {
new Keyframe(0f, 1f), new Keyframe(0.5f, 0.5f), new Keyframe(1f, 0.5f)
});
[SerializeField, Tooltip("A curve that defines the breathing strength based on stamina. The X-axis is the normalised stamina (stamina / max stamina), and the Y-Axis is the strength of the character's breathing (0 = non-existant, 1 = heaving/panting).")]
private AnimationCurve m_BreathingStrengthCurve = new AnimationCurve(new Keyframe[] {
new Keyframe(0f, 1f), new Keyframe(0.5f, 0.25f), new Keyframe(1f, 0.25f)
});
[Header("Exhaustion")]
[SerializeField, Tooltip("Should the character suffer an exhaustion effect on hitting a specific stamina threshold. The other properties will be hidden if this is false.")]
private bool m_UseExhaustion = true;
[SerializeField, Tooltip("The stamina level below which the character will become exhausted.")]
private float m_ExhaustionThreshold = 0f;
[SerializeField, Tooltip("The character will stop being exhausted once their stamina has recovered above this value.")]
private float m_RecoverThreshold = 50f;
[SerializeField, MotionGraphParameterKey(MotionGraphParameterType.Switch), Tooltip("The name of the switch motion graph parameter that the graph uses as a condition for preventing sprinting.")]
private string m_ExhaustedMotionParameter = string.Empty;
[SerializeField, MotionGraphParameterKey(MotionGraphParameterType.Switch), Tooltip("The name of the switch motion graph parameter that the character input handler sets to tell the motion graph to start sprinting.")]
private string m_SprintMotionParameter = string.Empty;
[Space]
[SerializeField, Tooltip("An event which is fired when the character hits the exhaustion threshold.")]
private UnityEvent m_OnExhausted = new UnityEvent();
[SerializeField, Tooltip("An event which is fired when the character recovers from exhaustion.")]
private UnityEvent m_OnRecovered = new UnityEvent();
private List<StaminaDrainDelegate> m_StaminaDrains = new List<StaminaDrainDelegate>(4);
private SwitchParameter m_ExhaustedSwitch = null;
private SwitchParameter m_SprintSwitch = null;
public event UnityAction onStaminaChanged;
public event UnityAction onExhausted
{
add { m_OnExhausted.AddListener(value); }
remove { m_OnExhausted.RemoveListener(value); }
}
public event UnityAction onRecovered
{
add { m_OnRecovered.AddListener(value); }
remove { m_OnRecovered.RemoveListener(value); }
}
public float stamina
{
get { return m_Stamina; }
private set { m_Stamina = value; }
}
public float maxStamina
{
get { return m_MaxStamina; }
set
{
m_MaxStamina = Mathf.Clamp(value, 1f, 100000f);
staminaNormalised = stamina / m_MaxStamina;
}
}
public float staminaRefreshRate
{
get { return m_StaminaRefreshRate; }
set { m_StaminaRefreshRate = Mathf.Clamp(value, 0.1f, 100000f); }
}
public float staminaNormalised
{
get;
private set;
}
public bool isExhausted
{
get;
private set;
}
public float breathCounter
{
get;
private set;
}
public float breathStrength
{
get;
private set;
}
public float breathingRate
{
get;
private set;
}
void OnValidate()
{
// Stamina
m_MaxStamina = Mathf.Clamp(m_MaxStamina, 1f, 100000f);
m_Stamina = Mathf.Clamp(m_Stamina, 1f, 100000f);
m_StaminaRefreshRate = Mathf.Clamp(m_StaminaRefreshRate, 0.1f, 100000f);
// Breathing
m_BreatheSlowInterval = Mathf.Clamp(m_BreatheSlowInterval, 1f, 20f);
m_BreatheFastInterval = Mathf.Clamp(m_BreatheFastInterval, 0.1f, 10f);
// Exhaustion
m_ExhaustionThreshold = Mathf.Clamp(m_ExhaustionThreshold, 1f, m_MaxStamina - 1f);
m_RecoverThreshold = Mathf.Clamp(m_RecoverThreshold, m_ExhaustionThreshold + 1f, m_MaxStamina);
}
void Start()
{
var mc = GetComponent<MotionController>();
if (mc != null)
{
// Apply movement speed overrides
if (m_AffectMovementSpeed)
mc.motionGraph.AddDataOverrides(this);
// Get motion graph parameter references
if (!string.IsNullOrEmpty(m_ExhaustedMotionParameter))
m_ExhaustedSwitch = mc.motionGraph.GetSwitchProperty(m_ExhaustedMotionParameter);
if (!string.IsNullOrEmpty(m_SprintMotionParameter))
m_SprintSwitch = mc.motionGraph.GetSwitchProperty(m_SprintMotionParameter);
}
// Set starting stamina (to update breathing, etc)
SetStamina(stamina);
}
public void SetStamina(float s, bool normalised = false)
{
// Apply
if (normalised)
{
staminaNormalised = Mathf.Clamp01(s);
stamina = staminaNormalised * m_MaxStamina;
}
else
{
stamina = Mathf.Clamp(s, 0f, m_MaxStamina);
staminaNormalised = stamina / m_MaxStamina;
}
// Update breathing
breathStrength = m_BreathingStrengthCurve.Evaluate(staminaNormalised);
breathingRate = m_BreathingRateCurve.Evaluate(staminaNormalised);
// Check for exhaustion changes
if (m_UseExhaustion)
{
if (isExhausted)
{
if (stamina > m_RecoverThreshold)
{
isExhausted = false;
m_OnRecovered.Invoke();
if (m_ExhaustedSwitch != null)
m_ExhaustedSwitch.on = false;
if (m_SprintSwitch != null)
{
m_SprintSwitch.on = false;
m_SprintSwitch.RemoveBlocker();
}
}
}
else
{
if (stamina < m_ExhaustionThreshold)
{
isExhausted = true;
m_OnExhausted.Invoke();
if (m_ExhaustedSwitch != null)
m_ExhaustedSwitch.on = true;
if (m_SprintSwitch != null)
{
m_SprintSwitch.on = false;
m_SprintSwitch.AddBlocker();
}
}
}
}
// Signal stamina changed
OnStaminaChange();
}
protected virtual void OnStaminaChange()
{
// Trigger event
if (onStaminaChanged != null)
onStaminaChanged();
}
public Func<float, float> GetFloatOverride(FloatData data)
{
if (!string.IsNullOrEmpty(m_WalkSpeedData) && data.name == m_WalkSpeedData)
return GetMoveSpeedWalking;
if (!string.IsNullOrEmpty(m_AimWalkSpeedData) && data.name == m_AimWalkSpeedData)
return GetMoveSpeedWalking;
if (!string.IsNullOrEmpty(m_SprintSpeedData) && data.name == m_SprintSpeedData)
return GetMoveSpeedSprinting;
if (!string.IsNullOrEmpty(m_AimSprintSpeedData) && data.name == m_AimSprintSpeedData)
return GetMoveSpeedSprinting;
if (!string.IsNullOrEmpty(m_CrouchSpeedData) && data.name == m_CrouchSpeedData)
return GetMoveSpeedCrouching;
if (!string.IsNullOrEmpty(m_AimCrouchSpeedData) && data.name == m_AimCrouchSpeedData)
return GetMoveSpeedCrouching;
return null;
}
float GetMoveSpeedWalking(float input)
{
return Mathf.Lerp(input * m_MinWalkMultiplier, input, m_MoveSpeedCurve.Evaluate(staminaNormalised));
}
float GetMoveSpeedSprinting(float input)
{
return Mathf.Lerp(input * m_MinSprintMultiplier, input, m_MoveSpeedCurve.Evaluate(staminaNormalised));
}
float GetMoveSpeedCrouching(float input)
{
return Mathf.Lerp(input * m_MinCrouchMultiplier, input, m_MoveSpeedCurve.Evaluate(staminaNormalised));
}
public Func<int, int> GetIntOverride(IntData data)
{
return null;
}
public Func<bool, bool> GetBoolOverride(BoolData data)
{
return null;
}
public void AddStaminaDrain(StaminaDrainDelegate drain)
{
if (drain == null || m_StaminaDrains.Contains(drain))
return;
m_StaminaDrains.Add(drain);
}
public void RemoveStaminaDrain(StaminaDrainDelegate drain)
{
m_StaminaDrains.Remove(drain);
}
public void IncrementStamina(float amount, bool isFactor = false)
{
if (isFactor)
SetStamina(stamina + amount * maxStamina);
else
SetStamina(stamina + amount);
}
public void DecrementStamina(float amount, bool isFactor = false)
{
if (isFactor)
SetStamina(stamina - amount * maxStamina);
else
SetStamina(stamina - amount);
}
void FixedUpdate()
{
// Update breathing cycle
breathCounter += 2f * Time.deltaTime / Mathf.Lerp(m_BreatheSlowInterval, m_BreatheFastInterval, breathingRate);
// Refresh first (falloff doesn't work as well otherwise)
float s = stamina + m_StaminaRefreshRate * Time.deltaTime;
// Drain
for (int i = 0; i < m_StaminaDrains.Count; ++i)
s -= m_StaminaDrains[i].Invoke(this, s);
// Clamp & assign
s = Mathf.Clamp(s, 0f, m_MaxStamina);
if (!Mathf.Approximately(s, stamina))
SetStamina(s);
}
public float GetBreathCycle()
{
return EasingFunctions.EaseInOutQuadratic(Mathf.PingPong(breathCounter, 1f)) * 2f - 1f;
}
public float GetBreathCycle(float offset)
{
return EasingFunctions.EaseInOutQuadratic(Mathf.PingPong(breathCounter + offset, 1f)) * 2f - 1f;
}
public float GetBreathCycle(float offset, float multiplier)
{
return EasingFunctions.EaseInOutQuadratic(Mathf.PingPong(multiplier * (breathCounter + offset), 1f)) * 2f - 1f;
}
#region INeoSerializableComponent IMPLEMENTATION
private static readonly NeoSerializationKey k_BreathCounterKey = new NeoSerializationKey("counter");
private static readonly NeoSerializationKey k_BreathStaminaKey = new NeoSerializationKey("stamina");
private static readonly NeoSerializationKey k_BreathMaxStaminaKey = new NeoSerializationKey("maxStamina");
private static readonly NeoSerializationKey k_BreathStaminaRefreshKey = new NeoSerializationKey("staminaRefresh");
public void WriteProperties(INeoSerializer writer, NeoSerializedGameObject nsgo, SaveMode saveMode)
{
writer.WriteValue(k_BreathCounterKey, breathCounter);
writer.WriteValue(k_BreathStaminaKey, stamina);
writer.WriteValue(k_BreathMaxStaminaKey, maxStamina);
writer.WriteValue(k_BreathStaminaRefreshKey, staminaRefreshRate);
}
public void ReadProperties(INeoDeserializer reader, NeoSerializedGameObject nsgo)
{
float floatValue = 0f;
if (reader.TryReadValue(k_BreathCounterKey, out floatValue, 0f))
breathCounter = floatValue;
if (reader.TryReadValue(k_BreathMaxStaminaKey, out floatValue, maxStamina))
maxStamina = floatValue;
if (reader.TryReadValue(k_BreathStaminaKey, out floatValue, stamina))
SetStamina(floatValue);
if (reader.TryReadValue(k_BreathStaminaRefreshKey, out floatValue, staminaRefreshRate))
staminaRefreshRate = floatValue;
}
#endregion
}
}