Skip to content

Instantly share code, notes, and snippets.

@jcarcangiu
Created January 16, 2020 06:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jcarcangiu/3783fe2595bc93f5d2cc590dd7297242 to your computer and use it in GitHub Desktop.
Save jcarcangiu/3783fe2595bc93f5d2cc590dd7297242 to your computer and use it in GitHub Desktop.
Custom Character Controller
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
/// <summary>
/// Character Controller class.
/// </summary>
public class JC_CharacterController : JC_SimulatedPhysics
{
[Header("Player Character Settings")]
[SerializeField] private float speed = 3.5f;
[SerializeField] private float rotationSpeed = 10f;
[SerializeField] private float sprintMultiplier = 2f;
[SerializeField] private float jumpForce = 4.5f;
private float horizInput;
private float vertInput;
private IEnumerator jumpCoroutine;
private bool isJumping = false;
private bool isRunning = false;
public bool jumping
{ get { return isJumping; } }
public bool running
{ get { return isRunning; } }
public float horizontalInput
{ get { return horizInput; } }
public float verticalInput
{ get { return vertInput; } }
protected override void Awake()
{
base.Awake();
jumpCoroutine = Jumping();
}
protected override void FixedUpdate()
{
base.FixedUpdate();
// If not grounded can't control character.
if (grounded)
{
// Apply movement.
Move();
if (Input.GetKeyDown(KeyCode.Space))
StartCoroutine(jumpCoroutine);
else
{
isJumping = false;
StopCoroutine(jumpCoroutine);
}
}
else
// Apply Gravity.
ApplyGravity();
}
// Just to double check collision.
public void LateUpdate()
{
CollisionDetection();
}
#region Player Movement
/// <summary>
/// Gets the direction of current Input.
/// </summary>
private void GetMovementDirection()
{
horizInput = Input.GetAxis("Horizontal");
vertInput = Input.GetAxis("Vertical");
targetVelocity = Vector3.ClampMagnitude(new Vector3(horizInput, 0, vertInput), 1);
velocity += targetVelocity * sprintMultiplier;
}
/// <summary>
/// Moves the character on keyboard input. Adds a sprint when Shift is pressed.
/// </summary>
public void Move()
{
// Get direction from Input.
GetMovementDirection();
// Apply velocity. Change to GetButton.
if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))
{
appliedVelocity = new Vector3(velocity.x, 0, velocity.z) * speed * sprintMultiplier;
isRunning = true;
}
else
{
appliedVelocity = new Vector3(velocity.x, 0, velocity.z) * speed;
isRunning = false;
}
prevVelocity = appliedVelocity;
nextPos = transform.position + (appliedVelocity);
transform.position = Vector3.Lerp(transform.position, nextPos, Time.deltaTime);
// Unless the Input resulting is 0, apply rotation.
if (targetVelocity != Vector3.zero)
transform.localRotation = Quaternion.Slerp(transform.localRotation, Quaternion.LookRotation(targetVelocity), Time.deltaTime * rotationSpeed);
// Calculate collisions.
CollisionDetection();
// Reset movement direction vector.
nextPos = Vector3.zero;
// Reset velocity.
velocity = Vector3.zero;
}
// !!! Add Jumping cooldown.
/// <summary>
/// Adds a vertical impulse force to the character, if he's grounded.
/// </summary>
private IEnumerator Jumping()
{
Vector3 _prevVelocity = prevVelocity / 500;
float _jump = 0;
while (true)
{
_jump = jumpForce;
isJumping = true;
// If on slope make prev velocity not go forward so he can't climb on slopes.
nextPos = transform.position + new Vector3(_prevVelocity.x, _jump, _prevVelocity.z);
transform.position = Vector3.Lerp(transform.position, nextPos, Time.deltaTime);
nextPos = Vector3.zero;
yield return null;
}
}
#endregion
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class JC_SimpleAnimation : MonoBehaviour
{
// Make a list of Available animation states.
// Change clip
public enum Motion
{
Idle,
Running,
Jumping,
Falling
}
private Motion currMotion;
public Motion currentMotion
{ get { return currentMotion; } }
private Animator animator;
private JC_CharacterController cc;
private float notGroundedTimer = 0;
private void Awake()
{
animator = GetComponent<Animator>();
cc = GetComponent<JC_CharacterController>();
}
// Start is called before the first frame update
void Start()
{
animator.SetBool("isRunning", false);
}
// Update is called once per frame
void Update()
{
Animate();
}
private bool CanSwitchToGrounded()
{
if (cc.notGroundedTimer > 0.2)
return true;
else
return false;
}
// Check if he's grounded for less than 0.1 secs or smth before switching animation.
private void Animate()
{
// If the character is not mid-air and moving.
if (cc.grounded && ( cc.horizontalInput != 0 || cc.verticalInput != 0))
animator.SetBool("isRunning", true);
// If the character is grounded but not moving.
else if (cc.grounded && (cc.horizontalInput == 0 && cc.verticalInput == 0))
animator.SetBool("isRunning", false);
// If the character is not grounded.
else if (!cc.grounded)
animator.SetBool("isRunning", false);
if (cc.jumping)
animator.SetBool("isJumping", true);
else
animator.SetBool("isJumping", false);
if (cc.grounded)
animator.SetBool("isJumping", false);
if (animator.GetBool("isJumping"))
animator.SetBool("isRunning", false);
}
// To be Added.
// Shooting IK. (Moves the animation depending on Aim)
private void OnAnimatorIK(int layerIndex)
{
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class JC_SimpleCameraFollow : MonoBehaviour
{
public GameObject playerCharacter;
private Vector3 offset;
// Start is called before the first frame update
void Start()
{
offset = transform.position;
transform.position = offset + playerCharacter.transform.position;
}
// Update is called once per frame
void FixedUpdate()
{
SimpleCameraFollow();
}
void SimpleCameraFollow()
{
transform.position = Vector3.Lerp(transform.position, offset + playerCharacter.transform.position, Time.deltaTime * 3.5f);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// All simulated physics used by the Character Controller.
/// </summary>
public class JC_SimulatedPhysics : MonoBehaviour
{
[Header("Gravity Settings")]
[SerializeField] protected float gravity = 9.81f;
[SerializeField] private float mass = 1f;
[SerializeField] private bool isGrounded;
// Only really needed on the Player Character.
public bool grounded
{
get { return isGrounded; }
set { isGrounded = value; }
}
[SerializeField] protected float gravityTimer;
public float notGroundedTimer
{ get { return gravityTimer; } }
[SerializeField] protected float impulseTimer = 0f;
protected IEnumerator jumpingCoroutine;
protected Vector3 velocity;
protected Vector3 targetVelocity;
protected Vector3 nextPos;
protected Vector3 appliedVelocity;
protected Vector3 prevVelocity;
private Vector3 finalVelocity;
protected Collider myCollider;
// Collider properties.
// Change this to automatically resize with collider.
protected Vector3 myColliderOffset;
protected float myColliderRadius;
// Player Layer.
protected LayerMask ignorePlayer = ~(1 << 9);
// Objects the player collides with.
protected Collider[] currHitObjects = new Collider[0];
protected int currHitObjectsLength = 0;
[Header("Slope Detection Settings")]
[SerializeField] protected float maxSlope = 45f;
[SerializeField] protected float maxStep = 0.3f;
protected virtual void Awake()
{
myCollider = GetComponent<CapsuleCollider>();
if (myCollider == null)
{
Debug.Log("PC: No Collider Attached.");
myCollider = GetComponent<CapsuleCollider>();
}
myColliderRadius = myCollider.bounds.extents.x;
myColliderOffset = new Vector3(myColliderRadius, myColliderRadius - 0.05f, myColliderRadius);
}
protected virtual void FixedUpdate()
{
CheckForGroundedCapsule();
// Everytime the character is not grounded, restart the timer.
if (grounded)
gravityTimer = 0;
else
gravityTimer += Time.deltaTime;
}
#region Basic Collision Detection
/// <summary>
/// Checks if the character's collider is grounded.
/// </summary>
protected bool CheckForGroundedCapsule()
{
// Checks capsule collider, excluding itself.
if (Physics.CheckCapsule(myCollider.bounds.min + myColliderOffset, myCollider.bounds.max - myColliderOffset, myColliderRadius, ignorePlayer))
{
isGrounded = true;
return true;
}
else
{
isGrounded = false;
return false;
}
}
// Add Foot / Hands collider to make animation look better with IK.
/// <summary>
/// Detects collision with everything that is not the player.
/// </summary>
protected void CollisionDetection()
{
// ADD: include another 2 raycast check to see if the point in front & the point behind is valid and if he should stick to the ground.
Vector3 _startCheckCapsule = myCollider.bounds.min + myColliderOffset;
Vector3 _endCheckCapsule = myCollider.bounds.min + myColliderOffset + new Vector3(0, 1.05f, 0);
// If we are colliding with something. (Excluding the player)
if (Physics.CheckCapsule(_startCheckCapsule, _endCheckCapsule, myColliderRadius, ignorePlayer))
{
// Set currently hit objects size of array & array itself.
currHitObjectsLength = Physics.OverlapCapsuleNonAlloc(_startCheckCapsule, _endCheckCapsule, myColliderRadius, currHitObjects, ignorePlayer);
currHitObjects = new Collider[currHitObjectsLength];
currHitObjects = Physics.OverlapCapsule(_startCheckCapsule, _endCheckCapsule, myColliderRadius, ignorePlayer);
Vector3 _sumOverlapVector = Vector3.zero;
Vector3 _sumProjectVelocity = Vector3.zero;
for (int i = 0; i < currHitObjectsLength; i++)
{
// Everything else ignoring the Y axis.
if (Physics.ComputePenetration(myCollider, transform.position, Quaternion.identity, currHitObjects[i], currHitObjects[i].transform.position, currHitObjects[i].transform.rotation, out Vector3 dir, out float dist))
{
Vector3 _overlapVector = dir * dist;
Vector3 _projectedVelocity = Vector3.Project(velocity, -dir);
Debug.DrawLine(transform.position, transform.position + dir, Color.blue, Time.deltaTime);
float _hitSurfaceAngle;
//If we're hitting the ground.
if (currHitObjects[i].gameObject.layer == 10)
{
_sumOverlapVector += _overlapVector;
_sumProjectVelocity += _projectedVelocity;
}
else
{
// If the step is not too steep, don't need to check whether is slope or not, otherwise also check slope.
if (AltCheckStep(currHitObjects[i], myCollider, maxStep))
{
// Can GO.
_sumOverlapVector += _overlapVector;
_sumProjectVelocity += _projectedVelocity;
}
else
{
if (CheckSlope(myCollider.bounds.min + new Vector3(myColliderRadius, 0.5f, myColliderRadius), transform.TransformDirection(new Vector3(0, -1, 1f)), out RaycastHit hit, 1f, ignorePlayer, out _hitSurfaceAngle))
{
// Can Go.
_sumOverlapVector += _overlapVector;
_sumProjectVelocity += _projectedVelocity;
}
else
{
// Can't Go.
_sumOverlapVector += new Vector3(_overlapVector.x, 0, _overlapVector.z);
_sumProjectVelocity += new Vector3(_projectedVelocity.x, 0, _projectedVelocity.z);
}
}
}
}
}
Debug.DrawLine(transform.position, transform.position + _sumOverlapVector, Color.red, Time.deltaTime * 20);
transform.position += _sumOverlapVector;
appliedVelocity -= _sumProjectVelocity;
}
}
#endregion
#region Slope / Step Detection
/// <summary>Casts a ray that checks if the hit object's angle is too big for the character.</summary>
/// <param name="origin"> Origin of the Ray cast in front of the Character.</param>
/// <param name="direction"> Direction of the Ray cast in front of the Character.</param>
/// <param name="hitInfo"> Raycast hit object info.</param>
/// <param name="maxDistance"> Maximum distance reached by the Ray.</param>
/// <param name="layerMask"> Layermasks detected by the Ray.</param>
/// <param name="angle"> Resulting angle from the hit object normal. Set to -1 when there's no object to detect.</param>
/// <returns>False if the slope is too angled.</returns>
private bool CheckSlope(Vector3 origin, Vector3 direction, out RaycastHit hitInfo, float maxDistance, int layerMask, out float angle)
{
// Cast a Ray and check if the hit object is on a slope.
if (Physics.Raycast(origin, direction, out hitInfo, maxDistance, layerMask))
{
Debug.DrawRay(origin, direction, Color.green, Time.deltaTime * 10);
// If hit mesh is at a greater angle than the maximum slope.
if (Vector3.Angle(hitInfo.normal, Vector3.up) <= maxSlope)
{
Debug.DrawRay(origin, hitInfo.normal, Color.white, Time.deltaTime * 10);
angle = Vector3.Angle(hitInfo.normal, Vector3.up);
print("Can Go, Angle: " + angle);
return true;
}
else
{
Debug.DrawRay(origin, hitInfo.normal, Color.white, Time.deltaTime * 10);
angle = Vector3.Angle(hitInfo.normal, Vector3.up);
print("Can't Go, Angle: " + angle);
return false;
}
}
// If there nothing under the PC.
else
{
angle = -1;
return false;
}
}
/// <summary>Checks the height of the object in front of the player.</summary>
/// <param name="hitObjCollider"> Origin of the Ray cast in front of the Character.</param>
private bool AltCheckStep(Collider hitObjCollider, Collider goCollider, float maxStep)
{
// Check if the height of the collided with object is greater than the maxStep height.
if (hitObjCollider.bounds.max.y - goCollider.bounds.min.y < maxStep + 0.01f)
{
print("Can Go Step, " + hitObjCollider.name + " HitObj max Y bound: " + hitObjCollider.bounds.max.y + ", PC min Y collider: " + goCollider.bounds.min.y + ", " + (hitObjCollider.bounds.max.y - goCollider.bounds.min.y));
return true;
}
else
{
print("Can't Go Step, " + hitObjCollider.name + " HitObj max Y bound: " + hitObjCollider.bounds.max.y + ", PC min Y collider: " + goCollider.bounds.min.y + ", " + (hitObjCollider.bounds.max.y - goCollider.bounds.min.y));
return false;
}
}
/// <summary>
/// Casts a ray at a the maximum step height and checks if the hit object's step is too big for the character.
/// </summary>
private bool CheckStep(Vector3 origin, out RaycastHit hitInfo, float maxStep, int layerMask)
{
if (Physics.Raycast(origin, Vector3.forward, out hitInfo, maxStep * 1.5f, layerMask))
{
Debug.DrawRay(origin, Vector3.forward, Color.magenta, Time.deltaTime * 100);
// If it detects something over the maxStep Height, can't go.
return false;
}
else
{
// If the step is lower than the maxStep height it won't be detected and can go.
Debug.DrawRay(origin, Vector3.forward, Color.cyan, Time.deltaTime * 100);
return true;
}
}
#endregion
#region Basic Physics
/// <summary>
/// Applies gravity everytime the character is not grounded.
/// </summary>
protected void ApplyGravity()
{
// Dampen velocity a bit.
Vector3 _prevVelocity = prevVelocity / 2;
targetVelocity = Vector3.ClampMagnitude(Vector3.down, 1);
velocity += targetVelocity;
// Applying gravity acceleration to the y of the transform.
appliedVelocity = new Vector3(_prevVelocity.x, (velocity.y * gravityTimer) + (0.5f * -gravity * mass * gravityTimer * gravityTimer), _prevVelocity.z);
nextPos = transform.position + (appliedVelocity);
transform.position = Vector3.Lerp(transform.position, nextPos, Time.deltaTime);
CollisionDetection();
// Reset movement direction vector.
nextPos = Vector3.zero;
// Reset velocity.
velocity = Vector3.zero;
}
#endregion
#region Maths
//https://www.desmos.com/calculator
/// <summary>
/// Circular Easing In Function.
/// </summary>
protected float CircularEaseOut(float x)
{
return Mathf.Sqrt(1 - (x * x));
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment