using System.Collections; using System.Collections.Generic; using UnityEngine; using KinematicCharacterController; using System; namespace KinematicCharacterController.Walkthrough.FramePerfectRotation { public struct PlayerCharacterInputs { public float MoveAxisForward; public float MoveAxisRight; public Quaternion CameraRotation; } public class MyCharacterController : MonoBehaviour, ICharacterController { public KinematicCharacterMotor Motor; [Header("Stable Movement")] public float MaxStableMoveSpeed = 10f; public float StableMovementSharpness = 15; public float OrientationSharpness = 10; [Header("Air Movement")] public float MaxAirMoveSpeed = 10f; public float AirAccelerationSpeed = 5f; public float Drag = 0.1f; [Header("Misc")] public Vector3 Gravity = new Vector3(0, -30f, 0); public Transform MeshRoot; public bool FramePerfectRotation = true; private Vector3 _moveInputVector; private Vector3 _lookInputVector; private void Start() { // Assign to motor Motor.CharacterController = this; } /// /// This is called every frame by MyPlayer in order to tell the character what its inputs are /// public void SetInputs(ref PlayerCharacterInputs inputs) { // Clamp input Vector3 moveInputVector = Vector3.ClampMagnitude(new Vector3(inputs.MoveAxisRight, 0f, inputs.MoveAxisForward), 1f); // Calculate camera direction and rotation on the character plane Vector3 cameraPlanarDirection = Vector3.ProjectOnPlane(inputs.CameraRotation * Vector3.forward, Motor.CharacterUp).normalized; if (cameraPlanarDirection.sqrMagnitude == 0f) { cameraPlanarDirection = Vector3.ProjectOnPlane(inputs.CameraRotation * Vector3.up, Motor.CharacterUp).normalized; } Quaternion cameraPlanarRotation = Quaternion.LookRotation(cameraPlanarDirection, Motor.CharacterUp); // Move and look inputs _moveInputVector = cameraPlanarRotation * moveInputVector; _lookInputVector = cameraPlanarDirection; } public void PostInputUpdate(float deltaTime, Vector3 cameraForward) { if (FramePerfectRotation) { _lookInputVector = Vector3.ProjectOnPlane(cameraForward, Motor.CharacterUp); Quaternion newRotation = default; HandleRotation(ref newRotation, deltaTime); MeshRoot.rotation = newRotation; } } private void HandleRotation(ref Quaternion rot, float deltaTime) { if (_lookInputVector != Vector3.zero) { rot = Quaternion.LookRotation(_lookInputVector, Motor.CharacterUp); } } /// /// (Called by KinematicCharacterMotor during its update cycle) /// This is called before the character begins its movement update /// public void BeforeCharacterUpdate(float deltaTime) { } /// /// (Called by KinematicCharacterMotor during its update cycle) /// This is where you tell your character what its rotation should be right now. /// This is the ONLY place where you should set the character's rotation /// public void UpdateRotation(ref Quaternion currentRotation, float deltaTime) { HandleRotation(ref currentRotation, deltaTime); } /// /// (Called by KinematicCharacterMotor during its update cycle) /// This is where you tell your character what its velocity should be right now. /// This is the ONLY place where you can set the character's velocity /// public void UpdateVelocity(ref Vector3 currentVelocity, float deltaTime) { Vector3 targetMovementVelocity = Vector3.zero; if (Motor.GroundingStatus.IsStableOnGround) { // Reorient velocity on slope currentVelocity = Motor.GetDirectionTangentToSurface(currentVelocity, Motor.GroundingStatus.GroundNormal) * currentVelocity.magnitude; // Calculate target velocity Vector3 inputRight = Vector3.Cross(_moveInputVector, Motor.CharacterUp); Vector3 reorientedInput = Vector3.Cross(Motor.GroundingStatus.GroundNormal, inputRight).normalized * _moveInputVector.magnitude; targetMovementVelocity = reorientedInput * MaxStableMoveSpeed; // Smooth movement Velocity currentVelocity = Vector3.Lerp(currentVelocity, targetMovementVelocity, 1 - Mathf.Exp(-StableMovementSharpness * deltaTime)); } else { // Add move input if (_moveInputVector.sqrMagnitude > 0f) { targetMovementVelocity = _moveInputVector * MaxAirMoveSpeed; // Prevent climbing on un-stable slopes with air movement if (Motor.GroundingStatus.FoundAnyGround) { Vector3 perpenticularObstructionNormal = Vector3.Cross(Vector3.Cross(Motor.CharacterUp, Motor.GroundingStatus.GroundNormal), Motor.CharacterUp).normalized; targetMovementVelocity = Vector3.ProjectOnPlane(targetMovementVelocity, perpenticularObstructionNormal); } Vector3 velocityDiff = Vector3.ProjectOnPlane(targetMovementVelocity - currentVelocity, Gravity); currentVelocity += velocityDiff * AirAccelerationSpeed * deltaTime; } // Gravity currentVelocity += Gravity * deltaTime; // Drag currentVelocity *= (1f / (1f + (Drag * deltaTime))); } } /// /// (Called by KinematicCharacterMotor during its update cycle) /// This is called after the character has finished its movement update /// public void AfterCharacterUpdate(float deltaTime) { } public bool IsColliderValidForCollisions(Collider coll) { return true; } public void OnGroundHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, ref HitStabilityReport hitStabilityReport) { } public void OnMovementHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, ref HitStabilityReport hitStabilityReport) { } public void PostGroundingUpdate(float deltaTime) { } public void AddVelocity(Vector3 velocity) { } public void ProcessHitStabilityReport(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, Vector3 atCharacterPosition, Quaternion atCharacterRotation, ref HitStabilityReport hitStabilityReport) { } public void OnDiscreteCollisionDetected(Collider hitCollider) { } } }