Player/ThirdPersonController.cs
using Sandbox;
using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;

public sealed class ThirdPersonController : Component
{

	[Property, Category( "Movement" )] private float speed = 240f;
	[Property, Category( "Movement" )] public bool shouldStrafe = false;
	[Property, Category( "Movement" )] private float gravity = 980f; // Unreal-style units, can tweak

	[Property, Category( "Jump" )] public float GravityRayLength = 100f;
	[Property, Category( "Jump" )] public float groundOffset = 0.1f;
	[Property, Category( "Jump" )] private float jumpForce = 300f;

	[Property, Category( "Smoothers" )] private float rotationSpeed = 10f;

	[InfoBox( "Note: you must name your camera 'FollowCamera'" )]
	[Property, Category( "References" )] private GameObject camera;



	private float verticalVelocity = 0f; // Tracks upward/downward velocity
	private bool isGrounded = true; // Simple grounded check

	protected override void OnAwake()
	{
		//initial camera position.
		camera = Scene.Directory.FindByName( "FollowCamera" ).First(); //get the camera from the scene by name
		if ( camera != null )
		{
			camera.LocalPosition = new Vector3( 0, 0, 64 ); //position the camera above the player
		}

	}
	protected override void OnUpdate()
	{
		HandleMovement();
		HandleGravityAndJump();
	}

	private void HandleMovement()
	{
		if ( camera == null )
			return;


		Vector3 input = Input.AnalogMove;
		if ( input.LengthSquared > 0 )
			input = input.Normal;

		// Get camera forward/right vectors and ignore the roll (z) so we don't have weird camera rotation behaviour.
		Vector3 camForward = camera.WorldRotation.Forward.WithZ( 0 ).Normal;
		Vector3 camRight = camera.WorldRotation.Right.WithZ( 0 ).Normal;


		Vector3 moveDir = (camForward * -input.x) + (camRight * input.y);


		WorldPosition += moveDir * -speed * Time.Delta;

		//Face the direction you're moving
		if ( moveDir.LengthSquared > 0.001f && shouldStrafe == false )
		{

			Rotation targetRot = Rotation.LookAt( moveDir, Vector3.Up ); //unsmoothed
			WorldRotation = Rotation.Slerp( WorldRotation, targetRot, Time.Delta * rotationSpeed ); //smoothed
		}
	}

	private void HandleGravityAndJump()
	{
		// Raycast down
		Vector3 rayStart = WorldPosition;
		Vector3 rayEnd = WorldPosition + Vector3.Down * GravityRayLength;

		var tr = Scene.Trace.Ray( rayStart, rayEnd )
							.IgnoreGameObject( GameObject )
							.Run();

		bool hitGround = tr.Hit;
		float groundZ = hitGround ? tr.EndPosition.z + groundOffset : float.MinValue;
		float distanceToGround = hitGround ? WorldPosition.z - groundZ : float.MaxValue;

		// Check if grounded
		if ( hitGround && distanceToGround <= 0.1f )
		{
			// Snap to ground + offset
			WorldPosition = new Vector3( WorldPosition.x, WorldPosition.y, groundZ );
			verticalVelocity = 0f;
			isGrounded = true;

			// Jump input
			if ( Input.Pressed( "Jump" ) )
			{
				verticalVelocity = jumpForce;
				isGrounded = false;

				// Immediately move the player up to avoid gravity canceling jump
				WorldPosition += new Vector3( 0, 0, verticalVelocity * Time.Delta );
				return; // skip the gravity application below
			}
		}
		else
		{
			isGrounded = false;
		}

		// Apply gravity if airborne
		if ( !isGrounded )
		{
			verticalVelocity -= gravity * Time.Delta;
			WorldPosition += new Vector3( 0, 0, verticalVelocity * Time.Delta );
		}
	}
}