FPS Physics-based Controller

About

A first person physics based character controller created for prototyping games & mechanics. Built from the ground up with the SOLID pattern guiding its archietecture. The controller allows for easily adding and removing features based on your games needs.

Project Details

Team Size: 1

Year: 2023

Project Form: Prototyping

Engine: Unity

Notable Work

Overview

The asset contains several independent features which can be disabled or enabled at any given time based on the projects needs. The controller is designed to be easily expandable with a decoupled SOLID approach. Some of the base features include:

  • Movement
  • Wall-running
  • Grappling Hook
  • Attatching/Connecting objects
  • Pick-up and inspect objects
  • Weapon/Hand Sway

Dependencies

The project is broken into several independent classes that function together. Utilizing the SOLID principle, the project allows for a decoupled approach thats easily tweakable and expandable. The system allows for quick enbaling & disabling of features from a centralized location, making it easy to pick and choose what features are needed for the prototype.


Name Description
Movement A physics based feature that controls player movement on the ground, in the air & on slopes.
Jumping A feature which handles player jumping, with attention to game feel. Including additional features such as coyote time, jump buffering, jump correction and jump height based on input.
Wall Running A feature for handling horizontal movement across walls, for games with parkour elements.
Attatching A feature for combining or attatching objects to different parts of the world. Utilizing spring joints. The feature was inspired by The Legend of Zelda: Tears of the Kingdom's fuse.
Grappling Hook A physics based feature that allows the player to use a hook to get around, utilizing spring joints & line renderer's.
Inspecting A feature to view an object at different angles and perspectives, while keeping its original transform in tact.
Grab & Throw A feature to physically pick up, move or throw objects.
Sway A feature to simulate sway for whatever the character is holding. The sway intensity can be adjusted per object if needed.
Head-tilt A feature to simulate head-tilt based on the players perspective to add immersive game juice.

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

Grappling Hook

The grappling-hook feature empowers players to swing through the environment using Unity's SpringJoints.

The feature's implementation also facilitated intriguing experiments with an attachment mechanic. This mechanic ingeniously fuses two objects using SpringJoints, reminiscent of The Legend of Zelda: Tears of the Kingdom, albeit with a playful "ragdoll" twist.


using System.Collections.Generic;
using System.Collections;
using UnityEngine;

namespace FreyPhysicsController
{
	public class GrapplingHook : MonoBehaviour
	{
		[Header("Dependencies")]
		[SerializeField] private Dependencies dependencies;
		
		[Header("Hook Properties")]
		[SerializeField] private GameObject hookModel;
		[SerializeField] private float holdDelayToSwing = 0.2f;
		[SerializeField] private float playerRetractStrength = 1000f;
		[SerializeField] private float retractStrength = 500f; 
		[SerializeField] private float latchOnImpulse = 200f;
		
		[Header("Rope Properties")]
		[SerializeField] private Material ropeMaterial;
		[SerializeField] private float startThickness = 0.02f;
		[SerializeField] private float endThickness = 0.06f;

		[Header("Rope Visual Spring Properties")]
		[SerializeField] private int segments = 50;
		[SerializeField] private float damper = 12;
		[SerializeField] private float springStrength = 800;
		[SerializeField] private float speed = 12;
		[SerializeField] private float waveCount = 5;
		[SerializeField] private float waveHeight = 4;
		[SerializeField] private AnimationCurve affectCurve;

		[HideInInspector]
		[SerializeField] private List hooks, hookModels, hookLatches, ropeColliders;
		[HideInInspector]
		[SerializeField] private List ropes;

		private float mouseDownTimer = 0;

		private bool executeHookSwing = false;
		private bool hookRelease = false;
		private bool hooked = false;
		private bool isOptimizing = false;

		private Rigidbody player;
		private Transform spawnPoint;
		private Spring spring;

		private Ray ray;
		private RaycastHit hit;

		private void Start()
		{
			Initialize();
			CreateSpring();
		}

		private void Update()
		{
			InputCheck();
			CreateHooks();
			RetractHooks();
			CutRopes();
		}

		private void LateUpdate()
		{
			DrawRopes();
		}
		
		private void Initialize()
		{
			player = dependencies.rb;
			spawnPoint = dependencies.spawnPoint;
		}

		private void CreateSpring()
		{
			spring = new Spring();
			spring.SetTarget(0);
		}

