A player movement component for a Facepunch s&box game that implements sprinting, crouch-sliding, jump momentum preservation (bunny hop style), and camera effects like roll and FOV kick. It updates movement and camera each frame, applies slide physics via Rigidbody or CharacterController, and plays a slide sound.
using Sandbox;
using System;
public sealed class EvadeMovement : Component
{
[Property] public float WalkSpeed { get; set; } = 220f;
[Property] public float SprintSpeed { get; set; } = 480f;
[Property] public float SlideInitialSpeed { get; set; } = 680f;
[Property] public float SlideDecayRate { get; set; } = 260f;
[Property] public float SlideMinSpeed { get; set; } = 220f;
[Property] public float JumpImpulse { get; set; } = 330f;
[Property] public float CameraTiltMaxAngle { get; set; } = 3.5f;
[Property] public float CameraFovKickSprint { get; set; } = 8f;
[Property] public float CameraFovKickSlide { get; set; } = 15f;
[Property] public string SlideSound { get; set; } = "music/buttonhover.sound";
public bool IsSliding { get; private set; }
public float CurrentSlideSpeed { get; private set; }
public Vector3 SlideDirection { get; private set; }
private PlayerController _playerController;
private Rigidbody _rigidbody;
private CharacterController _characterController;
private CameraComponent _camera;
private PlayerHealth _playerHealth;
private float _baseFov = 90f;
private float _currentRoll = 0f;
private float _airMomentum = 0f;
[Sync] private TimeSince _timeSinceLanded { get; set; } = 0f;
[Sync] private TimeSince _timeSinceSlideStart { get; set; } = 0f;
private bool _wasGrounded = true;
private float _originalBodyHeight = 72f;
protected override void OnStart()
{
_playerController = Components.Get<PlayerController>();
_rigidbody = Components.Get<Rigidbody>();
_characterController = Components.Get<CharacterController>();
_playerHealth = Components.Get<PlayerHealth>() ?? Components.GetInChildren<PlayerHealth>();
_camera = Scene.Camera ?? Components.GetInChildren<CameraComponent>();
if ( _camera.IsValid() )
{
_baseFov = _camera.FieldOfView;
if ( _baseFov <= 10f ) _baseFov = 90f;
}
if ( _playerController.IsValid() )
{
_originalBodyHeight = _playerController.BodyHeight;
_playerController.WalkSpeed = WalkSpeed;
_playerController.RunSpeed = SprintSpeed;
}
}
protected override void OnUpdate()
{
if ( IsProxy ) return;
if ( _playerHealth.IsValid() && ( _playerHealth.IsDead || _playerHealth.IsDowned ) )
{
if ( IsSliding ) EndSlide();
ResetCameraEffects();
return;
}
bool isGrounded = CheckIsGrounded();
if ( isGrounded && !_wasGrounded )
{
_timeSinceLanded = 0f;
}
_wasGrounded = isGrounded;
UpdateMovement( isGrounded );
UpdateCameraJuice( isGrounded );
}
private bool CheckIsGrounded()
{
if ( _characterController.IsValid() && _characterController.IsOnGround )
return true;
var tr = Scene.Trace.Ray( WorldPosition + Vector3.Up * 10f, WorldPosition + Vector3.Down * 25f )
.IgnoreGameObjectHierarchy( GameObject )
.Run();
return tr.Hit;
}
private void UpdateMovement( bool isGrounded )
{
var moveInput = Input.AnalogMove;
bool wantsRun = Input.Down( "run" );
bool wantsDuck = Input.Down( "duck" );
bool wantsJump = Input.Pressed( "jump" );
// 1. SPRINT
if ( _playerController.IsValid() )
{
_playerController.WalkSpeed = WalkSpeed;
_playerController.RunSpeed = SprintSpeed;
}
// 2. CROUCH SLIDE
if ( isGrounded )
{
if ( wantsDuck && !IsSliding && ( wantsRun || _airMomentum > SprintSpeed * 0.7f ) && !moveInput.IsNearlyZero() )
{
StartSlide( moveInput );
}
else if ( IsSliding )
{
if ( !wantsDuck || wantsJump || CurrentSlideSpeed <= SlideMinSpeed )
{
EndSlide();
}
else
{
CurrentSlideSpeed = MathF.Max( SlideMinSpeed, CurrentSlideSpeed - SlideDecayRate * Time.Delta );
// Направляем движение скольжения
var velocity = SlideDirection * CurrentSlideSpeed;
ApplyHorizontalVelocity( velocity );
}
}
}
else
{
// В воздухе скольжение прекращается
if ( IsSliding )
{
EndSlide();
}
}
// 3. BUNNY HOP / MOMENTUM PRESERVATION
if ( wantsJump && isGrounded )
{
float jumpMomentum = IsSliding ? CurrentSlideSpeed : (wantsRun ? SprintSpeed : WalkSpeed);
_airMomentum = MathF.Max( _airMomentum, jumpMomentum );
if ( IsSliding )
{
EndSlide();
}
// Прыжок
if ( _rigidbody.IsValid() )
{
_rigidbody.Velocity = _rigidbody.Velocity.WithZ( JumpImpulse );
}
else if ( _characterController.IsValid() )
{
_characterController.Velocity = _characterController.Velocity.WithZ( JumpImpulse );
}
}
// Управление инерцией в воздухе (air strafing)
if ( !isGrounded && _airMomentum > WalkSpeed )
{
if ( !moveInput.IsNearlyZero() && _camera.IsValid() )
{
var camYaw = _camera.WorldRotation.Angles().yaw;
var forward = Rotation.FromYaw( camYaw ).Forward;
var left = Rotation.FromYaw( camYaw ).Left;
var wishDir = (forward * moveInput.x + left * moveInput.y).Normal;
var airVelocity = wishDir * _airMomentum;
ApplyHorizontalVelocity( airVelocity );
}
}
// Затухание сохранённого импульса на земле после серии прыжков
if ( isGrounded && !IsSliding && _timeSinceLanded > 0.25f )
{
float targetSpeed = wantsRun ? SprintSpeed : WalkSpeed;
_airMomentum = MathX.Lerp( _airMomentum, targetSpeed, Time.Delta * 4f );
}
}
private void StartSlide( Vector2 moveInput )
{
IsSliding = true;
_timeSinceSlideStart = 0f;
var camYaw = _camera.IsValid() ? _camera.WorldRotation.Angles().yaw : WorldRotation.Angles().yaw;
var forward = Rotation.FromYaw( camYaw ).Forward;
var left = Rotation.FromYaw( camYaw ).Left;
var wishDir = (forward * moveInput.x + left * moveInput.y).Normal;
if ( wishDir.IsNearlyZero() )
wishDir = forward;
SlideDirection = wishDir;
CurrentSlideSpeed = MathF.Max( _airMomentum + 50f, SlideInitialSpeed );
_airMomentum = CurrentSlideSpeed;
if ( _playerController.IsValid() )
{
_playerController.BodyHeight = _playerController.DuckedHeight;
}
if ( !string.IsNullOrEmpty( SlideSound ) )
{
Sound.Play( SlideSound, WorldPosition );
}
}
private void EndSlide()
{
IsSliding = false;
if ( _playerController.IsValid() )
{
_playerController.BodyHeight = _originalBodyHeight;
}
}
private void ApplyHorizontalVelocity( Vector3 desiredVelocity )
{
if ( _rigidbody.IsValid() )
{
_rigidbody.Velocity = desiredVelocity.WithZ( _rigidbody.Velocity.z );
}
else if ( _characterController.IsValid() )
{
_characterController.Velocity = desiredVelocity.WithZ( _characterController.Velocity.z );
_characterController.Move();
}
}
private void UpdateCameraJuice( bool isGrounded )
{
// 1. ДИНАМИЧЕСКИЙ TILT КАМЕРЫ (Roll при стрейфах A/D)
var strafeInput = Input.AnalogMove.y;
float targetRoll = -strafeInput * CameraTiltMaxAngle;
if ( IsSliding )
{
// При подкате легкий наклон в сторону движения
targetRoll += (strafeInput == 0 ? -1f : 0f);
}
_currentRoll = MathX.Lerp( _currentRoll, targetRoll, Time.Delta * 8f );
if ( _playerController.IsValid() )
{
var angles = _playerController.EyeAngles;
angles.roll = _currentRoll;
_playerController.EyeAngles = angles;
}
// 2. FOV KICK (Увеличение угла обзора при спринте и слайде)
if ( _camera.IsValid() )
{
float targetFov = _baseFov;
if ( IsSliding )
{
targetFov += CameraFovKickSlide;
}
else if ( Input.Down( "run" ) && (isGrounded || _airMomentum > SprintSpeed * 0.85f) )
{
targetFov += CameraFovKickSprint;
}
_camera.FieldOfView = MathX.Lerp( _camera.FieldOfView, targetFov, Time.Delta * 6f );
}
}
private void ResetCameraEffects()
{
if ( _camera.IsValid() )
{
_camera.FieldOfView = MathX.Lerp( _camera.FieldOfView, _baseFov, Time.Delta * 6f );
}
if ( _playerController.IsValid() )
{
var angles = _playerController.EyeAngles;
angles.roll = MathX.Lerp( angles.roll, 0f, Time.Delta * 8f );
_playerController.EyeAngles = angles;
}
}
}