Skip to content

Instantly share code, notes, and snippets.

@Kellojo
Last active July 13, 2025 16:04
Show Gist options
  • Save Kellojo/5482771ba2a5052a43eb86458267cd5a to your computer and use it in GitHub Desktop.
Save Kellojo/5482771ba2a5052a43eb86458267cd5a to your computer and use it in GitHub Desktop.
using System;
using KinematicCharacterController;
using UnityEngine;
using UnityEngine.Events;
// Requires https://assetstore.unity.com/packages/tools/physics/kinematic-character-controller-99131?srsltid=AfmBOoo58w1Y72hrXmLezY_KlLuoDZaYktGEHg0t2no4Mjzz6900IHtK
[RequireComponent(typeof(KinematicCharacterMotor))]
public class CharacterController : MonoBehaviour, ICharacterController {
[SerializeField] private float MovementSpeed = 9f;
[SerializeField] private float MovementResponsiveness = 20f;
[SerializeField] private float Gravity = -20f;
[SerializeField] private float JumpForce = 8f;
[SerializeField, Range(0, 1)] private float jumpHeldGravityMultiplier = 0.8f;
[SerializeField] private float CoyoteTime = 0.1f;
[SerializeField] private float airSpeed = 9f;
[SerializeField] private float airAcceleration = 60f;
[SerializeField] private Transform CameraTarget;
private KinematicCharacterMotor _motor;
[SerializeField] private Vector3 _requestedRotation;
[SerializeField] private Vector3 _requestedMovement;
[SerializeField] private bool _requestedJump;
[SerializeField] private bool _requestedJumpHelp;
[SerializeField] private Vector3 _requestedExternalForce;
[SerializeField] private float _timeSinceJumpRequested;
[SerializeField] private float _timeSinceUngrounded;
[SerializeField] private bool _ungroundedDueToJump;
/// <summary>
/// The character's current velocity caused by it's own movement
/// </summary>
public Vector3 CurrentOwnMoveVelocity => _motor.BaseVelocity;
/// <summary>
/// Includes external velocity such as velocity from moving platforms, ...
/// </summary>
public Vector3 CurrentOverallVelocity => _motor.Velocity;
public bool IsAirborne => !_motor.GroundingStatus.IsStableOnGround;
public float TurnSpeed => _requestedRotation.y;
public UnityEvent OnJump;
public UnityEvent OnLand;
private void Awake() {
Cursor.lockState = CursorLockMode.Locked;
_requestedRotation = transform.eulerAngles;
_motor = GetComponent<KinematicCharacterMotor>();
_motor.CharacterController = this;
}
private void Update() {
CameraTarget.localRotation = Quaternion.Euler(_requestedRotation.x, 0, 0);
_requestedRotation.x = CameraTarget.localRotation.eulerAngles.x;
}
public void UpdateInput(CharacterInputData input) {
_requestedRotation += new Vector3(-input.Rotation.y, input.Rotation.x, 0);
_requestedMovement = new Vector3(input.Movement.x, 0, input.Movement.z);
_requestedMovement = Vector3.ClampMagnitude(_requestedMovement, 1f);
_requestedMovement = CameraTarget.rotation * _requestedMovement;
var wasRequestingJump = _requestedJump;
_requestedJump = input.Jump || _requestedJump;
if (_requestedJump && !wasRequestingJump) {
_timeSinceJumpRequested = 0f;
}
_requestedJumpHelp = input.JumpHelp;
}
public void UpdateVelocity(ref Vector3 currentVelocity, float deltaTime) {
if (_requestedExternalForce.sqrMagnitude > 0) {
currentVelocity = _requestedExternalForce;
_requestedExternalForce = Vector3.zero;
_motor.ForceUnground(0);
} else if (_motor.GroundingStatus.IsStableOnGround) {
if (_ungroundedDueToJump) OnLand?.Invoke();
_timeSinceUngrounded = 0f;
_ungroundedDueToJump = false;
var groundedMovement = _motor.GetDirectionTangentToSurface(_requestedMovement, _motor.GroundingStatus.GroundNormal) * _requestedMovement.magnitude;
var targetVelocity = groundedMovement * MovementSpeed;
currentVelocity = Vector3.Lerp(currentVelocity, targetVelocity, 1 - Mathf.Exp(-MovementResponsiveness * deltaTime));
} else {
_timeSinceUngrounded += deltaTime;
if (_requestedMovement.sqrMagnitude > 0) {
var planarMovement = Vector3.ProjectOnPlane(_requestedMovement, _motor.CharacterUp).normalized * _requestedMovement.magnitude;
var currentPlanarVelocity = Vector3.ProjectOnPlane(currentVelocity, _motor.CharacterUp);
var movementForce = planarMovement * (airAcceleration * deltaTime);
var targetPlanarVelocity = currentPlanarVelocity + movementForce;
targetPlanarVelocity = Vector3.ClampMagnitude(targetPlanarVelocity, airSpeed);
currentVelocity += targetPlanarVelocity - currentPlanarVelocity;
}
var gravity = Gravity;
var verticalVelocity = Vector3.Dot(currentVelocity, _motor.CharacterUp);
if (_requestedJumpHelp && verticalVelocity > 0f) {
gravity *= jumpHeldGravityMultiplier;
}
currentVelocity += _motor.CharacterUp * (gravity * deltaTime);
}
if (_requestedJump) {
var grounded = _motor.GroundingStatus.IsStableOnGround;
var canCoyoteJump = _timeSinceUngrounded < CoyoteTime && !_ungroundedDueToJump;
if (grounded || canCoyoteJump) {
_requestedJump = false;
_motor.ForceUnground(0);
_ungroundedDueToJump = true;
var currentVerticalVelocity = Vector3.Dot(currentVelocity, _motor.CharacterUp);
var targetVerticalVelocity = Mathf.Max(currentVerticalVelocity, JumpForce);
currentVelocity += _motor.CharacterUp * (targetVerticalVelocity - currentVerticalVelocity);
OnJump?.Invoke();
} else {
_timeSinceJumpRequested += deltaTime;
var canJumpLater = _timeSinceJumpRequested < CoyoteTime;
_requestedJump = canJumpLater;
}
}
}
public void UpdateRotation(ref Quaternion currentRotation, float deltaTime) {
currentRotation = Quaternion.Euler(0, _requestedRotation.y, 0);
_requestedRotation = currentRotation.eulerAngles;
_requestedRotation.x = CameraTarget.localRotation.eulerAngles.x;
}
public void BeforeCharacterUpdate(float deltaTime) {
}
public void PostGroundingUpdate(float deltaTime) {
}
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 ProcessHitStabilityReport(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, Vector3 atCharacterPosition, Quaternion atCharacterRotation, ref HitStabilityReport hitStabilityReport) {
}
public void OnDiscreteCollisionDetected(Collider hitCollider) {
}
public void TeleportTo(Vector3 position) {
_motor.SetPosition(position);
}
public void ApplyExternalForce(Vector3 velocity) {
_requestedExternalForce = velocity;
}
}
public struct CharacterInputData {
public Vector2 Rotation;
public Vector3 Movement;
public bool Jump;
public bool JumpHelp;
}
using System;
using UnityEngine;
public class CharacterInput : MonoBehaviour
{
[SerializeField] private CharacterController _controller;
private void Update() {
var input = new CharacterInputData {
Rotation = new Vector2(Input.GetAxisRaw("Mouse X"), Input.GetAxisRaw("Mouse Y")),
Movement = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical")),
Jump = Input.GetButtonDown("Jump"),
JumpHelp = Input.GetButton("Jump")
};
_controller.UpdateInput(input);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment