using System;
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
#region Variables: Movement
private Vector2 _input;
private CharacterController _characterController;
private Animator _animator;
private Vector3 _direction;
[SerializeField] private float speed;
[SerializeField] private float directionDampTime;
[SerializeField] private Movement movement;
#endregion
#region Variables: Rotation
[SerializeField] private float rotationSpeed = 500;
private Camera _mainCamera;
#endregion
#region Variables: Gravity
private float _gravity = -9.81f;
[SerializeField] private float gravityMultiplier = 3;
private float _velocity;
#endregion
#region Variables: Jumping
[SerializeField] private float jumpPower;
[SerializeField] private float jumpCooldown = 0.5f;
[SerializeField] private float jumpBufferTime = 0.2f;
[SerializeField] private float coyoteTime = 0.2f;
private float lastJumpTime = -Mathf.Infinity;
private float lastJumpPressedTime = -Mathf.Infinity;
private float lastGroundedTime = -Mathf.Infinity;
#endregion
private void Start()
{
if (_animator.layerCount >= 2)
{
_animator.SetLayerWeight(1, 1);
}
}
private void Awake()
{
_characterController = GetComponent<CharacterController>();
_animator = GetComponent<Animator>();
_mainCamera = Camera.main;
}
private void Update()
{
ApplyRotation();
ApplyGravity();
ApplyMovement();
HandleBufferedJump();
UpdateLastGroundedTime();
enableFeetIk = IsGrounded();
float inputMagnitude = new Vector2(_input.x, _input.y).magnitude;
float targetSpeed = inputMagnitude * (movement.isSprinting ? 2f : 1f);
speed = Mathf.Lerp(speed, targetSpeed, Time.deltaTime * 10f);
float direction = 0f;
if (Mathf.Abs(_input.y) > 0.1f)
{
direction = _input.x * Mathf.Sign(_input.y);
}
else
{
direction = 0f;
}
_animator.SetFloat("Speed", speed);
_animator.SetFloat("Direction", direction, directionDampTime, Time.deltaTime);
}
private void ApplyGravity()
{
if (IsGrounded() && _velocity < 0)
{
_velocity = -1;
_animator.SetBool("Jump", false);
}
else
{
_velocity += _gravity * gravityMultiplier * Time.deltaTime;
}
_direction.y = _velocity;
}
private void ApplyRotation()
{
if (_input.sqrMagnitude == 0) return;
_direction = Quaternion.Euler(0, _mainCamera.transform.eulerAngles.y, 0) * new Vector3(_input.x, 0, _input.y);
var targetRotation = Quaternion.LookRotation(_direction, Vector3.up);
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
}
private void ApplyMovement()
{
var targetSpeed = movement.isSprinting ? movement.speed * movement.multiplier : movement.speed;
movement.currentSpeed = Mathf.MoveTowards(movement.currentSpeed, targetSpeed, movement.acceleration * Time.deltaTime);
Vector3 move = new Vector3(_direction.x, 0, _direction.z).normalized * movement.currentSpeed;
move.y = _velocity;
_characterController.Move(move * Time.deltaTime);
}
public void Move(InputAction.CallbackContext context)
{
_input = context.ReadValue<Vector2>();
_direction = new Vector3(_input.x, 0, _input.y);
}
public void Jump(InputAction.CallbackContext context)
{
if (!context.started) return;
lastJumpPressedTime = Time.time;
}
private void UpdateLastGroundedTime()
{
if (IsGrounded())
{
lastGroundedTime = Time.time;
}
}
private void HandleBufferedJump()
{
if (Time.time - lastJumpPressedTime <= jumpBufferTime &&
Time.time >= lastJumpTime + jumpCooldown &&
(IsGrounded() || Time.time - lastGroundedTime <= coyoteTime))
{
PerformJump();
lastJumpPressedTime = -Mathf.Infinity;
}
}
private void PerformJump()
{
_velocity += jumpPower;
_animator.SetBool("Jump", true);
lastJumpTime = Time.time;
}
public void Sprint(InputAction.CallbackContext context)
{
movement.isSprinting = context.started || context.performed;
}
private bool IsGrounded() => _characterController.isGrounded;
private Vector3 rightFootPosition, leftFootPosition, leftFootIkPostion, rightFootIkPosition;
private Quaternion leftFootIkRotation, rightFootIkRotation;
private float lastPelvisPositionY, lastRightFootPostionY, lastLeftFootPostionY;
[Header("Feet IK")]
public bool enableFeetIk = true;
[Range(0, 2f)][SerializeField] private float heightFromGroundRayCast = 1.14f;
[Range(0, 2f)][SerializeField] private float raycastDownDistance = 1.5f;
[SerializeField] private LayerMask environmentLayer;
[SerializeField] private float pelvisOffset;
[Range(0, 1f)][SerializeField] private float pelvisUpAndDownSpeed = 0.28f;
[Range(0, 1f)][SerializeField] private float feetToIkPositionSpeed = 0.5f;
public string leftFootAnimVariableName = "RightFootCurve";
public string rightFootAnimVariableName = "LeftFootCurve";
public bool useProIkFeature = false;
public bool showSolverDebug = true;
/// <summary>
/// We are updating the AdjustFeetTarget method and also find the position of each foot inside our Solver Position.
/// </summary>
private void FixedUpdate()
{
if (enableFeetIk == false) { return; }
if (_animator == null) { return; }
AdjustFeetTarget(ref rightFootPosition, HumanBodyBones.RightFoot);
AdjustFeetTarget(ref leftFootPosition, HumanBodyBones.LeftFoot);
//find an raycast to the ground to find positions
FeetPositionSolver(rightFootPosition, ref rightFootIkPosition, ref rightFootIkRotation); // handle the solver for right foot
FeetPositionSolver(leftFootPosition, ref leftFootIkPostion, ref leftFootIkRotation); // handle the solver for left foot
}
private void OnAnimatorIK(int layerIndex)
{
if (enableFeetIk == false) { return; }
if (_animator == null) { return; }
MovePelvisHeight();
//right foot ik position and rotation -- utilise the pro featuresin here
_animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, 1);
if (useProIkFeature)
{
_animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, _animator.GetFloat(rightFootAnimVariableName));
}
MoveFeetToIkPoint(AvatarIKGoal.RightFoot, rightFootIkPosition, rightFootIkRotation, ref lastRightFootPostionY);
//left foot ik position and rotation -- utilise the pro featuresin here
_animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, 1);
if (useProIkFeature)
{
_animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, _animator.GetFloat(leftFootAnimVariableName));
}
MoveFeetToIkPoint(AvatarIKGoal.LeftFoot, leftFootIkPostion, leftFootIkRotation, ref lastLeftFootPostionY);
}
void MoveFeetToIkPoint(AvatarIKGoal foot, Vector3 positionIkHollder, Quaternion rotationIkHolder, ref float lastFootPostionY)
{
Vector3 targetIkPosition = _animator.GetIKPosition(foot);
if (positionIkHollder != Vector3.zero)
{
targetIkPosition = transform.InverseTransformPoint(targetIkPosition);
positionIkHollder = transform.InverseTransformPoint(positionIkHollder);
float yVariable = Mathf.Lerp(lastFootPostionY, positionIkHollder.y, feetToIkPositionSpeed);
targetIkPosition.y += yVariable;
lastFootPostionY = yVariable;
targetIkPosition = transform.TransformPoint(targetIkPosition);
_animator.SetIKRotation(foot, rotationIkHolder);
}
_animator.SetIKPosition(foot, targetIkPosition);
}
private void MovePelvisHeight()
{
if (rightFootIkPosition == Vector3.zero || leftFootIkPostion == Vector3.zero || lastPelvisPositionY == 0)
{
lastPelvisPositionY = _animator.bodyPosition.y;
return;
}
float lOffestPosition = leftFootIkPostion.y - transform.position.y;
float rOffsetPosition = rightFootIkPosition.y - transform.position.y;
float totalOffset = (lOffestPosition < rOffsetPosition) ? lOffestPosition : rOffsetPosition;
Vector3 newPelvisPosition = _animator.bodyPosition + Vector3.up * totalOffset;
newPelvisPosition.y = Mathf.Lerp(lastPelvisPositionY, newPelvisPosition.y, pelvisUpAndDownSpeed);
_animator.bodyPosition = newPelvisPosition;
lastPelvisPositionY = _animator.bodyPosition.y;
}
/// <summary>
/// We are locating the feet position via a Raycast and then Solving
/// </summary>
/// <param name="fromSkyPosition"></param>
/// <param name="feetIkPostions"></param>
/// <param name="feetIkRotations"></param>
private void FeetPositionSolver(Vector3 fromSkyPosition, ref Vector3 feetIkPostions, ref Quaternion feetIkRotations)
{
// raycast handling section
RaycastHit feetOutHit;
if (showSolverDebug)
{
Debug.DrawLine(fromSkyPosition, fromSkyPosition + Vector3.down * (raycastDownDistance + heightFromGroundRayCast), Color.yellow);
}
if (Physics.Raycast(fromSkyPosition, Vector3.down, out feetOutHit, raycastDownDistance + heightFromGroundRayCast, environmentLayer))
{
// finding our feet ik positions from the sky position
feetIkPostions = fromSkyPosition;
feetIkPostions.y = feetOutHit.point.y + pelvisOffset;
feetIkRotations = Quaternion.FromToRotation(Vector3.up, feetOutHit.normal) * transform.rotation;
return;
}
feetIkPostions = Vector3.zero; // it didn't work
}
/// <summary>
/// Adjusts the feet target.
/// </summary>
/// <param name="feetPositions"></param>
/// <param name="foot"></param>
private void AdjustFeetTarget(ref Vector3 feetPositions, HumanBodyBones foot)
{
feetPositions = _animator.GetBoneTransform(foot).position;
feetPositions.y = transform.position.y + heightFromGroundRayCast;
}
}
[Serializable]
public struct Movement
{
public float speed;
public float multiplier;
public float acceleration;
[HideInInspector] public bool isSprinting;
[HideInInspector] public float currentSpeed;
}