		private void InputCheck()
		{
				//Reset checker
				if(Input.GetMouseButtonDown(1) && !Input.GetKey(KeyCode.LeftControl) && !dependencies.isInspecting)
				{
					mouseDownTimer = 0;
					hookRelease = false;
					executeHookSwing = false;
				}

				//Check input for hook to swing
				if(Input.GetMouseButton(1) && !Input.GetKey(KeyCode.LeftControl) && !dependencies.isInspecting)
				{
					mouseDownTimer += Time.deltaTime;

					if(hooked && mouseDownTimer >= holdDelayToSwing && !executeHookSwing)
					{
						executeHookSwing = true;
					}
				}

				//Check input for hook to latch
				if(Input.GetMouseButtonUp(1) && !Input.GetKey(KeyCode.LeftControl) && mouseDownTimer >= holdDelayToSwing && executeHookSwing && !dependencies.isInspecting)
				{
					executeHookSwing = false;
					hookRelease = true;
				}
		}


		//Create Hooks
		private void CreateHooks()
		{
			ray = Camera.main.ScreenPointToRay(Input.mousePosition);

			if (Input.GetMouseButtonDown(1) && !Input.GetKey(KeyCode.LeftControl) && !dependencies.isInspecting)
			{
				if (Physics.Raycast(ray.origin, ray.direction, out hit, Mathf.Infinity, ~(1 << LayerMask.NameToLayer("Ignore Raycast")), QueryTriggerInteraction.Ignore))
				{
					if (hit.transform.gameObject.GetComponent() == null)
					{
						hit.transform.gameObject.AddComponent().isKinematic = true;
					}
				}

				if (!hooked)
				{
					CreateFirstHook();
				}
				else if (hooked)
				{
					CreateHookLatch();
				}
			}
		}

		private void CreateFirstHook()
		{
			if (Physics.Raycast(ray.origin, ray.direction, out hit, Mathf.Infinity, ~(1 << LayerMask.NameToLayer("Ignore Raycast")), QueryTriggerInteraction.Ignore))
			{
				if (!hit.collider.isTrigger && hit.collider.gameObject.GetComponent() != player)
				{
					hooked = true;

					// Create hook and related objects
					CreateHookObject(hit.point);

					// Set hook rope values
					SetupHookRope();

					// Set joint parameters and player's spring joint
					SetupPlayerSpringJoint();
				}
			}
		}

		private void CreateHookLatch()
		{
			if (Physics.Raycast(ray.origin, ray.direction, out hit, Mathf.Infinity, ~(1 << LayerMask.NameToLayer("Ignore Raycast")), QueryTriggerInteraction.Ignore))
			{
				if (!hit.collider.isTrigger && hit.collider.gameObject.GetComponent() != player)
				{
					// Create hook latch and related objects
					CreateHookLatchObject(hit.point);

					// Remove hook start point model and add hook latch point model
					ModifyHookModels();

					// Update joint parameters and spring joint
					UpdateHookSpringJoint();

					// Enable rope collider and perform optimization
					OptimizeRopeCollider();

					// Reset hooked state and retract
					hooked = false;
					Retract();
				}
			}
		}

		//Retract hooked objects
		private void RetractHooks()
		{
			//Set player hook swing strength
			if(executeHookSwing &&player.GetComponent() && player.GetComponent().spring != playerRetractStrength)
			{
				player.GetComponent().spring = playerRetractStrength;
			}

			//Set player hook retract strength
			if(Input.GetMouseButtonDown(2) && !dependencies.isInspecting)
			{
				Retract();
			}
		}

		private void Retract()
		{
			if (player.GetComponent() != null)
			{
				player.GetComponent().spring = playerRetractStrength;
			}

			//Set all other hook and latched retract strengths
			foreach (GameObject hookJoints in hooks)
			{
				if (hookJoints.GetComponent() && hookJoints.GetComponent().connectedBody != player)
				{
					hookJoints.GetComponent().spring = retractStrength;
				}
			}
		}

