using System;
using UnityEngine;
public class PlayerMovementImproved : MonoBehaviour {
[Header("Assignables")]
[Tooltip("This is a reference to the MainCamera object, not the parent of it.")]
public Transform playerCam;
[Tooltip("The Camera Holder not the main camera itself")]
public GameObject cam;
[Tooltip("Reference to the Gun, not the container")]
public GameObject gun;
[Tooltip("Reference to orientation object, needed for moving forward and not up or something.")]
public Transform orientation;
[Tooltip("What is the Settings Panel to toggle with Tab")]
public GameObject canvas;
[Tooltip("LayerMask for ground layer, important because otherwise the collision detection wont know what ground is")]
public LayerMask whatIsGround;
private Rigidbody rb;
[Header("Interaction Bools")]
public bool busy;
public bool upgrading;
public bool escaped;
public bool escaping;
public bool buying;
public bool aiming;
[Header("Rotation and look")]
private float xRotation;
[Tooltip("mouse/look sensitivity")]
public int sensitivity = 50;
private float sensMultiplier = 1.5f;
private Vector2 lookInput;
[Header("Movement")]
[Tooltip("additive force amount. every physics update that forward is pressed, this force (multiplied by 1/tickrate) will be added to the player.")]
public float moveSpeed = 4500;
private Vector2 walkInput;
[Tooltip("maximum local velocity before input is cancelled")]
public float maxSpeed = 20;
[Tooltip("normal countermovement when not crouching.")]
public float counterMovement = 0.175f;
private float threshold = 0.0001f;
[Tooltip("the maximum angle the ground can have relative to the players up direction.")]
public float maxSlopeAngle = 35f;
private Vector3 crouchScale = new Vector3(1, 0.5f, 1);
private Vector3 playerScale;
[Tooltip("forward force for when a crouch is started.")]
public float slideForce = 400;
[Tooltip("countermovement when sliding. this doesnt work the same way as normal countermovement.")]
public float slideCounterMovement = 0.2f;
[Tooltip("Sets the max amount of jumps allowed (including initial jump from ground) before hitting the ground again")]
public int jumpCacheMax = 2;
public int jumpCache = 2;
private bool jumpReset;
private bool readyToJump = true;
private float jumpCooldown = 0.25f;
[Tooltip("this determines the jump force but is also applied when jumping off of walls, if you decrease it, you may end up being able to walljump and then get back onto the wall leading to infinite height.")]
public float jumpForce = 550f;
float x, y;
bool jumping;
private Vector3 normalVector = Vector3.up;
/*
[Header("Hovering")]
Vector3 hoverVelocity;
float hoverDuration = 2f;
float fallDuration = 1f;
bool hovering;
*/
[Header("Gun Fancy Toggles")]
bool swayEnabled;
bool swayRotEnabled;
bool bobEnabled;
bool bobRotEnabled;
[Header("Sway")]
public float step = 0.01f; //Multiplied by the value from the mouse for 1 frame
public float maxStepDistance = 0.06f; //Max distance from the local origin
Vector3 swayPos; //Store our value for later
float smooth = 10f; //Used by both BobOffset and Sway
[Header("Sway Rotation")]
public float rotationStep = 4f; //Multiplied by the value from the mouse 1 frame
public float maxRotationStep = 5f; //Max rotation from the local identity rotation
Vector3 swayEulerRot; //Store our value
float smoothRot = 12f; //Used by both BobSway and TiltSway
[Header("Bobbing")]
public float speedCurve; //Used by both bobbing methods
float curveSin { get => Mathf.Sin(speedCurve); } //Easy getter for the sin of our curve
float curveCos { get => Mathf.Cos(speedCurve); } //Easy getter for the cos of our curve
public Vector3 travelLimit = Vector3.one * 0.025f; //The maximum limits of travel from the move input
public Vector3 bobLimit = Vector3.one * 0.01f; //Limits of travel from bobbing over time
Vector3 bobPosition;
[Header("Bob Rotation")]
public Vector3 multiplier;
Vector3 bobEulerRotation;
[Header("WallRunning")]
[Tooltip("Does Nothing because everything is built off the ground layer and changing it is a nightmare, so currently does nothing")]
public LayerMask whatIsWall;
private float actualWallRotation;
private float wallRotationVel;
private Vector3 wallNormalVector;
[Tooltip("when wallrunning, an upwards force is constantly applied to negate gravity by about half (at default), increasing this value will lead to more upwards force and decreasing will lead to less upwards force.")]
public float wallRunGravity = 1;
[Tooltip("when a wallrun is started, an upwards force is applied, this describes that force.")]
public float initialForce = 20f;
[Tooltip("float to choose how much force is applied outwards when ending a wallrun. this should always be greater than Jump Force")]
public float escapeForce = 600f;
private float wallRunRotation;
[Tooltip("how much you want to rotate the camera sideways while wallrunning")]
public float wallRunRotateAmount = 10f;
[Tooltip("a bool to check if the player is wallrunning because thats kinda necessary.")]
public bool isWallRunning;
[Tooltip("a bool to determine whether or not to actually allow wallrunning.")]
public bool useWallrunning = true;
[Header("Hover Ability")]
[Tooltip("Determines whether hovering is enabled")]
public bool hoverAbility;
public float hoverTimer = 1f;
float gravityStrength = 0.9f;
float decayRate = 0.5f;
[Header("Collisions")]
[Tooltip("is the player on the ground.")]
public bool grounded;
[Tooltip("is the player currently crouching.")]
public bool crouching;
private bool surfing;
[Tooltip("is the player currently holding tab.")]
private bool cancellingGrounded;
private bool cancellingSurf;
private bool cancellingWall;
private bool cancelling;
public static PlayerMovementImproved Instance { get; private set; }
public bool movementStateLocked = false;
void Awake() {
Instance = this;
rb = GetComponent<Rigidbody>();
//Create a physic material with no friction to allow for wallrunning and smooth movement not being dependant
//and smooth movement not being dependant on the in-built unity physics engine, apart from collisions.
PhysicsMaterial mat = new PhysicsMaterial("tempNoFrictMat");
mat.bounceCombine = PhysicsMaterialCombine.Average;
mat.bounciness = 0;
mat.frictionCombine = PhysicsMaterialCombine.Minimum;
mat.staticFriction = 0;
mat.dynamicFriction = 0;
gameObject.GetComponent<Collider>().material = mat;
}
public Vector3 startingPosition;
void Start() {
startingPosition = transform.position;
transform.position = startingPosition;
playerScale = transform.localScale;
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
readyToJump = true;
wallNormalVector = Vector3.up;
}
private void FixedUpdate() {
Movement();
//Hover();
Sway();
SwayRotation();
BobOffset();
BobRotation();
GunSway();
}
private void Update() {
if (!movementStateLocked){
Busy();
Upgrade();
MyInput();
Look();
} else {
NoInput();
}
}
private void LateUpdate() {
//call the wallrunning Function
WallRunning();
WallRunRotate();
}
private void Busy() {
if (upgrading || buying) {
busy = true;
if (crouching) {
StopCrouch();
}
Time.timeScale = 0.001f;
} else {
busy = false;
Time.timeScale = 1;
}
}
private void Upgrade() {
if (escaped) {
//Close Shop if open
if (gameObject.GetComponent<InteractScript>().isShopOpen == true) {
gameObject.GetComponent<InteractScript>().CloseShop();
} else {
if (canvas.GetComponent<PauseMenuScript>().menuDepth > 0) Debug.Log("Player is still in menu");
if (!upgrading) {
upgrading = true;
canvas.GetComponent<PauseMenuScript>().ShowScreen();
} else if (upgrading) {
if (canvas.GetComponent<PauseMenuScript>().menuDepth == 0) upgrading = false;
canvas.GetComponent<PauseMenuScript>().HideScreen();
}
}
}
}
private void WallRunRotate() {
FindWallRunRotation();
float num = 12f;
actualWallRotation = Mathf.SmoothDamp(actualWallRotation, wallRunRotation, ref wallRotationVel, num * Time.deltaTime);
playerCam.localRotation = Quaternion.Euler(playerCam.rotation.eulerAngles.x, playerCam.rotation.eulerAngles.y, actualWallRotation);
}
/// Find user input. Should put this in its own class but im lazy
private void MyInput() {
x = Input.GetAxisRaw("Horizontal");
y = Input.GetAxisRaw("Vertical");
walkInput.x = x;
walkInput.y = y;
walkInput = walkInput.normalized;
jumping = Input.GetButton("Jump");
//upgrading = Input.GetKey(KeyCode.Tab);
escaped = Input.GetKeyDown(KeyCode.Escape) || Input.GetKeyDown(KeyCode.Tab);
aiming = Input.GetButton("Fire2");
//Crouching
if (!busy) {
crouching = Input.GetButton("Crouch");
if (Input.GetButtonDown("Crouch")) StartCrouch();
if (Input.GetButtonUp("Crouch")) StopCrouch();
}
}
private bool gameOver = false;
private void NoInput() {
if (gameOver) {
Destroy(rb);
Destroy(gameObject.GetComponent<PlayerMovementImproved>());
}
x = 0;
y = 0;
walkInput.x = x;
walkInput.y = y;
walkInput = walkInput.normalized;
jumping = false;
if (crouching) StopCrouch();
crouching = false;
//upgrading = Input.GetKey(KeyCode.Tab);
escaped = false;
aiming = false;
rb.linearVelocity = Vector3.zero;
}
private void StartCrouch() {
transform.localScale = crouchScale;
transform.position = new Vector3(transform.position.x, transform.position.y - 0.1f, transform.position.z);
if (rb.linearVelocity.magnitude > 0.2f && grounded) {
if (grounded) {
if (x > 0.5f || y > 0.5f) {
rb.AddForce(orientation.transform.forward * slideForce);
}
}
}
}
private void StopCrouch() {
crouching = false;
transform.localScale = playerScale;
transform.position = new Vector3(transform.position.x, transform.position.y + 0.1f, transform.position.z);
}
private void Movement() {
//Extra gravity
rb.AddForce(Vector3.down * Time.deltaTime * 10);
//Find actual velocity relative to where player is looking
Vector2 mag = FindVelRelativeToLook();
float xMag = mag.x, yMag = mag.y;
//Counteract sliding and sloppy movement
CounterMovement(x, y, mag);
if (!busy) {
//If holding jump && ready to jump, then jump
if (readyToJump && jumping) Jump();
//Set max speed
float maxSpeed = this.maxSpeed;
if (grounded) ResetHover();
//If sliding down a ramp, add force down so player stays grounded and also builds speed
if (crouching && grounded && readyToJump && !jumping) {
rb.AddForce(Vector3.down * Time.deltaTime * 3000);
return;
}
//If speed is larger than maxspeed, cancel out the input so you don't go over max speed
if (x > 0 && xMag > maxSpeed) x = 0;
if (x < 0 && xMag < -maxSpeed) x = 0;
if (y > 0 && yMag > maxSpeed) y = 0;
if (y < 0 && yMag < -maxSpeed) y = 0;
//Some multipliers
float multiplier = 1f, multiplierV = 1f;
// Movement in air
if (!grounded) {
multiplier = 0.5f;
multiplierV = 0.5f;
}
if (grounded && crouching) {
multiplierV = 0f;
}
if (isWallRunning) {
multiplier = 0.3f;
multiplierV = 0.3f;
}
if (surfing) {
multiplier = 0.7f;
multiplierV = 0.3f;
}
if (!jumpReset) {
if (grounded && readyToJump || isWallRunning && readyToJump) {
jumpReset = true;
jumpCache = jumpCacheMax;
}
}
//Apply forces to move player
rb.AddForce(orientation.transform.forward * y * moveSpeed * Time.deltaTime * multiplier * multiplierV);
rb.AddForce(orientation.transform.right * x * moveSpeed * Time.deltaTime * multiplier);
}
}
/*
public void Hover() {
if (!grounded && aiming) {
hovering = true;
Vector3 hoverVelocity = rb.velocity;
hoverVelocity.x = 0;
hoverVelocity.y = -hoverVelocity.y;
hoverVelocity.z = 0;
rb.AddForce(hoverVelocity);
}
hovering = false;
}
*/
private void Jump() {
if (jumpCache <= 0) {
return;
} else if (jumpCache >= 1 ) {
if (/*(grounded || isWallRunning || surfing) && */readyToJump) {
//Debug.Log("jumping");
readyToJump = false;
jumpCache--;
Vector3 velocity = rb.linearVelocity;
rb.AddForce(Vector2.up * jumpForce * 1.5f);
rb.AddForce(normalVector * jumpForce * 0.5f);
if (rb.linearVelocity.y < 0.5f) {
rb.linearVelocity = new Vector3(velocity.x, 0f, velocity.z);
}
else if (rb.linearVelocity.y > 0f) {
rb.linearVelocity = new Vector3(velocity.x, velocity.y / 2f, velocity.z);
}
if (isWallRunning) {
rb.AddForce(wallNormalVector * jumpForce * 3f);
}
Invoke("ResetJump", jumpCooldown);
if (isWallRunning) {
isWallRunning = false;
}
}
}
}
private void ResetJump() {
readyToJump = true;
jumpReset = false;
}
private float desiredX;
private void Look() {
if (!busy) {
//Toggle mouse usability
Cursor.visible = false;
Cursor.lockState = CursorLockMode.Locked;
//Track mouse movements
float mouseX = Input.GetAxisRaw("Mouse X") * sensitivity * Time.fixedDeltaTime * sensMultiplier;
float mouseY = Input.GetAxisRaw("Mouse Y") * sensitivity * Time.fixedDeltaTime * sensMultiplier;
lookInput.x = mouseX;
lookInput.y = mouseY;
//Find current look rotation
Vector3 rot = playerCam.transform.localRotation.eulerAngles;
desiredX = rot.y + mouseX;
//Rotate, and also make sure we dont over- or under-rotate.
xRotation -= mouseY;
float clamp = 89.5f;
xRotation = Mathf.Clamp(xRotation, -clamp, clamp);
//Perform the rotations
playerCam.transform.localRotation = Quaternion.Euler(xRotation, desiredX, 0);
orientation.transform.localRotation = Quaternion.Euler(0, desiredX, 0);
if (aiming == true) {
swayEnabled = true;
swayRotEnabled = false;
bobEnabled = false;
bobRotEnabled = false;
gun.GetComponent<Gun>().ADSIn();
if (hoverAbility) Hover();
} else {
swayEnabled = true;
swayRotEnabled = true;
bobEnabled = true;
bobRotEnabled = true;
gun.GetComponent<Gun>().ADSOut();
}
} else {
//Toggle mouse usability
Cursor.visible = true;
Cursor.lockState = CursorLockMode.None;
swayEnabled = false;
swayRotEnabled = false;
bobEnabled = false;
bobRotEnabled = false;
if (aiming == true) {
gun.GetComponent<Gun>().ADSOut();
}
}
}
Vector3 gravity;
void Hover() {
if (hoverTimer <= 0) return;
gravity = rb.linearVelocity;
gravity.x = 0;
gravity.z = 0;
//Debug.Log(gravity.y);
if (gravity.y <= 0) {
gravity.y = -gravity.y * (gravityStrength * hoverTimer);
//Debug.Log(gravity.y);
//Extra gravity
rb.AddForce(gravity * Time.deltaTime * 2250);
hoverTimer -= Time.deltaTime * (Time.timeScale * decayRate);
}
canvas.GetComponent<PauseMenuScript>().UpdateHoverUI(hoverTimer);
}
void ResetHover() {
hoverTimer = 1f;
canvas.GetComponent<PauseMenuScript>().UpdateHoverUI(hoverTimer);
}
void Sway() {
if (swayEnabled) {
Vector3 invertLook = lookInput * -step;
invertLook.x = Mathf.Clamp(invertLook.x, -maxStepDistance, maxStepDistance);
invertLook.y = Mathf.Clamp(invertLook.y, -maxStepDistance, maxStepDistance);
swayPos = invertLook;
} else {
swayPos = new Vector3(0,0,0);
}
}
void SwayRotation() {
if (swayRotEnabled) {
Vector2 invertLook = lookInput * -rotationStep;
invertLook.x = Mathf.Clamp(invertLook.x, -maxRotationStep, maxRotationStep);
invertLook.y = Mathf.Clamp(invertLook.y, -maxRotationStep, maxRotationStep);
swayEulerRot = new Vector3(invertLook.y, invertLook.x, invertLook.x);
} else {
swayEulerRot= new Vector3(0,0,0);
}
}
void BobOffset() {
if (bobEnabled) {
//Used to generate our sin and cos waves
speedCurve += Time.fixedDeltaTime * (grounded ? rb.linearVelocity.magnitude : 1f) + 0.01f;
bobPosition.x = (curveCos * bobLimit.x * (grounded ? 1 : 0)) - (walkInput.x * travelLimit.x); //(bob) - (input offset)
bobPosition.y = (curveSin * bobLimit.y) - (rb.linearVelocity.y * travelLimit.y); //(bob) - (y velocity offset)
bobPosition.z = -(walkInput.y * travelLimit.z); // - (input offset)
} else {
bobPosition = new Vector3(0,0,0);
}
}
void BobRotation() {
if (bobRotEnabled) {
bobEulerRotation.x = (walkInput != Vector2.zero ? multiplier.x * (Mathf.Sin(2 * speedCurve)) : multiplier.x * (Mathf.Sin(2 * speedCurve) / 2));
bobEulerRotation.y = (walkInput != Vector2.zero ? multiplier.y * curveCos : 0);
bobEulerRotation.z = (walkInput != Vector2.zero ? multiplier.z * curveCos * walkInput.x : 0);
} else {
bobEulerRotation = new Vector3(0,0,0);
}
}
void GunSway() {
//Debug.Log(gun.transform.parent.name);
gun.transform.parent.GetComponent<GunSwayScript>().CompositePositionRotation(swayPos, smooth, bobPosition, swayEulerRot, bobEulerRotation, smoothRot);
}
private void CounterMovement(float x, float y, Vector2 mag) {
if (!grounded || jumping) return;
//Slow down sliding
if (crouching) {
rb.AddForce(moveSpeed * Time.deltaTime * -rb.linearVelocity.normalized * slideCounterMovement);
return;
}
//Counter movement
if (Math.Abs(mag.x) > threshold && Math.Abs(x) < 0.05f || (mag.x < -threshold && x > 0) || (mag.x > threshold && x < 0)) {
rb.AddForce(moveSpeed * orientation.transform.right * Time.deltaTime * -mag.x * counterMovement);
}
if (Math.Abs(mag.y) > threshold && Math.Abs(y) < 0.05f || (mag.y < -threshold && y > 0) || (mag.y > threshold && y < 0)) {
rb.AddForce(moveSpeed * orientation.transform.forward * Time.deltaTime * -mag.y * counterMovement);
}
//Limit diagonal running. This will also cause a full stop if sliding fast and un-crouching, so not optimal.
if (Mathf.Sqrt((Mathf.Pow(rb.linearVelocity.x, 2) + Mathf.Pow(rb.linearVelocity.z, 2))) > maxSpeed) {
float fallspeed = rb.linearVelocity.y;
Vector3 n = rb.linearVelocity.normalized * maxSpeed;
rb.linearVelocity = new Vector3(n.x, fallspeed, n.z);
}
}
/// Find the velocity relative to where the player is looking
/// Useful for vectors calculations regarding movement and limiting movement
public Vector2 FindVelRelativeToLook() {
float lookAngle = orientation.transform.eulerAngles.y;
float moveAngle = Mathf.Atan2(rb.linearVelocity.x, rb.linearVelocity.z) * Mathf.Rad2Deg;
float u = Mathf.DeltaAngle(lookAngle, moveAngle);
float v = 90 - u;
float magnitude = rb.linearVelocity.magnitude;
float yMag = magnitude * Mathf.Cos(u * Mathf.Deg2Rad);
float xMag = magnitude * Mathf.Cos(v * Mathf.Deg2Rad);
return new Vector2(xMag, yMag);
}
//a lot of math (dont touch)
private void FindWallRunRotation() {
if (!isWallRunning) {
wallRunRotation = 0f;
return;
}
_ = new Vector3(0f, playerCam.transform.rotation.y, 0f).normalized;
new Vector3(0f, 0f, 1f);
float num = 0f;
float current = playerCam.transform.rotation.eulerAngles.y;
if (Math.Abs(wallNormalVector.x - 1f) < 0.1f) {
num = 90f;
}
else if (Math.Abs(wallNormalVector.x - -1f) < 0.1f) {
num = 270f;
}
else if (Math.Abs(wallNormalVector.z - 1f) < 0.1f) {
num = 0f;
}
else if (Math.Abs(wallNormalVector.z - -1f) < 0.1f) {
num = 180f;
}
num = Vector3.SignedAngle(new Vector3(0f, 0f, 1f), wallNormalVector, Vector3.up);
float num2 = Mathf.DeltaAngle(current, num);
wallRunRotation = (0f - num2 / 90f) * wallRunRotateAmount;
if (!useWallrunning) {
return;
}
if ((Mathf.Abs(wallRunRotation) < 4f && y > 0f && Math.Abs(x) < 0.1f) || (Mathf.Abs(wallRunRotation) > 22f && y < 0f && Math.Abs(x) < 0.1f)) {
if (!cancelling) {
cancelling = true;
CancelInvoke("CancelWallrun");
Invoke("CancelWallrun", 0.2f);
}
}
else {
cancelling = false;
CancelInvoke("CancelWallrun");
}
}
private bool IsFloor(Vector3 v) {
return Vector3.Angle(Vector3.up, v) < maxSlopeAngle;
}
private bool IsSurf(Vector3 v) {
float num = Vector3.Angle(Vector3.up, v);
if (num < 89f) {
return num > maxSlopeAngle;
}
return false;
}
private bool IsWall(Vector3 v) {
return Math.Abs(90f - Vector3.Angle(Vector3.up, v)) < 0.05f;
}
private bool IsRoof(Vector3 v) {
return v.y == -1f;
}
/// Handle ground detection
private void OnCollisionStay(Collision other) {
int layer = other.gameObject.layer;
if ((int)whatIsGround != ((int)whatIsGround | (1 << layer))) {
return;
}
for (int i = 0; i < other.contactCount; i++) {
Vector3 normal = other.contacts[i].normal;
if (IsFloor(normal)) {
if (isWallRunning) {
isWallRunning = false;
}
grounded = true;
normalVector = normal;
cancellingGrounded = false;
CancelInvoke("StopGrounded");
}
if (IsWall(normal) && (layer == (int)whatIsGround || (int)whatIsGround == -1 || layer == LayerMask.NameToLayer("Ground") || layer == LayerMask.NameToLayer("Ground"))) { //Seriously, what is this
StartWallRun(normal);
cancellingWall = false;
CancelInvoke("StopWall");
}
if (IsSurf(normal)) {
surfing = true;
cancellingSurf = false;
CancelInvoke("StopSurf");
}
IsRoof(normal);
}
float num = 3f;
if (!cancellingGrounded) {
cancellingGrounded = true;
Invoke("StopGrounded", Time.deltaTime * num);
}
if (!cancellingWall) {
cancellingWall = true;
Invoke("StopWall", Time.deltaTime * num);
}
if (!cancellingSurf) {
cancellingSurf = true;
Invoke("StopSurf", Time.deltaTime * num);
}
}
private void StopGrounded() {
grounded = false;
}
private void StopWall() {
isWallRunning = false;
}
private void StopSurf() {
surfing = false;
}
//wallrunning functions
private void CancelWallrun() {
//for when we want to stop wallrunning
//Debug.Log("cancelled wallrun");
//Invoke("GetReadyToWallrun", 0.01f);
rb.AddForce(wallNormalVector * escapeForce);
isWallRunning = false;
}
private void StartWallRun(Vector3 normal) {
//Debug.Log("wallrunning");
//Cancels all y momentum and then applies an upwards force.
if (!grounded && useWallrunning) {
wallNormalVector = normal;
if (!isWallRunning) {
rb.linearVelocity = new Vector3(rb.linearVelocity.x, 0f, rb.linearVelocity.z);
rb.AddForce(Vector3.up * initialForce, ForceMode.Impulse);
}
isWallRunning = true;
}
}
private void WallRunning() {
//checks if the wallrunning bool is set to true and if it is then applies
//a force to counter gravity enough to make it feel like wallrunning
if (isWallRunning) {
rb.AddForce(-wallNormalVector * Time.deltaTime * moveSpeed);
rb.AddForce(Vector3.up * Time.deltaTime * rb.mass * wallRunGravity * -Physics.gravity.y * 0.4f);
}
}
}
💡 What Makes This Special
Feature-rich but cohesive
I packed several common FPS movement features into one unified component—walking, crouch/slide, jump caching, wallrunning, surfing detection, aiming state, hover, and procedural gun sway/bob. Rather than scattering logic across many scripts, I kept movement, camera look, and weapon-sway orchestration in one place so the interplay between movement and camera/gun systems is explicit and easy to reason about.
Physics-driven, responsive movement
I intentionally used Rigidbody forces and velocity control (instead of purely kinematic transforms) so movement interacts naturally with Unity’s physics — collisions, slopes, wall normals, and momentum feel organic. The code applies additive forces scaled by Time.deltaTime, uses counter-movement to tame unwanted momentum, and clamps diagonal speed — all of which produce controlled, responsive movement while preserving physics realism.