PlayerController.cs
using Sandbox.Citizen;
using System.Text.Json;
[Group( "Walker" )]
[Title( "Walker - Player Controller" )]
public sealed class PlayerController : Component
{
[Property] public CharacterController CharacterController { get; set; }
[Property] public float CrouchMoveSpeed { get; set; } = 64.0f;
[Property] public float WalkMoveSpeed { get; set; } = 190.0f;
[Property] public float RunMoveSpeed { get; set; } = 190.0f;
[Property] public float SprintMoveSpeed { get; set; } = 320.0f;
[Property] public CitizenAnimationHelper AnimationHelper { get; set; }
[Sync] public bool Crouching { get; set; }
[Sync] public Angles EyeAngles { get; set; }
[Sync] public Vector3 WishVelocity { get; set; }
public bool WishCrouch;
public float EyeHeight = 64;
protected override void OnAwake()
{
if ( !IsProxy ) Tags.Add( "player_local" );
if ( IsProxy ) Tags.Remove( "player_local" );
Log.Info( Tags );
Log.Info( JsonSerializer.Serialize( Tags ) );
}
protected override void OnUpdate()
{
if (!IsProxy)
{
MouseInput();
Transform.Rotation = new Angles( 0, EyeAngles.yaw, 0 );
}
UpdateAnimation();
}
protected override void OnFixedUpdate()
{
if ( IsProxy ) return;
CrouchingInput();
MovementInput();
}
private void MouseInput()
{
var e = EyeAngles;
e += Input.AnalogLook;
e.pitch = e.pitch.Clamp( -90, 90 );
e.roll = 0.0f;
EyeAngles = e;
}
float CurrentMoveSpeed
{
get
{
if ( Crouching ) return CrouchMoveSpeed;
if ( Input.Down( "run" ) ) return SprintMoveSpeed;
if ( Input.Down( "walk" ) ) return WalkMoveSpeed;
return RunMoveSpeed;
}
}
RealTimeSince lastGrounded;
RealTimeSince lastUngrounded;
RealTimeSince lastJump;
float GetFriction()
{
if ( CharacterController.IsOnGround ) return 6.0f;
// air friction
return 0.2f;
}
private void MovementInput()
{
if ( CharacterController is null )
return;
var cc = CharacterController;
Vector3 halfGravity = Scene.PhysicsWorld.Gravity * Time.Delta * 0.5f;
WishVelocity = Input.AnalogMove;
if ( lastGrounded < 0.2f && lastJump > 0.3f && Input.Pressed( "jump" ) )
{
lastJump = 0;
cc.Punch( Vector3.Up * 300 );
}
if ( !WishVelocity.IsNearlyZero() )
{
WishVelocity = new Angles( 0, EyeAngles.yaw, 0 ).ToRotation() * WishVelocity;
WishVelocity = WishVelocity.WithZ( 0 );
WishVelocity = WishVelocity.ClampLength( 1 );
WishVelocity *= CurrentMoveSpeed;
if ( !cc.IsOnGround )
{
WishVelocity = WishVelocity.ClampLength( 50 );
}
}
cc.ApplyFriction( GetFriction() );
if ( cc.IsOnGround )
{
cc.Accelerate( WishVelocity );
cc.Velocity = CharacterController.Velocity.WithZ( 0 );
}
else
{
cc.Velocity += halfGravity;
cc.Accelerate( WishVelocity );
}
//
// Don't walk through other players, let them push you out of the way
//
var pushVelocity = PlayerPusher.GetPushVector( Transform.Position + Vector3.Up * 40.0f, Scene, GameObject );
if ( !pushVelocity.IsNearlyZero() )
{
var travelDot = cc.Velocity.Dot( pushVelocity.Normal );
if ( travelDot < 0 )
{
cc.Velocity -= pushVelocity.Normal * travelDot * 0.6f;
}
cc.Velocity += pushVelocity * 128.0f;
}
cc.Move();
if ( !cc.IsOnGround )
{
cc.Velocity += halfGravity;
}
else
{
cc.Velocity = cc.Velocity.WithZ( 0 );
}
if ( cc.IsOnGround )
{
lastGrounded = 0;
}
else
{
lastUngrounded = 0;
}
}
float DuckHeight = (64 - 36);
bool CanUncrouch()
{
if ( !Crouching ) return true;
if ( lastUngrounded < 0.2f ) return false;
var tr = CharacterController.TraceDirection( Vector3.Up * DuckHeight );
return !tr.Hit; // hit nothing - we can!
}
public void CrouchingInput()
{
WishCrouch = Input.Down( "duck" );
if ( WishCrouch == Crouching )
return;
// crouch
if ( WishCrouch )
{
CharacterController.Height = 36;
Crouching = WishCrouch;
// if we're not on the ground, slide up our bbox so when we crouch
// the bottom shrinks, instead of the top, which will mean we can reach
// places by crouch jumping that we couldn't.
if ( !CharacterController.IsOnGround )
{
CharacterController.MoveTo( Transform.Position += Vector3.Up * DuckHeight, false );
Transform.ClearInterpolation();
EyeHeight -= DuckHeight;
}
return;
}
// uncrouch
if ( !WishCrouch )
{
if ( !CanUncrouch() ) return;
CharacterController.Height = 64;
Crouching = WishCrouch;
return;
}
}
private void UpdateCamera()
{
var camera = Scene.GetAllComponents<CameraComponent>().Where( x => x.IsMainCamera ).FirstOrDefault();
if ( camera is null ) return;
// Determine target eye height based on crouching state
var targetEyeHeight = Crouching ? -10f : 6.4f;
EyeHeight = EyeHeight.LerpTo( targetEyeHeight, RealTime.Delta * 10.0f );
// Get the camera's local position relative to the player's position
var targetLocalCameraPos = new Vector3( 0, 0, EyeHeight ); // Local offset for the camera (only affects z for height)
// Smooth transition when ungrounded (e.g. going up/down stairs or ducking)
if ( lastUngrounded > 0.2f )
{
// Interpolating z-axis (local space) smoothly
targetLocalCameraPos.z = camera.Transform.LocalPosition.z.LerpTo( targetLocalCameraPos.z, RealTime.Delta * 25.0f );
}
// Apply local position and rotation to the camera
camera.Transform.LocalPosition = targetLocalCameraPos;
// Set the camera's local rotation based on player's current view angles
camera.Transform.LocalRotation = new Angles( EyeAngles.pitch, 0, EyeAngles.roll );
// Set the camera's field of view from player preferences
camera.FieldOfView = Preferences.FieldOfView;
}
protected override void OnPreRender()
{
UpdateBodyVisibility();
if ( IsProxy )
return;
UpdateCamera();
}
private void UpdateAnimation()
{
if ( AnimationHelper is null ) return;
var wv = WishVelocity.Length;
AnimationHelper.WithWishVelocity( WishVelocity );
AnimationHelper.WithVelocity( CharacterController.Velocity );
AnimationHelper.IsGrounded = CharacterController.IsOnGround;
AnimationHelper.DuckLevel = Crouching ? 1.0f : 0.0f;
AnimationHelper.MoveStyle = wv < 160f ? CitizenAnimationHelper.MoveStyles.Walk : CitizenAnimationHelper.MoveStyles.Run;
var lookDir = EyeAngles.ToRotation().Forward * 1024;
AnimationHelper.WithLook( lookDir, 1, 0.5f, 0.25f );
}
private void UpdateBodyVisibility()
{
if ( AnimationHelper is null )
return;
var renderMode = ModelRenderer.ShadowRenderType.On;
if ( !IsProxy ) renderMode = ModelRenderer.ShadowRenderType.ShadowsOnly;
AnimationHelper.Target.RenderType = renderMode;
foreach ( var clothing in AnimationHelper.Target.Components.GetAll<ModelRenderer>( FindMode.InChildren ) )
{
if ( !clothing.Tags.Has( "clothing" ) )
continue;
clothing.RenderType = renderMode;
}
}
}