EvadeMovement.cs

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.

File Access
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;
		}
	}
}