This commit is contained in:
Alex38Lyon
2025-06-03 12:00:47 +02:00
parent ed8041abcd
commit 878ea46cac
1300 changed files with 527178 additions and 0 deletions
@@ -0,0 +1,529 @@
using Unity.FPS.Game;
using UnityEngine;
using UnityEngine.Events;
namespace Unity.FPS.Gameplay
{
[RequireComponent(typeof(CharacterController), typeof(PlayerInputHandler), typeof(AudioSource))]
public class PlayerCharacterController : MonoBehaviour
{
[Header("References")] [Tooltip("Reference to the main camera used for the player")]
public Camera PlayerCamera;
[Tooltip("Audio source for footsteps, jump, etc...")]
public AudioSource AudioSource;
[Header("General")] [Tooltip("Force applied downward when in the air")]
public float GravityDownForce = 20f;
[Tooltip("Physic layers checked to consider the player grounded")]
public LayerMask GroundCheckLayers = -1;
[Tooltip("distance from the bottom of the character controller capsule to test for grounded")]
public float GroundCheckDistance = 0.05f;
[Header("Movement")] [Tooltip("Max movement speed when grounded (when not sprinting)")]
public float MaxSpeedOnGround = 10f;
[Tooltip(
"Sharpness for the movement when grounded, a low value will make the player accelerate and decelerate slowly, a high value will do the opposite")]
public float MovementSharpnessOnGround = 15;
[Tooltip("Max movement speed when crouching")] [Range(0, 1)]
public float MaxSpeedCrouchedRatio = 0.5f;
[Tooltip("Max movement speed when not grounded")]
public float MaxSpeedInAir = 10f;
[Tooltip("Acceleration speed when in the air")]
public float AccelerationSpeedInAir = 25f;
[Tooltip("Multiplicator for the sprint speed (based on grounded speed)")]
public float SprintSpeedModifier = 2f;
[Tooltip("Height at which the player dies instantly when falling off the map")]
public float KillHeight = -50f;
[Header("Rotation")] [Tooltip("Rotation speed for moving the camera")]
public float RotationSpeed = 200f;
[Range(0.1f, 1f)] [Tooltip("Rotation speed multiplier when aiming")]
public float AimingRotationMultiplier = 0.4f;
[Header("Jump")] [Tooltip("Force applied upward when jumping")]
public float JumpForce = 9f;
[Header("Stance")] [Tooltip("Ratio (0-1) of the character height where the camera will be at")]
public float CameraHeightRatio = 0.9f;
[Tooltip("Height of character when standing")]
public float CapsuleHeightStanding = 1.8f;
[Tooltip("Height of character when crouching")]
public float CapsuleHeightCrouching = 0.9f;
[Tooltip("Speed of crouching transitions")]
public float CrouchingSharpness = 10f;
[Header("Audio")] [Tooltip("Amount of footstep sounds played when moving one meter")]
public float FootstepSfxFrequency = 1f;
[Tooltip("Amount of footstep sounds played when moving one meter while sprinting")]
public float FootstepSfxFrequencyWhileSprinting = 1f;
[Tooltip("Sound played for footsteps")]
public AudioClip FootstepSfx;
[Tooltip("Sound played when jumping")] public AudioClip JumpSfx;
[Tooltip("Sound played when landing")] public AudioClip LandSfx;
[Tooltip("Sound played when taking damage froma fall")]
public AudioClip FallDamageSfx;
[Header("Fall Damage")]
[Tooltip("Whether the player will recieve damage when hitting the ground at high speed")]
public bool RecievesFallDamage;
[Tooltip("Minimun fall speed for recieving fall damage")]
public float MinSpeedForFallDamage = 10f;
[Tooltip("Fall speed for recieving th emaximum amount of fall damage")]
public float MaxSpeedForFallDamage = 30f;
[Tooltip("Damage recieved when falling at the mimimum speed")]
public float FallDamageAtMinSpeed = 10f;
[Tooltip("Damage recieved when falling at the maximum speed")]
public float FallDamageAtMaxSpeed = 50f;
public UnityAction<bool> OnStanceChanged;
public Vector3 CharacterVelocity { get; set; }
public bool IsGrounded { get; private set; }
public bool HasJumpedThisFrame { get; private set; }
public bool IsDead { get; private set; }
public bool IsCrouching { get; private set; }
public float RotationMultiplier
{
get
{
if (m_WeaponsManager.IsAiming)
{
return AimingRotationMultiplier;
}
return 1f;
}
}
public float CurrentCharacterHeight => m_Controller.height;
// public float CurrentCharacterHeight => _GetHeadClearance;
Health m_Health;
PlayerInputHandler m_InputHandler;
CharacterController m_Controller;
PlayerWeaponsManager m_WeaponsManager;
Actor m_Actor;
Vector3 m_GroundNormal;
Vector3 m_CharacterVelocity;
Vector3 m_LatestImpactSpeed;
float m_LastTimeJumped = 0f;
float m_CameraVerticalAngle = 0f;
float m_FootstepDistanceCounter;
float m_TargetCharacterHeight;
float _GetHeadClearance;
float lastHeightDecreaseTime = -Mathf.Infinity;
const float k_JumpGroundingPreventionTime = 0.2f;
const float k_GroundCheckDistanceInAir = 0.07f;
void Awake()
{
ActorsManager actorsManager = FindFirstObjectByType<ActorsManager>();
if (actorsManager != null)
actorsManager.SetPlayer(gameObject);
}
void Start()
{
// fetch components on the same gameObject
m_Controller = GetComponent<CharacterController>();
DebugUtility.HandleErrorIfNullGetComponent<CharacterController, PlayerCharacterController>(m_Controller,
this, gameObject);
m_InputHandler = GetComponent<PlayerInputHandler>();
DebugUtility.HandleErrorIfNullGetComponent<PlayerInputHandler, PlayerCharacterController>(m_InputHandler,
this, gameObject);
m_WeaponsManager = GetComponent<PlayerWeaponsManager>();
DebugUtility.HandleErrorIfNullGetComponent<PlayerWeaponsManager, PlayerCharacterController>(
m_WeaponsManager, this, gameObject);
m_Health = GetComponent<Health>();
DebugUtility.HandleErrorIfNullGetComponent<Health, PlayerCharacterController>(m_Health, this, gameObject);
m_Actor = GetComponent<Actor>();
DebugUtility.HandleErrorIfNullGetComponent<Actor, PlayerCharacterController>(m_Actor, this, gameObject);
m_Controller.enableOverlapRecovery = true;
m_Health.OnDie += OnDie;
// force the crouch state to false when starting
SetCrouchingState(false, true);
UpdateCharacterHeight(true);
}
void Update()
{
// check for Y kill
if (!IsDead && transform.position.y < KillHeight)
{
m_Health.Kill();
}
HasJumpedThisFrame = false;
bool wasGrounded = IsGrounded;
GroundCheck();
// landing
if (IsGrounded && !wasGrounded)
{
// Fall damage
float fallSpeed = -Mathf.Min(CharacterVelocity.y, m_LatestImpactSpeed.y);
float fallSpeedRatio = (fallSpeed - MinSpeedForFallDamage) /
(MaxSpeedForFallDamage - MinSpeedForFallDamage);
if (RecievesFallDamage && fallSpeedRatio > 0f)
{
float dmgFromFall = Mathf.Lerp(FallDamageAtMinSpeed, FallDamageAtMaxSpeed, fallSpeedRatio);
m_Health.TakeDamage(dmgFromFall, null);
// fall damage SFX
AudioSource.PlayOneShot(FallDamageSfx);
}
else
{
// land SFX
AudioSource.PlayOneShot(LandSfx);
}
}
// crouching
if (m_InputHandler.GetCrouchInputDown())
{
SetCrouchingState(!IsCrouching, false);
}
UpdateCharacterHeight(false);
HandleCharacterMovement();
}
void OnDie()
{
IsDead = true;
// Tell the weapons manager to switch to a non-existing weapon in order to lower the weapon
m_WeaponsManager.SwitchToWeaponIndex(-1, true);
EventManager.Broadcast(Events.PlayerDeathEvent);
}
void GroundCheck()
{
// Make sure that the ground check distance while already in air is very small, to prevent suddenly snapping to ground
float chosenGroundCheckDistance =
IsGrounded ? (m_Controller.skinWidth + GroundCheckDistance) : k_GroundCheckDistanceInAir;
// reset values before the ground check
IsGrounded = false;
m_GroundNormal = Vector3.up;
// only try to detect ground if it's been a short amount of time since last jump; otherwise we may snap to the ground instantly after we try jumping
if (Time.time >= m_LastTimeJumped + k_JumpGroundingPreventionTime)
{
// if we're grounded, collect info about the ground normal with a downward capsule cast representing our character capsule
if (Physics.CapsuleCast(GetCapsuleBottomHemisphere(), GetCapsuleTopHemisphere(m_Controller.height),
m_Controller.radius, Vector3.down, out RaycastHit hit, chosenGroundCheckDistance, GroundCheckLayers,
QueryTriggerInteraction.Ignore))
{
// storing the upward direction for the surface found
m_GroundNormal = hit.normal;
// Only consider this a valid ground hit if the ground normal goes in the same direction as the character up
// and if the slope angle is lower than the character controller's limit
if (Vector3.Dot(hit.normal, transform.up) > 0f &&
IsNormalUnderSlopeLimit(m_GroundNormal))
{
IsGrounded = true;
// handle snapping to the ground
if (hit.distance > m_Controller.skinWidth)
{
m_Controller.Move(Vector3.down * hit.distance);
}
}
}
}
}
void HandleCharacterMovement()
{
// horizontal character rotation
{
// rotate the transform with the input speed around its local Y axis
transform.Rotate(
new Vector3(0f, (m_InputHandler.GetLookInputsHorizontal() * RotationSpeed * RotationMultiplier),
0f), Space.Self);
}
// vertical camera rotation
{
// add vertical inputs to the camera's vertical angle
m_CameraVerticalAngle += m_InputHandler.GetLookInputsVertical() * RotationSpeed * RotationMultiplier;
// limit the camera's vertical angle to min/max
m_CameraVerticalAngle = Mathf.Clamp(m_CameraVerticalAngle, -89f, 89f);
// apply the vertical angle as a local rotation to the camera transform along its right axis (makes it pivot up and down)
PlayerCamera.transform.localEulerAngles = new Vector3(m_CameraVerticalAngle, 0, 0);
}
// character movement handling
bool isSprinting = m_InputHandler.GetSprintInputHeld();
{
_GetHeadClearance = GetHeadClearance();
if ((_GetHeadClearance < 0.25f) && ( m_TargetCharacterHeight >= 0.50f))
{
m_TargetCharacterHeight = m_TargetCharacterHeight - 0.10f;
lastHeightDecreaseTime = Time.time; // Enregistre lheure de la réduction
}
if ((_GetHeadClearance > 1.00f) && ( m_TargetCharacterHeight <= 1.60f))
{
// Ne remonte que si plus de 15 secondes se sont écoulées depuis la dernière baisse
if (Time.time - lastHeightDecreaseTime >= 15f)
{
m_TargetCharacterHeight = m_TargetCharacterHeight + 0.10f;
}
}
if (isSprinting)
{
isSprinting = SetCrouchingState(false, false);
}
float speedModifier = isSprinting ? SprintSpeedModifier : 1f;
// converts move input to a worldspace vector based on our character's transform orientation
Vector3 worldspaceMoveInput = transform.TransformVector(m_InputHandler.GetMoveInput());
// handle grounded movement
if (IsGrounded)
{
// calculate the desired velocity from inputs, max speed, and current slope
Vector3 targetVelocity = worldspaceMoveInput * MaxSpeedOnGround * speedModifier;
// reduce speed if crouching by crouch speed ratio
if (IsCrouching)
targetVelocity *= MaxSpeedCrouchedRatio;
targetVelocity = GetDirectionReorientedOnSlope(targetVelocity.normalized, m_GroundNormal) *
targetVelocity.magnitude;
// smoothly interpolate between our current velocity and the target velocity based on acceleration speed
CharacterVelocity = Vector3.Lerp(CharacterVelocity, targetVelocity,
MovementSharpnessOnGround * Time.deltaTime);
// jumping
if (IsGrounded && m_InputHandler.GetJumpInputDown())
{
// force the crouch state to false
if (SetCrouchingState(false, false))
{
// start by canceling out the vertical component of our velocity
CharacterVelocity = new Vector3(CharacterVelocity.x, 0f, CharacterVelocity.z);
// then, add the jumpSpeed value upwards
CharacterVelocity += Vector3.up * JumpForce;
// play sound
AudioSource.PlayOneShot(JumpSfx);
// remember last time we jumped because we need to prevent snapping to ground for a short time
m_LastTimeJumped = Time.time;
HasJumpedThisFrame = true;
// Force grounding to false
IsGrounded = false;
m_GroundNormal = Vector3.up;
}
}
// footsteps sound
float chosenFootstepSfxFrequency =
(isSprinting ? FootstepSfxFrequencyWhileSprinting : FootstepSfxFrequency);
if (m_FootstepDistanceCounter >= 1f / chosenFootstepSfxFrequency)
{
m_FootstepDistanceCounter = 0f;
AudioSource.PlayOneShot(FootstepSfx);
}
// keep track of distance traveled for footsteps sound
m_FootstepDistanceCounter += CharacterVelocity.magnitude * Time.deltaTime;
}
// handle air movement
else
{
// add air acceleration
CharacterVelocity += worldspaceMoveInput * AccelerationSpeedInAir * Time.deltaTime;
// limit air speed to a maximum, but only horizontally
float verticalVelocity = CharacterVelocity.y;
Vector3 horizontalVelocity = Vector3.ProjectOnPlane(CharacterVelocity, Vector3.up);
horizontalVelocity = Vector3.ClampMagnitude(horizontalVelocity, MaxSpeedInAir * speedModifier);
CharacterVelocity = horizontalVelocity + (Vector3.up * verticalVelocity);
// apply the gravity to the velocity
CharacterVelocity += Vector3.down * GravityDownForce * Time.deltaTime;
}
}
// apply the final calculated velocity value as a character movement
Vector3 capsuleBottomBeforeMove = GetCapsuleBottomHemisphere();
Vector3 capsuleTopBeforeMove = GetCapsuleTopHemisphere(m_Controller.height);
m_Controller.Move(CharacterVelocity * Time.deltaTime);
// detect obstructions to adjust velocity accordingly
m_LatestImpactSpeed = Vector3.zero;
if (Physics.CapsuleCast(capsuleBottomBeforeMove, capsuleTopBeforeMove, m_Controller.radius,
CharacterVelocity.normalized, out RaycastHit hit, CharacterVelocity.magnitude * Time.deltaTime, -1,
QueryTriggerInteraction.Ignore))
{
// We remember the last impact speed because the fall damage logic might need it
m_LatestImpactSpeed = CharacterVelocity;
CharacterVelocity = Vector3.ProjectOnPlane(CharacterVelocity, hit.normal);
}
}
// Returns true if the slope angle represented by the given normal is under the slope angle limit of the character controller
bool IsNormalUnderSlopeLimit(Vector3 normal)
{
return Vector3.Angle(transform.up, normal) <= m_Controller.slopeLimit;
}
// Gets the center point of the bottom hemisphere of the character controller capsule
Vector3 GetCapsuleBottomHemisphere()
{
return transform.position + (transform.up * m_Controller.radius);
}
// Gets the center point of the top hemisphere of the character controller capsule
Vector3 GetCapsuleTopHemisphere(float atHeight)
{
return transform.position + (transform.up * (atHeight - m_Controller.radius));
}
// Gets a reoriented direction that is tangent to a given slope
public Vector3 GetDirectionReorientedOnSlope(Vector3 direction, Vector3 slopeNormal)
{
Vector3 directionRight = Vector3.Cross(direction, transform.up);
return Vector3.Cross(slopeNormal, directionRight).normalized;
}
void UpdateCharacterHeight(bool force)
{
// Update height instantly
if (force)
{
m_Controller.height = m_TargetCharacterHeight;
m_Controller.center = Vector3.up * m_Controller.height * 0.5f;
PlayerCamera.transform.localPosition = Vector3.up * m_TargetCharacterHeight * CameraHeightRatio;
lastHeightDecreaseTime = Time.time; // Enregistre lheure de la réduction
// m_Actor.AimPoint.transform.localPosition = m_Controller.center;
}
// Update smooth height
else if (m_Controller.height != m_TargetCharacterHeight)
{
// resize the capsule and adjust camera position
m_Controller.height = Mathf.Lerp(m_Controller.height, m_TargetCharacterHeight,
CrouchingSharpness * Time.deltaTime);
m_Controller.center = Vector3.up * m_Controller.height * 0.5f;
PlayerCamera.transform.localPosition = Vector3.Lerp(PlayerCamera.transform.localPosition,
Vector3.up * m_TargetCharacterHeight * CameraHeightRatio, CrouchingSharpness * Time.deltaTime);
lastHeightDecreaseTime = Time.time; // Enregistre lheure de la réduction
// m_Actor.AimPoint.transform.localPosition = m_Controller.center;
}
// Debug.Log("Height: " + m_Controller.height);
// Debug.Log("Height: " + CurrentCharacterHeight);
// heightText.text = $"Height: {m_Controller.height:F2} m";
}
// returns false if there was an obstruction
bool SetCrouchingState(bool crouched, bool ignoreObstructions)
{
// set appropriate heights
if (crouched)
{
m_TargetCharacterHeight = CapsuleHeightCrouching;
}
else
{
// Detect obstructions
if (!ignoreObstructions)
{
Collider[] standingOverlaps = Physics.OverlapCapsule(
GetCapsuleBottomHemisphere(),
GetCapsuleTopHemisphere(CapsuleHeightStanding),
m_Controller.radius,
-1,
QueryTriggerInteraction.Ignore);
foreach (Collider c in standingOverlaps)
{
if (c != m_Controller)
{
return false;
}
}
}
m_TargetCharacterHeight = CapsuleHeightStanding;
}
if (OnStanceChanged != null)
{
OnStanceChanged.Invoke(crouched);
}
IsCrouching = crouched;
return true;
}
float GetHeadClearance()
{
// Position du haut actuel du capsule collider
Vector3 headPosition = GetCapsuleTopHemisphere(m_Controller.height);
// Raycast vers le haut pour détecter le premier obstacle
RaycastHit hit;
if (Physics.Raycast(headPosition, Vector3.up, out hit, Mathf.Infinity, -1, QueryTriggerInteraction.Ignore))
{
// Retourne la distance entre la tête et l'obstacle
return hit.distance;
}
// Aucun obstacle détecté au-dessus : clearance infinie
return Mathf.Infinity;
}
}
}