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