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);
			}
		}
	}
}