		private void CutRopes()
		{
			//Destroy player hooks upon hold release
			if(hookRelease && hooked)
			{
				hookRelease = false;

				if(hooks.Count > 0)
				{
					Destroy(player.GetComponent());
					Destroy(hooks[hooks.Count - 1].gameObject);
					hooks.RemoveAt(hooks.Count - 1);
				}

				if(ropeColliders.Count > 0)
				{
					Destroy(ropeColliders[ropeColliders.Count - 1].gameObject);
					ropeColliders.RemoveAt(ropeColliders.Count - 1);
				}

				if(hookModels.Count > 0)
				{
					Destroy(hookModels[hookModels.Count - 1].gameObject);
					hookModels.RemoveAt(hookModels.Count - 1);
				}

				if(ropes.Count > 0)
				{
					ropes.RemoveAt(ropes.Count - 1);
				}
						
				hooked = false;
				hookRelease = false;
			}

			if(Input.GetMouseButton(1) && Input.GetKey(KeyCode.LeftControl) && !dependencies.isInspecting)
			{
				//If attached to player
				if(hooked)
				{
					if(hooks.Count > 0)
					{
						Destroy(player.GetComponent());
						Destroy(hooks[hooks.Count - 1].gameObject);
						hooks.RemoveAt(hooks.Count - 1);
					}

					if(ropeColliders.Count > 0)
					{
						Destroy(ropeColliders[ropeColliders.Count - 1].gameObject);
						ropeColliders.RemoveAt(ropeColliders.Count - 1);
					}

					if(hookModels.Count > 0)
					{
						Destroy(hookModels[hookModels.Count - 1].gameObject);
						hookModels.RemoveAt(hookModels.Count - 1);
					}

					if(ropes.Count > 0)
					{
						ropes.RemoveAt(ropes.Count - 1);
					}
						
					hooked = false;
				}
				else if(!hooked && Physics.Raycast(ray.origin, ray.direction, out hit, Mathf.Infinity, ~(1 << LayerMask.NameToLayer("Ignore Raycast")))) 
				{
					if(hit.collider.isTrigger)
					{
						int index = GameObjectToIndex(hit.collider.gameObject);
						
						Destroy(hooks[index].gameObject);
						Destroy(hookLatches[index].gameObject);
						Destroy(ropeColliders[index].gameObject);

						hooks.RemoveAt(index);
						hookLatches.RemoveAt(index);
						ropes.RemoveAt(index);
						ropeColliders.RemoveAt(index);

						if(hooks.Count == 0)
						{
							hooked = false;
						}
					}

					//Clean up the hook model list if missing after the models get destroyed
					for(var i = hookModels.Count - 1; i >= 0; i--)
					{
						if(hookModels[i] == null)
						{
							hookModels.RemoveAt(i);
						}
					}
				}
			}

			//Destroy everything created and clear all lists
			if(Input.GetKeyDown("r") && !dependencies.isInspecting)
			{
				hooked = false;

				//Destroy joints and objects
				if(hooks.Count > 0)
				{
					if(player.GetComponent())
					{
						Destroy(player.GetComponent());
					}

					foreach(GameObject hookObjects in hooks)
					{
						Destroy(hookObjects);
					}
						
					foreach(GameObject hookLatchObjects in hookLatches)
					{
							Destroy(hookLatchObjects);
					}

					foreach(GameObject ropeColliderObjects in ropeColliders)
					{
							Destroy(ropeColliderObjects);
					}

					foreach(GameObject hookModelObjects in hookModels)
					{
						Destroy(hookModelObjects);
					}
					
					//Clear all lists
					hooks.Clear();
					hookModels.Clear();
					hookLatches.Clear();
					ropes.Clear();
					ropeColliders.Clear();
				}
			}
		}


		//Draw ropes
		private void DrawRopes()
		{
			if (ropes.Count != 0 && ropes.Count == hooks.Count)
			{
				for (int i = 0; i < ropes.Count; i++)
				{
					if (player.GetComponent() != null && player.GetComponent().connectedBody == hooks[i].GetComponent())
					{
						HandlePlayerGrapple(i);
					}
					else if (hooks[i].GetComponent() != null && hooks[i].GetComponent().connectedBody != player && ropes[i].positionCount > 2)
					{
						HandleNonPlayerGrapple(i);
					}
					else if (hooks[i].GetComponent() != null && hooks[i].GetComponent().connectedBody != player && ropes[i].positionCount == 2)
					{
						ropes[i].SetPosition(0, hooks[i].transform.position);
						ropes[i].SetPosition(1, hookLatches[i].transform.position);
					}

					UpdateRopeCollider(i);
				}
			}
		}

