PlayerControllerPlus/PlayerControllerPlus.Input.cs

Partial PlayerControllerPlus component handling player input and movement-related settings. It defines input properties (speeds, buttons, look controls), updates eye angles, processes movement inertia, jump input and broadcasting jump animation, and ducking logic that adjusts height and position.

Networking
namespace Sandbox;

public sealed partial class PlayerControllerPlus : Component
{
	[FeatureEnabled( "Input", Icon = "sports_esports" )]
	public bool UseInputControls { get; set; } = true;

	[Property, Feature( "Input" )] public float WalkSpeed { get; set; } = 110;
	[Property, Feature( "Input" )] public float RunSpeed { get; set; } = 320;
	[Property, Feature( "Input" )] public float DuckedSpeed { get; set; } = 70;
	[Property, Feature( "Input" )] public float JumpSpeed { get; set; } = 300;
	[Property, Feature( "Input" )] public float DuckedHeight { get; set; } = 36;

	/// <summary>
	/// Amount of seconds it takes to get from your current speed to your requuested speed, if higher
	/// </summary>
	[Property, Feature( "Input" )] public float AccelerationTime { get; set; } = 0;

	/// <summary>
	/// Amount of seconds it takes to get from your current speed to your requuested speed, if lower
	/// </summary>
	[Property, Feature( "Input" )] public float DeaccelerationTime { get; set; } = 0;

	/// <summary>
	/// The button that the player will press to use to run
	/// </summary>
	[Property, Feature( "Input" ), InputAction, Category( "Running" )] public string AltMoveButton { get; set; } = "run";

	/// <summary>
	/// If true then the player will run by default, and holding AltMoveButton will switch to walk
	/// </summary>
	[Property, Feature( "Input" ), Category( "Running" )] public bool RunByDefault { get; set; }

	/// <summary>
	/// Allows to player to interact with things by "use"ing them. 
	/// Usually by pressing the "use" button.
	/// </summary>
	[Property, Feature( "Input" ), ToggleGroup( "EnablePressing", Label = "Enable Pressing" )] public bool EnablePressing { get; set; } = true;

	/// <summary>
	/// The button that the player will press to use things
	/// </summary>
	[Property, Feature( "Input" ), Group( "EnablePressing" ), InputAction] public string UseButton { get; set; } = "use";

	/// <summary>
	/// How far from the eye can the player reach to use things
	/// </summary>
	[Property, Feature( "Input" ), Group( "EnablePressing" )] public float ReachLength { get; set; } = 130;


	/// <summary>
	/// When true we'll move the camera around using the mouse
	/// </summary>
	[Property, Feature( "Input" ), Category( "Eye Angles" )] public bool UseLookControls { get; set; } = true;
	[Property, Feature( "Input" ), Category( "Eye Angles" )] public bool RotateWithGround { get; set; } = true;
	[Property, Feature( "Input" ), Category( "Eye Angles" ), Range( 0, 180 )] public float PitchClamp { get; set; } = 90;

	/// <summary>
	/// Allows modifying the eye angle sensitivity. Note that player preference sensitivity is already automatically applied, this is just extra.
	/// </summary>
	[Property, Feature( "Input" ), Category( "Eye Angles" ), Range( 0, 2 )] public float LookSensitivity { get; set; } = 1;

	[Property, Feature( "Extended" ), Group( "Input" )] public float SlowWalkSpeed { get; set; } = 70;

	/// <summary>
	/// The button that the player will press to slow walk
	/// </summary>
	[Property, Feature( "Extended" ), Group( "Input" ), InputAction] public string SlowWalkButton { get; set; } = "walk";

	[Property, Feature( "Extended" ), Group( "Input" )] public bool EnableSlowWalk { get; set; }

	[Property, Feature( "Extended" ), Group( "Input" ), Range( 0, 1 )] public float AirControlForward { get; set; } = 0;
	[Property, Feature( "Extended" ), Group( "Input" ), Range( 0, 1 )] public float AirControlBackward { get; set; } = 0;
	[Property, Feature( "Extended" ), Group( "Input" ), Range( 0, 1 )] public float AirControlLateral { get; set; } = 0;

