Created
January 16, 2020 06:26
-
-
Save jcarcangiu/3783fe2595bc93f5d2cc590dd7297242 to your computer and use it in GitHub Desktop.
Custom Character Controller
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
{ | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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