Movement
The movement mechanics embody a fundamental yet refined approach to first-person locomotion. Rather than relying on Unity's CharacterController, I've implemented a bespoke physics-driven system to govern movement. This bespoke system not only provides a more tailored experience but also offers enhanced adaptability to varied terrains and air dynamics. This versatility extends to inclines and aerial maneuvers, ensuring seamless traversal across different environmental conditions.
Additionally, I've incorporated a dynamic jumping mechanic that complements the movement system. This jump mechanic operates independently, which grants the flexibility to easily fine-tune or even remove it, should project requirements demand such adjustments. This separation of movement and jumping mechanics exemplifies a modular design ethos, allowing for precise control and customization in alignment with project objectives.
Wall Running was seamlessly integrated as a distinctive gameplay element, offering intriguing possibilities for specific prototypes. This feature introduces a layer of innovation beyond conventional mechanics. The implementation incorporates a dynamic game juice system for wall-running, effectively conveying a sense of momentum and fluid horizontal movement. This not only enriches the player experience but also opens avenues for creative level design and captivating gameplay scenarios.
Movement
Jumping
Wall-Run
using System.Collections.Generic;
using System.Collections;
using UnityEngine;
using FMOD.Studio;
namespace FreyPhysicsController
{
public class Movement : MonoBehaviour
{
[Header("Dependencies")]
[SerializeField] private Dependencies dependencies;
[Header("Movement Properties")]
[SerializeField] private float walkSpeed = 6.5f;
[SerializeField] private float sprintSpeed = 12f;
[SerializeField] private float acceleration = 70f;
[SerializeField] private float multiplier = 10f;
[SerializeField] private float airMultiplier = 0.4f;
[Header("Tilt Properties")]
[SerializeField] private float strafeTilt = 1.1f;
[SerializeField] private float stafeTiltSpeed = 8f;
[Header("Drag Properties")]
[SerializeField] private float groundDrag = 6f;
[SerializeField] private float airDrag = 1f;
[Header("Ground Detection Properties")]
[SerializeField] private Transform groundCheck;
[SerializeField] private float groundCheckRadius = 0.2f;
[SerializeField] private float slopeAngleCheck = 0.5f;
[Header("Footstep Audio Properties")]
[SerializeField] private AnimationCurve footstepCurve;
[SerializeField] private float footstepMultiplier = 0.17f;
[SerializeField] private float footstepRate = 0.25f;
private float moveAmount;
private float horizontalMovement;
private float verticalMovement;
private float anticipatedSpeed;
private float playerHeight = 2f;
private float curveTime = 0f;
private bool movementTriggeredFootsteps;
private Camera cam;
private Rigidbody rb;
private Transform orientation;
private Vector3 moveDirection;
private Vector3 slopeMoveDirection;
private RaycastHit slopeHit;
private EventInstance playerFootsteps;
// Should be altered to be handled whatever way the current project maintains input/keybinding
private KeyCode sprintKey = KeyCode.LeftShift;
private void Start()
{
Initialize();
}
private void Update()
{
GroundCheck();
CalculatDirection();
CalculateSlope();
ControlSpeed();
ControlDrag();
StrafeTilt();
Footsteps();
}
private void FixedUpdate()
{
Move();
}
private void Initialize()
{
//Set player on the ignore raycast layer
transform.gameObject.layer = 2;
// Setup dependencies
rb = dependencies.rb;
cam = dependencies.cam;
orientation = dependencies.orientation;
// FMOD
playerFootsteps = AudioManager.Instance.CreateInstance(FMODEvents.Instance.playerFootsteps);
// Set rigidbody properties
rb.freezeRotation = true;
rb.mass = 50;
}
private void GroundCheck()
{
dependencies.isGrounded = Physics.CheckSphere(groundCheck.position, groundCheckRadius);
if(!dependencies.isGrounded)
{
// Fadeout footsteps if the player is not grounded
PLAYBACK_STATE playbackState;
playerFootsteps.getPlaybackState(out playbackState);
if (playbackState.Equals(PLAYBACK_STATE.PLAYING))
{
playerFootsteps.stop(STOP_MODE.ALLOWFADEOUT);
movementTriggeredFootsteps = false;
}
}
}
private void CalculatDirection()
{
horizontalMovement = Input.GetAxisRaw("Horizontal");
verticalMovement = Input.GetAxisRaw("Vertical");
// Set calculated direction
moveDirection = orientation.forward * verticalMovement + orientation.right * horizontalMovement;
}
private void CalculateSlope()
{
// Get slope Vector in regard to direction and normal orthogonal to said plane
slopeMoveDirection = Vector3.ProjectOnPlane(moveDirection, slopeHit.normal);
}
private void ControlSpeed()
{
anticipatedSpeed = Input.GetKey(sprintKey) && dependencies.isGrounded ? sprintSpeed : walkSpeed;
moveAmount = Mathf.Lerp(moveAmount, anticipatedSpeed, acceleration * Time.deltaTime);
}
// Add drag to movement
private void ControlDrag() => rb.drag = dependencies.isGrounded ? groundDrag : airDrag;
private void StrafeTilt()
{
// Calculate tilt direction
if(horizontalMovement != 0f)
{
dependencies.tilt = Mathf.Lerp(dependencies.tilt, horizontalMovement < 0f ? strafeTilt : -strafeTilt, stafeTiltSpeed * Time.deltaTime);
}
}
// Footstep Audio, can be decoupled further based on accompanying features
private void Footsteps()
{
if(dependencies.isGrounded || dependencies.isWallRunning)
{
if(!dependencies.isVaulting && !dependencies.isInspecting)
{
// Combine input
Vector2 inputVector = new Vector2(horizontalMovement, verticalMovement);
// Start curve timer
if(inputVector.magnitude > 0f)
{
//Curve timer
if(dependencies.isGrounded)
{
curveTime += Time.deltaTime * footstepRate * moveAmount;
}
else if(dependencies.isWallRunning)
{
curveTime += Time.deltaTime * footstepRate * 2.5f * moveAmount;
}
//Reset time, loop time and play footstep sound
if(curveTime >= 1f)
{
// FMOD Audio
PLAYBACK_STATE playbackState;
playerFootsteps.getPlaybackState(out playbackState);
if (playbackState.Equals(PLAYBACK_STATE.STOPPED))
{
playerFootsteps.start();
movementTriggeredFootsteps = true;
}
curveTime = 0f;
}
}
// Fadeout / Clear footstep audio being played
else if(movementTriggeredFootsteps)
{
PLAYBACK_STATE playbackState;
playerFootsteps.getPlaybackState(out playbackState);
if (playbackState.Equals(PLAYBACK_STATE.PLAYING))
{
playerFootsteps.stop(STOP_MODE.ALLOWFADEOUT);
movementTriggeredFootsteps = false;
}
}
}
//Adjust camera height to animation curve value for bobbing effect when moving
cam.transform.localPosition = new Vector3(cam.transform.localPosition.x, footstepCurve.Evaluate(curveTime) * footstepMultiplier, cam.transform.localPosition.z);
}
}
private bool OnSlope()
{
if(Physics.Raycast(rb.transform.position, Vector3.down, out slopeHit, playerHeight / 2 + 0.5f))
{
if(slopeHit.normal != Vector3.up)
return true;
else
return false;
}
return false;
}
// Apply player movement
private void Move()
{
// Generic Movement
if(dependencies.isGrounded && !dependencies.isInspecting && !OnSlope())
{
rb.AddForce(moveDirection.normalized * moveAmount * multiplier, ForceMode.Acceleration);
}
// On a slope
if(dependencies.isGrounded && OnSlope())
{
rb.AddForce(slopeMoveDirection.normalized * moveAmount * multiplier, ForceMode.Acceleration);
}
// In the air / not grounded
if(!dependencies.isGrounded)
{
rb.AddForce(moveDirection.normalized * moveAmount * multiplier * airMultiplier, ForceMode.Acceleration);
}
}
}
}
using UnityEngine;
using FreyPhysicsController;
namespace FreyPhysicsController
{
public class Jump : MonoBehaviour
{
[Header("Dependencies")]
[SerializeField] private Dependencies dependencies;
[Header("Input Properties")]
[SerializeField] private KeyCode jumpKey = KeyCode.Space;
[Header("Jumping Properties")]
[SerializeField] private float amount = 14f;
[SerializeField] private float cooldown = 15f;
[Header("Landing Properties")]
[SerializeField] private float distanceBeforeForce = 25f;
[SerializeField] private float rateBeforeForce = -15f;
[SerializeField] private float hardLandForce = 0.25f;
private float nextTimeToJump = 0f;
private bool landed = true;
private Vector3 newFallVelocity;
private Rigidbody rb;
private RaycastHit falltHit;
private void Start()
{
Initialize();
}
private void Update()
{
Land();
}
private void FixedUpdate()
{
SimulateJump();
Fall();
}
private void Initialize()
{
rb = dependencies.rb;
}
//Initiate jump
private void SimulateJump()
{
if (Input.GetKey(jumpKey) && dependencies.isGrounded && !dependencies.isWallRunning && !dependencies.isInspecting && Time.time >= nextTimeToJump)
{
nextTimeToJump = Time.time + 1f / cooldown;
rb.AddForce(Vector3.up * (amount - rb.velocity.y), ForceMode.VelocityChange);
}
}
private void Fall()
{
if (!dependencies.isGrounded && rb.velocity.y < rateBeforeForce && Physics.Raycast(rb.transform.position, Vector3.down, out fallHit, distanceBeforeForce))
{
//Apply additional force towards ground if falling faster the fall rate
rb.velocity += Vector3.up * (-hardLandForce);
}
}
private void Land() => landed = dependencies.isGrounded ? true : false;
}
}
using UnityEngine;
using FreyPhysicsController;
namespace FreyPhysicsController
{
public class WallRun : MonoBehaviour
{
[Header("Dependencies")]
[SerializeField] private Dependencies dependencies;
[Header("Detection Properties")]
[SerializeField] private float wallCheckDistance = 1f;
[SerializeField] private float minOffGroundHeight = 1f;
[Header("Wall Run Properties")]
[SerializeField] private float onWallGravity = 2f;
[SerializeField] private float onWallJumpAmount = 8f;
[Header("Wall Run Camera Properties")]
[SerializeField] private float onWallFov = 65f;
[SerializeField] private float fovChangeSpeed = 10f;
[SerializeField] private float onWallTilt = 20f;
[SerializeField] private float onWallTiltSpeed = 5f;
[Header("Audio Properties")]
[SerializeField] private AudioClip wallJumpSound;
private float fov = 60;
private bool wallLeft = false;
private bool wallRight = false;
private bool jumping = false;
private bool gravityChange = false;
private RaycastHit leftWallHit;
private RaycastHit rightWallHit;
private Rigidbody rb;
private Camera cam;
private CapsuleCollider cc;
private Transform orientation;
private Vector3 jumpDirection;
private void Start()
{
Initialize();
}
private void Update()
{
CheckWall();
WallRunning();
}
private void FixedUpdate()
{
WallRunPhysics();
}
private void Initialize()
{
rb = dependencies.rb;
cam = dependencies.cam;
cc = dependencies.cc;
orientation = dependencies.orientation;
audioSource = dependencies.audioSourceBottom;
fov = cam.fieldOfView;
}
//Check if possible to wall run (is off the ground)
private bool CanWallRun()
{
return !Physics.Raycast(rb.transform.position + new Vector3(0, cc.height / 2, 0), Vector3.down, minOffGroundHeight);
}
//Check sides for walls
private void CheckWall()
{
wallLeft = Physics.Raycast(rb.transform.position, -orientation.right, out leftWallHit, wallCheckDistance);
wallRight = Physics.Raycast(rb.transform.position, orientation.right, out rightWallHit, wallCheckDistance);
}
private void WallRunning()
{
if (!CanWallRun())
{
ExitWallRun();
return;
}
if (!dependencies.isGrounded || (!wallLeft && !wallRight))
{
ExitWallRun();
return;
}
rb.useGravity = false;
dependencies.isWallRunning = true;
TransitionFOV();
TransitionTilt();
if (!gravityChange)
{
gravityChange = true;
}
WallRunJump();
}
private void ExitWallRun()
{
jumping = false;
gravityChange = false;
rb.useGravity = true;
dependencies.isWallRunning = false;
ResetFOV();
}
private void TransitionFOV()
{
var fovSpeed = fovChangeSpeed * Time.deltaTime;
cam.fieldOfView = Mathf.Lerp(cam.fieldOfView, onWallFov, fovSpeed);
}
private void TransitionTilt()
{
var tiltSpeed = onWallTiltSpeed * Time.deltaTime;
float targetTilt = wallLeft ? -onWallTilt : (wallRight ? onWallTilt : 0f);
dependencies.tilt = Mathf.Lerp(dependencies.tilt, targetTilt, tiltSpeed);
}
private void WallRunJump()
{
if (Input.GetKeyDown(KeyCode.Space))
{
if (wallLeft)
{
jumpDirection = rb.transform.up * 1.8f + leftWallHit.normal;
}
else if (wallRight)
{
jumpDirection = rb.transform.up + rightWallHit.normal;
}
if (!jumping)
{
jumping = true;
}
}
}
private void WallRunPhysics()
{
//Wall run gravity
if(gravityChange)
{
rb.AddForce(Vector3.down * (onWallGravity * 0.01f), ForceMode.VelocityChange);
}
//Wall run jump
if(jumping)
{
rb.velocity = new Vector3(rb.velocity.x, 0, rb.velocity.z);
rb.AddForce(jumpDirection * (onWallJumpAmount * 0.05f), ForceMode.VelocityChange);
}
}
}
}