PlayerMovementInput.cs
using Sandbox;
public sealed class PlayerMovementInput : Component
{
[Property]
public PlayerController Controller { get; set; }
[Property]
public string CrouchAction { get; set; } = "crouch";
[Property]
public string SprintAction { get; set; } = "run";
[Property]
public string JumpAction { get; set; } = "Jump";
// =========================================================
// CURRENT INPUT STATE
// =========================================================
public bool ControlsEnabled { get; private set; }
public Vector3 MoveInput { get; private set; }
public bool SprintDown { get; private set; }
public bool CrouchDown { get; private set; }
/// <summary>
/// When true, raw local Input is ignored and a bot/automation component feeds
/// this same input surface through SetBotInput(). PlayerLocomotion therefore
/// remains identical for humans and bots.
/// </summary>
public bool IsBotDriven { get; private set; }
// =========================================================
// QUEUED TRANSITIONS
// =========================================================
private bool _sprintPressedQueued;
private bool _sprintReleasedQueued;
private bool _crouchPressedQueued;
private bool _crouchReleasedQueued;
private bool _jumpPressedQueued;
private bool _previousSprintDown;
private bool _previousCrouchDown;
protected override void OnStart()
{
Controller ??=
Components.Get<PlayerController>();
if ( Controller is null )
{
Log.Warning(
"PlayerMovementInput could not find PlayerController."
);
return;
}
//
// PlayerLocomotion owns gameplay movement.
//
Controller.UseInputControls =
false;
ControlsEnabled =
false;
Log.Info(
$"PlayerMovementInput active | Proxy: {IsProxy} | Owner: {Network.OwnerId}"
);
}
protected override void OnUpdate()
{
if ( IsProxy )
return;
if ( Controller is null )
return;
Controller.UseInputControls =
false;
PlayerState playerState =
Components.Get<PlayerState>();
if ( playerState?.IsBot == true )
{
IsBotDriven = true;
}
// A bot uses this component as the shared input contract, but must never
// sample the host's keyboard/gamepad. Synthetic commands are supplied by
// OMRBotController and future bot-brain phases.
if ( IsBotDriven )
{
if ( !ControlsEnabled )
{
MoveInput = Vector3.Zero;
SprintDown = false;
CrouchDown = false;
_previousSprintDown = false;
_previousCrouchDown = false;
ClearQueuedActions();
}
return;
}
bool currentSprintDown =
Input.Down(
SprintAction
);
bool currentCrouchDown =
Input.Down(
CrouchAction
);
//
// While controls are disabled we still follow the
// physical state of the buttons so enabling controls
// later does not generate fake press events.
//
if ( !ControlsEnabled )
{
MoveInput =
Vector3.Zero;
SprintDown =
currentSprintDown;
CrouchDown =
currentCrouchDown;
_previousSprintDown =
currentSprintDown;
_previousCrouchDown =
currentCrouchDown;
ClearQueuedActions();
return;
}
MoveInput =
Input.AnalogMove;
// =====================================================
// SPRINT
// =====================================================
if (
currentSprintDown &&
!_previousSprintDown
)
{
_sprintPressedQueued =
true;
}
if (
!currentSprintDown &&
_previousSprintDown
)
{
_sprintReleasedQueued =
true;
}
SprintDown =
currentSprintDown;
_previousSprintDown =
currentSprintDown;
// =====================================================
// CROUCH / SLIDE
// =====================================================
if (
currentCrouchDown &&
!_previousCrouchDown
)
{
_crouchPressedQueued =
true;
}
if (
!currentCrouchDown &&
_previousCrouchDown
)
{
_crouchReleasedQueued =
true;
}
CrouchDown =
currentCrouchDown;
_previousCrouchDown =
currentCrouchDown;
// =====================================================
// JUMP
// =====================================================
if (
Input.Pressed(
JumpAction
)
)
{
_jumpPressedQueued =
true;
}
}
public void SetBotDriven(
bool botDriven
)
{
IsBotDriven = botDriven;
if ( !botDriven )
{
MoveInput = Vector3.Zero;
SprintDown = false;
CrouchDown = false;
_previousSprintDown = false;
_previousCrouchDown = false;
ClearQueuedActions();
}
}
/// <summary>
/// Synthetic equivalent of the human movement/button sampling path. Future
/// navigation/tactical AI should call this rather than manipulating
/// PlayerController directly.
/// </summary>
public void SetBotInput(
Vector3 moveInput,
bool sprintDown,
bool crouchDown,
bool jumpPressed
)
{
if ( !IsBotDriven )
return;
if ( !ControlsEnabled )
{
MoveInput = Vector3.Zero;
SprintDown = false;
CrouchDown = false;
_previousSprintDown = false;
_previousCrouchDown = false;
ClearQueuedActions();
return;
}
MoveInput = moveInput;
if ( sprintDown && !_previousSprintDown )
_sprintPressedQueued = true;
if ( !sprintDown && _previousSprintDown )
_sprintReleasedQueued = true;
SprintDown = sprintDown;
_previousSprintDown = sprintDown;
if ( crouchDown && !_previousCrouchDown )
_crouchPressedQueued = true;
if ( !crouchDown && _previousCrouchDown )
_crouchReleasedQueued = true;
CrouchDown = crouchDown;
_previousCrouchDown = crouchDown;
if ( jumpPressed )
_jumpPressedQueued = true;
}
public bool ConsumeSprintPressed()
{
bool value =
_sprintPressedQueued;
_sprintPressedQueued =
false;
return value;
}
public bool ConsumeSprintReleased()
{
bool value =
_sprintReleasedQueued;
_sprintReleasedQueued =
false;
return value;
}
public bool ConsumeCrouchPressed()
{
bool value =
_crouchPressedQueued;
_crouchPressedQueued =
false;
return value;
}
public bool ConsumeCrouchReleased()
{
bool value =
_crouchReleasedQueued;
_crouchReleasedQueued =
false;
return value;
}
public bool ConsumeJumpPressed()
{
bool value =
_jumpPressedQueued;
_jumpPressedQueued =
false;
return value;
}
public void SetControlsEnabled(
bool enabled
)
{
ControlsEnabled =
enabled;
if ( Controller is not null )
{
Controller.UseInputControls =
false;
if ( !enabled )
{
Controller.WishVelocity =
Vector3.Zero;
}
}
if ( !enabled )
{
MoveInput =
Vector3.Zero;
ClearQueuedActions();
}
}
private void ClearQueuedActions()
{
_sprintPressedQueued =
false;
_sprintReleasedQueued =
false;
_crouchPressedQueued =
false;
_crouchReleasedQueued =
false;
_jumpPressedQueued =
false;
}
}