462 lines
16 KiB
C#
462 lines
16 KiB
C#
using UnityEngine;
|
|
using NeoFPS.Constants;
|
|
using NeoFPS.CharacterMotion;
|
|
using NeoCC;
|
|
using NeoSaveGames.Serialization;
|
|
using NeoSaveGames;
|
|
|
|
namespace NeoFPS
|
|
{
|
|
[RequireComponent(typeof(MotionController))]
|
|
public abstract class BaseCharacter : MonoBehaviour, ICharacter, INeoSerializableComponent
|
|
{
|
|
private IHealthManager m_HealthManager = null;
|
|
public IHealthManager healthManager
|
|
{
|
|
get { return m_HealthManager; }
|
|
private set
|
|
{
|
|
if (m_HealthManager != null)
|
|
{
|
|
m_HealthManager.onIsAliveChanged -= OnIsAliveChanged;
|
|
m_HealthManager.onHealthChanged -= OnHealthChanged;
|
|
}
|
|
m_HealthManager = value;
|
|
if (m_HealthManager != null)
|
|
{
|
|
m_HealthManager.onIsAliveChanged += OnIsAliveChanged;
|
|
m_HealthManager.onHealthChanged += OnHealthChanged;
|
|
}
|
|
}
|
|
}
|
|
|
|
public Transform localTransform
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
protected virtual void OnValidate()
|
|
{
|
|
ValidateImpactDamage();
|
|
}
|
|
|
|
protected virtual void Awake()
|
|
{
|
|
localTransform = transform;
|
|
m_MotionController = GetComponent<MotionController>();
|
|
aimController = GetComponent<IAimController>();
|
|
audioHandler = GetComponent<ICharacterAudioHandler>();
|
|
healthManager = GetComponent<IHealthManager>();
|
|
inventory = GetComponent<IInventory>();
|
|
quickSlots = GetComponent<IQuickSlots>();
|
|
m_ImpactDamageSource = new ImpactDamageSource(this);
|
|
|
|
GetCamera();
|
|
SetFirstPerson(false);
|
|
}
|
|
|
|
protected virtual void GetCamera()
|
|
{
|
|
fpCamera = GetComponentInChildren<FirstPersonCameraBase>();
|
|
}
|
|
|
|
protected virtual void Start()
|
|
{
|
|
// Attach event handlers
|
|
m_MotionController.onGroundImpact += OnGroundImpact;
|
|
m_MotionController.onGroundImpact += OnLanded;
|
|
m_MotionController.onBodyImpact += OnBodyImpact;
|
|
m_MotionController.onHeadImpact += OnHeadImpact;
|
|
|
|
// Disable the object (needs a controller to function)
|
|
if (m_Controller == null || !m_Controller.isActiveAndEnabled)
|
|
gameObject.SetActive(false);
|
|
}
|
|
|
|
protected virtual void SetFirstPerson(bool firstPerson)
|
|
{
|
|
fpCamera.LookThrough(firstPerson);
|
|
|
|
// Hide arms
|
|
|
|
// Show body, hide fps arms, etc
|
|
// ...
|
|
// Implement 3rd person body & 1st person body
|
|
}
|
|
|
|
#region ICharacter implementation
|
|
|
|
[Header("Spring Effects")]
|
|
|
|
[SerializeField, NeoObjectInHierarchyField(false), Tooltip("The additive transform handler attached to the head heirarchy of this character (used for things like weapon recoil and impacts).")]
|
|
private AdditiveTransformHandler m_HeadTransformHandler = null;
|
|
|
|
[SerializeField, NeoObjectInHierarchyField(false), Tooltip("The additive transform handler attached to the body heirarchy of this character (used for things like leaning and impacts).")]
|
|
private AdditiveTransformHandler m_BodyTransformHandler = null;
|
|
|
|
public event CharacterDelegates.OnControllerChange onControllerChanged;
|
|
public event CharacterDelegates.OnIsAliveChange onIsAliveChanged;
|
|
public event CharacterDelegates.OnHitTarget onHitTarget;
|
|
|
|
private BaseController m_Controller = null;
|
|
public IController controller
|
|
{
|
|
get { return m_Controller; }
|
|
set
|
|
{
|
|
m_Controller = value as BaseController;
|
|
OnControllerChanged();
|
|
}
|
|
}
|
|
|
|
public FirstPersonCameraBase fpCamera
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public IAimController aimController
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
private MotionController m_MotionController = null;
|
|
public IMotionController motionController
|
|
{
|
|
get { return m_MotionController; }
|
|
}
|
|
|
|
public ICharacterAudioHandler audioHandler
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public AdditiveTransformHandler headTransformHandler
|
|
{
|
|
get { return m_HeadTransformHandler; }
|
|
}
|
|
|
|
public AdditiveTransformHandler bodyTransformHandler
|
|
{
|
|
get { return m_BodyTransformHandler; }
|
|
}
|
|
|
|
public IInventory inventory
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public IQuickSlots quickSlots
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public bool isAlive
|
|
{
|
|
get
|
|
{
|
|
if (healthManager != null)
|
|
return healthManager.isAlive;
|
|
else
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public bool isPlayerControlled
|
|
{
|
|
get { return controller != null && controller.isPlayer; }
|
|
}
|
|
|
|
public bool isLocalPlayerControlled
|
|
{
|
|
get { return isPlayerControlled; }
|
|
}
|
|
|
|
public bool isRemotePlayerControlled
|
|
{
|
|
get { return false; }
|
|
}
|
|
|
|
public void Kill()
|
|
{
|
|
if (healthManager != null)
|
|
healthManager.health = 0f;
|
|
}
|
|
|
|
protected virtual void OnControllerChanged ()
|
|
{
|
|
if (onControllerChanged != null)
|
|
onControllerChanged(this, m_Controller);
|
|
}
|
|
|
|
protected virtual void OnIsAliveChanged(bool isAlive)
|
|
{
|
|
// Collapse here
|
|
if (!isAlive)
|
|
{
|
|
motionController.SetHeightMultiplier(0.45f, 0.5f);
|
|
|
|
// Trigger audio
|
|
if (!isAlive && audioHandler != null)
|
|
audioHandler.PlayAudio(FpsCharacterAudio.Collapse);
|
|
}
|
|
else
|
|
motionController.SetHeightMultiplier(1f, 1f);
|
|
|
|
if (onIsAliveChanged != null)
|
|
onIsAliveChanged(this, isAlive);
|
|
}
|
|
|
|
public virtual void ReportTargetHit(bool critical)
|
|
{
|
|
if (onHitTarget != null)
|
|
onHitTarget(this, critical);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region DAMAGE AUDIO
|
|
|
|
[Header("Damage Audio")]
|
|
|
|
[Range(0f, 50f)]
|
|
[Tooltip("The amount of damage to take in a single hit before playing a character damage audio clip.")]
|
|
[SerializeField] private float m_DamageAudioThreshold = 10f;
|
|
|
|
protected virtual void OnHealthChanged(float from, float to, bool critical, IDamageSource source)
|
|
{
|
|
// Play pain audio if required
|
|
float diff = from - to;
|
|
if (diff > m_DamageAudioThreshold && audioHandler != null)
|
|
audioHandler.PlayAudio(FpsCharacterAudio.Pain);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IMPACT DAMAGE HANDLING
|
|
|
|
[Header("Impacts")]
|
|
|
|
[Tooltip("Should the character be subject to damage from landing impacts (impacts where the character capsule is hit in the bottom hemisphere).")]
|
|
[SerializeField] private bool m_ApplyFallDamage = true;
|
|
[Tooltip("The minimum landing impact magnitude before any damage is applied.")]
|
|
[SerializeField] private float m_LandingMinForce = 10f;
|
|
[Tooltip("The landing impact magnitude where a full 100 damage will be applied.")]
|
|
[SerializeField] private float m_LandingFullForce = 40f;
|
|
[Tooltip("Should the character be subject to damage from body impacts (impacts where the character capsule is hit in the central cylinder).")]
|
|
[SerializeField] private bool m_BodyImpactDamage = true;
|
|
[Tooltip("The minimum body impact magnitude before any damage is applied.")]
|
|
[SerializeField] private float m_BodyMinForce = 25f;
|
|
[Tooltip("The body impact magnitude where a full 100 damage will be applied.")]
|
|
[SerializeField] private float m_BodyFullForce = 100f;
|
|
[Tooltip("Should the character be subject to damage from head impacts (impacts where the character capsule is hit in the top hemisphere).")]
|
|
[SerializeField] private bool m_HeadImpactDamage = true;
|
|
[Tooltip("The minimum head impact magnitude before any damage is applied.")]
|
|
[SerializeField] private float m_HeadMinForce = 7.5f;
|
|
[Tooltip("The head impact magnitude where a full 100 damage will be applied.")]
|
|
[SerializeField] private float m_HeadFullForce = 20f;
|
|
|
|
public class ImpactDamageSource : IDamageSource
|
|
{
|
|
private DamageFilter m_DamageFilter = new DamageFilter(DamageType.Fall, DamageTeamFilter.All);
|
|
public DamageFilter outDamageFilter
|
|
{
|
|
get { return m_DamageFilter; }
|
|
set { m_DamageFilter = value; }
|
|
}
|
|
|
|
private BaseCharacter m_Character = null;
|
|
public IController controller
|
|
{
|
|
get { return m_Character.controller; }
|
|
}
|
|
|
|
public Transform damageSourceTransform
|
|
{
|
|
get { return m_Character.localTransform; }
|
|
}
|
|
|
|
public string description { get { return "Impact"; } }
|
|
|
|
public ImpactDamageSource(BaseCharacter c)
|
|
{
|
|
m_Character = c;
|
|
}
|
|
}
|
|
|
|
private ImpactDamageSource m_ImpactDamageSource = null;
|
|
|
|
public bool applyFallDamage
|
|
{
|
|
get { return m_ApplyFallDamage; }
|
|
set { m_ApplyFallDamage = value; }
|
|
}
|
|
|
|
public bool applyBodyImpactDamage
|
|
{
|
|
get { return m_BodyImpactDamage; }
|
|
set { m_BodyImpactDamage = value; }
|
|
}
|
|
|
|
public bool applyHeadImpactDamage
|
|
{
|
|
get { return m_HeadImpactDamage; }
|
|
set { m_HeadImpactDamage = value; }
|
|
}
|
|
|
|
void ValidateImpactDamage()
|
|
{
|
|
m_LandingMinForce = Mathf.Clamp(m_LandingMinForce, 1f, 100f);
|
|
m_LandingFullForce = Mathf.Clamp(m_LandingFullForce, 10f, 1000f);
|
|
m_BodyMinForce = Mathf.Clamp(m_BodyMinForce, 1f, 100f);
|
|
m_BodyFullForce = Mathf.Clamp(m_BodyFullForce, 10f, 1000f);
|
|
m_HeadMinForce = Mathf.Clamp(m_HeadMinForce, 1f, 100f);
|
|
m_HeadFullForce = Mathf.Clamp(m_HeadFullForce, 10f, 1000f);
|
|
}
|
|
|
|
public void OnGroundImpact(Vector3 impulse)
|
|
{
|
|
if (healthManager == null || !m_ApplyFallDamage)
|
|
return;
|
|
|
|
float sqrMagnitude = impulse.sqrMagnitude;
|
|
if (sqrMagnitude > m_LandingMinForce * m_LandingMinForce)
|
|
{
|
|
float damage = 100f * (Mathf.Sqrt(sqrMagnitude) - m_LandingMinForce) / (m_LandingFullForce - m_LandingMinForce);
|
|
healthManager.AddDamage(damage, false, m_ImpactDamageSource);
|
|
}
|
|
}
|
|
|
|
public void OnHeadImpact(Vector3 impulse)
|
|
{
|
|
if (healthManager == null || !m_HeadImpactDamage)
|
|
return;
|
|
|
|
float sqrMagnitude = impulse.sqrMagnitude;
|
|
if (sqrMagnitude > m_HeadMinForce * m_HeadMinForce)
|
|
{
|
|
float damage = 100f * (Mathf.Sqrt(sqrMagnitude) - m_HeadMinForce) / (m_HeadFullForce - m_HeadMinForce);
|
|
healthManager.AddDamage(damage, false, m_ImpactDamageSource);
|
|
}
|
|
}
|
|
|
|
public void OnBodyImpact(Vector3 impulse)
|
|
{
|
|
if (healthManager == null || !m_BodyImpactDamage)
|
|
return;
|
|
|
|
float sqrMagnitude = impulse.sqrMagnitude;
|
|
if (sqrMagnitude > m_BodyMinForce * m_BodyMinForce)
|
|
{
|
|
float damage = 100f * (Mathf.Sqrt(sqrMagnitude) - m_BodyMinForce) / (m_BodyFullForce - m_BodyMinForce);
|
|
healthManager.AddDamage(damage, false, m_ImpactDamageSource);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region LANDING AUDIO
|
|
|
|
[Header("Landing Audio")]
|
|
[Tooltip("Surface audio library used to trigger the correct sound when the character lands below the \"hard landing\" threshold.")]
|
|
[SerializeField] private SurfaceAudioData m_SoftLandings = null;
|
|
[Tooltip("Surface audio library used to trigger the correct sound when the character makes a heavy landing.")]
|
|
[SerializeField] private SurfaceAudioData m_HardLandings = null;
|
|
[Tooltip("The magnitude of the landing force below which no landing sound will be played.")]
|
|
[SerializeField] private float m_MinLandingThreshold = 0.5f;
|
|
[Tooltip("The magnitude of the landing force above which to play a hard landing sound.")]
|
|
[SerializeField] private float m_HardLandingThreshold = 8f;
|
|
[Tooltip("The maximum downward ray length for a ground test.")]
|
|
[SerializeField] private float m_MaxRayDistance = 1f;
|
|
[Tooltip("The vertical offset from the absolute bottom of the character to start the ground test raycast.")]
|
|
[SerializeField] private float m_RayOffset = 0.5f;
|
|
|
|
private RaycastHit m_Hit = new RaycastHit();
|
|
|
|
public delegate void LandingEventDelegate(FpsSurfaceMaterial surface, bool hardLanding);
|
|
public event LandingEventDelegate onLanded;
|
|
|
|
public void OnLanded(Vector3 force)
|
|
{
|
|
if (audioHandler == null)
|
|
return;
|
|
|
|
// Get force magnitude
|
|
float mag = force.magnitude;
|
|
if (mag < m_MinLandingThreshold)
|
|
return;
|
|
|
|
// Check if hard landing
|
|
bool hardLanding = mag > m_HardLandingThreshold;
|
|
|
|
// Get correct surface audio data (soft/hard)
|
|
SurfaceAudioData audio = m_SoftLandings;
|
|
if (audio == null)
|
|
{
|
|
audio = m_HardLandings;
|
|
}
|
|
else
|
|
{
|
|
if (hardLanding && m_HardLandings != null)
|
|
audio = m_HardLandings;
|
|
}
|
|
|
|
// Get suface material
|
|
var surface = GetGroundSurface();
|
|
|
|
//Play audio
|
|
if (audio != null)
|
|
{
|
|
// Get and play landing clip
|
|
float volume = 1f;
|
|
AudioClip clip = audio.GetAudioClip(surface, out volume);
|
|
if (clip != null)
|
|
audioHandler.PlayClip(clip, FpsCharacterAudioSource.Feet, volume);
|
|
}
|
|
|
|
// Fire event
|
|
if (onLanded != null)
|
|
onLanded(surface, hardLanding);
|
|
}
|
|
|
|
FpsSurfaceMaterial GetGroundSurface()
|
|
{
|
|
FpsSurfaceMaterial result = FpsSurfaceMaterial.Default;
|
|
|
|
Vector3 position = motionController.localTransform.position;
|
|
position.y += m_RayOffset;
|
|
Ray ray = new Ray(position, Vector3.down);
|
|
if (PhysicsExtensions.RaycastNonAllocSingle(ray, out m_Hit, m_MaxRayDistance, PhysicsFilter.Masks.BulletBlockers, motionController.localTransform, QueryTriggerInteraction.Ignore))
|
|
{
|
|
Transform t = m_Hit.transform;
|
|
if (t != null)
|
|
{
|
|
BaseSurface s = t.GetComponent<BaseSurface>();
|
|
if (s != null)
|
|
result = s.GetSurface(m_Hit);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region INeoSerializableComponent implementation
|
|
|
|
public virtual void WriteProperties(INeoSerializer writer, NeoSerializedGameObject nsgo, SaveMode saveMode)
|
|
{
|
|
}
|
|
|
|
public virtual void ReadProperties(INeoDeserializer reader, NeoSerializedGameObject nsgo)
|
|
{
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|