		private void HandlePlayerGrapple(int i)
		{
			// Transition spring properties
			spring.SetDamper(damper);
			spring.SetStrength(springStrength);
			spring.Update(Time.deltaTime);

			// ... Handle spring and rope visuals ...
		}

		private void HandleNonPlayerGrapple(int i)
		{
			// Transition spring properties
			spring.SetDamper(damper);
			spring.SetStrength(springStrength);
			spring.Update(Time.deltaTime);

			// ... Handle spring and rope visuals ...

			if (isOptimizing)
			{
				StartCoroutine(DelayOptimization(i));
			}
		}

		private IEnumerator DelayOptimization(int i)
		{
			yield return new WaitForSeconds(1);

			if (ropes.Count > 1 && i != ropes.Count)
			{
				ropes[ropes.Count - 2].positionCount = 2;
			}

			isOptimizing = false;
		}

		private void UpdateRopeCollider(int i)
		{
			if (ropeColliders.Count > 0 && hooks[i].GetComponent() != null)
			{
				// ... Update rope collider position and size ...
			}
		}


		//Rope collider Index checker for cutting
		private int GameObjectToIndex(GameObject ropeColliderList)
		{
			for (int i = 0; i < ropeColliders.Count; i++)
			{
				//Check if A rope collider is in the List
				if (ropeColliders[i] == ropeColliderList)
				{
					//Return the current index
					return i;
				}
			}
			//Return if nothing
			return -1;
		}
	}
}
									

Inspect vs Grab

An invaluable addition is the ability to pick up and examine items from all angles, granting players adjustable control. Notably, the object maintains its initial transform properties throughout. This feature proves particularly advantageous for game genres like horror, puzzle-solving, and adventure, where immersive interaction with objects enriches the overall experience.

In a parallel vein, the grab feature was expertly integrated, enabling players to tangibly lift and manipulate objects, while factoring in their physical attributes such as mass. This innovation amplifies the level of realism and engagement within the interactive environment.

Inspect
Grab

using System.Collections.Generic;
using UnityEngine;
using FreyPhysicsController;

namespace FreyPhysicsController
{
	public class Inspect : MonoBehaviour
	{
		[Header("Dependencies")]
		[SerializeField] private Dependencies dependencies;

		[Header("Input Properties")]
		[SerializeField] private KeyCode addToListKey = KeyCode.I;
		[SerializeField] private KeyCode inspectKey = KeyCode.E;

		[Header("Inspection Properties")]
		[SerializeField] private GameObject inspectIcon;
		[SerializeField] private GameObject aimDot;
		[SerializeField] private float maxPickupDistance = 6;
		[SerializeField] private float pickupSpeed = 5f;
		[SerializeField] private float rotateSpeed = 2f;
		[SerializeField] private float zoomSpeed = 0.2f;

		[Header("Inspect & Disable Lists")]
		[SerializeField] private List objectsToInspect;
		[SerializeField] private List objectsToIgnore;

		private float rotX, rotY;
		
		private Camera cam;
		private Transform inspectPoint;
		private GameObject inspectedObject;

		private Vector3 objectOrigin;
		private Vector3 originalDistance;

		private Quaternion objectRotation;

		private Ray ray;
		private RaycastHit hit;

		private void Start()
		{
			Initialize();
		}

		private void Update()
		{
			Inspection();
		}

		void Initialize()
		{
			cam = dependencies.cam;
			inspectPoint = dependencies.inspectPoint;
		}

		private void Inspection()
		{
			ray = Camera.main.ScreenPointToRay(Input.mousePosition);

			if (dependencies.isInspecting)
			{
				HandleInspectionExit();
			}
			else if (!dependencies.isInspecting && !dependencies.isGrabbing)
			{
				HandleInspectionEntry();
			}

			if (dependencies.isGrabbing)
			{
				inspectIcon.SetActive(false);
			}

			if (dependencies.isInspecting && inspectedObject != null)
			{
				HandleInspectionInProgress();
			}
			else if (!dependencies.isInspecting && inspectedObject != null)
			{
				HandleInspectionExitCleanup();
			}
		}