	/// <summary>
	/// Movement input is relative to the player body's facing direction rather than the camera direction.
	/// </summary>
	[Property, Feature( "Extended" ), Group( "Input" )] public bool MoveRelativeToBody { get; set; }

	/// <summary>
	/// Prevents mouse input from changing the eye/camera angles.
	/// </summary>
	[Property, Feature( "Extended" ), Group( "Input" )] public bool DisableLookInput { get; set; }

	TimeSince timeSinceJump = 0;

	void UpdateEyeAngles()
	{
		var input = Input.AnalogLook;

		input *= LookSensitivity;

		IEvents.PostToGameObject( GameObject, x => x.OnEyeAngles( ref input ) );

		var ee = EyeAngles;
		ee += input;
		ee.roll = 0;

		if ( PitchClamp > 0 )
		{
			ee.pitch = ee.pitch.Clamp( -PitchClamp, PitchClamp );
		}

		EyeAngles = ee;
	}

	Vector3.SmoothDamped _inertiaSmooth;

	void InputMove()
	{
		Rotation rot;

		if ( ExtendedFeaturesEnabled && MoveRelativeToBody && UseAnimatorControls && Renderer.IsValid() )
			rot = Renderer.WorldRotation;
		else
			rot = EyeAngles.ToRotation();

		var wish = Mode.UpdateMove( rot, Input.AnalogMove );

		if ( ExtendedFeaturesEnabled && Inertia > 0 )
		{
			_inertiaSmooth.Target = wish;
			_inertiaSmooth.SmoothTime = Inertia;
			_inertiaSmooth.Update( Time.Delta );
			wish = _inertiaSmooth.Current;
		}
		else
		{
			_inertiaSmooth.Current = wish;
		}

		WishVelocity = wish;
	}

	void InputJump()
	{
		if ( TimeSinceGrounded > 0.33f ) return; // been off the ground for this many seconds, don't jump
		if ( !Input.Pressed( "Jump" ) ) return; // not pressing jump
		if ( timeSinceJump < 0.5f ) return; // don't jump too often
		if ( JumpSpeed <= 0 ) return;

		timeSinceJump = 0;
		Jump( Vector3.Up * JumpSpeed );
		OnJumped();

		IEvents.PostToGameObject( GameObject, x => x.OnJumped() );
	}

	[Rpc.Broadcast( NetFlags.OwnerOnly | NetFlags.Unreliable )]
	public void OnJumped()
	{
		if ( UseAnimatorControls && Renderer.IsValid() )
		{
			Renderer.Set( "b_jump", true );
		}
	}

	Vector3 bodyDuckOffset = 0;

	/// <summary>
	/// Gets the current character height from <see cref="BodyHeight"/> when standing,
	/// otherwise uses <see cref="DuckedHeight"/> when ducking.
	/// </summary>
	public float CurrentHeight => IsDucking ? DuckedHeight : BodyHeight;

	/// <summary>
	/// Called during FixedUpdate when UseInputControls is enabled. Will duck if requested.
	/// If not, and we're ducked, will unduck if there is room
	/// </summary>
	public void UpdateDucking( bool wantsDuck )
	{
		if ( wantsDuck == IsDucking ) return;

		var unduckDelta = BodyHeight - DuckedHeight;

		// Can we unduck?
		if ( !wantsDuck )
		{
			if ( IsAirborne )
				return;

			if ( Headroom < unduckDelta )
				return;
		}

		IsDucking = wantsDuck;

		// if we're in the air, keep our head in the same position
		if ( wantsDuck && IsAirborne )
		{
			WorldPosition += Vector3.Up * unduckDelta;
			Transform.ClearInterpolation();
			bodyDuckOffset = Vector3.Up * -unduckDelta;
		}
	}
}