		private void HandleInspectionEntry()
		{
			if (Physics.Raycast(ray.origin, ray.direction, out hit, maxPickupDistance, ~(1 << LayerMask.NameToLayer("Ignore Raycast")), QueryTriggerInteraction.Ignore))
			{
				if (objectsToInspect.Contains(hit.collider))
				{
					aimDot.SetActive(false);
					inspectIcon.SetActive(true);
				}
				else
				{
					inspectIcon.SetActive(false);
					aimDot.SetActive(true);
				}

				if (!objectsToIgnore.Contains(hit.collider))
				{
					if (Input.GetKeyDown(addToListKey))
					{
						ToggleObjectInInspectionList(hit.collider);
					}
					else if (Input.GetKeyDown(inspectKey))
					{
						InitiateInspection(hit.collider);
					}
				}
			}
			else
			{
				if (inspectIcon.activeSelf || !aimDot.activeSelf)
				{
					inspectIcon.SetActive(false);
					aimDot.SetActive(true);
				}
			}
		}

		private void HandleInspectionInProgress()
		{
			UpdateObjectPositionAndRotation();
			UpdateInspectionDistance();
		}

		private void HandleInspectionExit()
		{
			if (Input.GetKeyDown(inspectKey))
			{
				FinalizeInspectionExit();
			}
		}

		private void HandleInspectionExitCleanup()
		{
			ResetObjectToOriginalState();
			ResetInspectionRotation();
			ResetInspectionDistance();
			ResetInspectedObject();
		}
	}
}
											

								
										

using UnityEngine;
using FreyPhysicsController;

namespace FreyPhysicsController
{
	public class GrabThrow : MonoBehaviour
	{
		[Header("Dependencies")]
		[SerializeField] private Dependencies dependencies;

		[Header("Input Properties")]
		[SerializeField] private KeyCode grabThrowKey = KeyCode.G;

		[Header("Grab/Throw Properties")]
		[SerializeField] private float maxGrabDistance = 8f;
		[SerializeField] private float grabSpeed = 15;
		[SerializeField] private float throwForce = 800f;
		[SerializeField] private GameObject grabIcon;

		[Header("Audio Properties")]
		[SerializeField] private AudioClip grabSound;
		[SerializeField] private AudioClip throwSound;

		private Camera cam;

		private Rigidbody rb;
		private Rigidbody grabbedObject;

		private Transform grabPoint;
		private Transform originalParent;

		private Ray ray;
		private RaycastHit hit;

		private void Start()
		{
			Initialize();
		}

		private void Update()
		{
			GrabHoldThrow();
		}

		private void FixedUpdate()
		{
			Hold();
		}

		private void Initialize()
		{
			cam = dependencies.cam;
			rb = dependencies.rb;
			grabPoint = dependencies.grabPoint;
			audioSource = dependencies.audioSourceTop;

			//Create and make grab point A kinematic rigidbody
			grabPoint.gameObject.AddComponent().useGravity = false;
			grabPoint.gameObject.GetComponent().isKinematic = true;
		}


		private void GrabHoldThrow()
		{
			ray = Camera.main.ScreenPointToRay(Input.mousePosition);

			if (dependencies.isGrabbing && grabbedObject != null)
			{
				HandleThrow();
			}
			else if (!dependencies.isGrabbing && !dependencies.isInspecting)
			{
				HandleGrab();
			}
		}

		private void HandleThrow()
		{
			if (Input.GetKeyDown(grabThrowKey))
			{
				grabbedObject.AddForce(dependencies.cam.transform.forward * throwForce, ForceMode.Impulse);
				ReleaseGrab();
				audioSource.PlayOneShot(throwSound);
			}
		}

		private void HandleGrab()
		{
			if (Input.GetKeyDown(grabThrowKey) && Physics.Raycast(ray.origin, ray.direction, out hit, maxGrabDistance, ~(1 << LayerMask.NameToLayer("Ignore Raycast")), QueryTriggerInteraction.Ignore))
			{
				Rigidbody hitRigidbody = hit.collider.gameObject.GetComponent();
				
				if (hitRigidbody != null && !hitRigidbody.isKinematic)
				{
					grabPoint.position = hit.point;
					grabbedObject = hitRigidbody;
					dependencies.isGrabbing = true;
					grabIcon.SetActive(true);
					audioSource.PlayOneShot(grabSound);
				}
			}
		}

		private void ReleaseGrab()
		{
			grabbedObject = null;
			dependencies.isGrabbing = false;
			grabIcon.SetActive(false);
		}

		private void Hold()
		{
			if (dependencies.isGrabbing && grabbedObject != null)
			{
				grabbedObject.velocity = grabSpeed * (grabPoint.position - grabbedObject.transform.position);
			}
		}
	}
}