fighter/fighter_movement.cs
using System;
using System.Collections.Generic;
using Sandbox;
public sealed class FighterMovement : Component
{
[Property] public CharacterController Controller { get; set; }
[Property] public NavMeshAgent Agent { get; set; }
[Property] public SkinnedModelRenderer AnimRenderer { get; set; }
[Property, Group( "Vitesse" )] public float WalkSpeed { get; set; } = 190f;
[Property, Group( "Vitesse" )] public float SprintSpeed { get; set; } = 280f;
[Property, Group( "Vitesse" )] public float CrouchSpeed { get; set; } = 100f;
[Property, Group( "Vitesse" )] public float Acceleration { get; set; } = 12f;
[Property, Group( "Vitesse" )] public float GroundFriction { get; set; } = 5f;
[Property, Group( "Vitesse" )] public float JumpImpulse { get; set; } = 320f;
[Property, Group( "Collision Corporelle" )] public float BodyRadius { get; set; } = 24f;
[Property, Group( "Collision Corporelle" )] public float PushStrength { get; set; } = 15f;
[Property, Group( "Wall Kick" )] public float WallKickBackwardForce { get; set; } = 340f;
[Property, Group( "Wall Kick" )] public float WallKickUpwardForce { get; set; } = 160f;
[Property, Group( "Wall Kick" )] public SoundEvent WallKickSound { get; set; }
[Property, Group( "Animation Blendspace 2D" )] public float BlendReferenceSpeed { get; set; } = 140f;
[Property, Group( "Animation Blendspace 2D" )] public float ForwardMultiplier { get; set; } = 1.0f;
[Property, Group( "Animation Blendspace 2D" )] public float StrafeMultiplier { get; set; } = 1.25f;
[Property, Group( "Animation Blendspace 2D" )] public float MinMoveThreshold { get; set; } = 6f;
[Property, Range( 1f, 30f ), Group( "Animation Blendspace 2D" )] public float SmoothingSpeed { get; set; } = 10f;
[Property, Group( "Audio Pas & Surfaces" )] public float StepDistanceWalk { get; set; } = 120f;
[Property, Group( "Audio Pas & Surfaces" )] public float StepDistanceSprint { get; set; } = 170f;
[Property, Group( "Audio Pas & Surfaces" )] public float StepFootWidthOffset { get; set; } = 14f;
[Property, Group( "Audio Pas & Surfaces" )] public SoundEvent FootstepSand { get; set; }
[Property, Group( "Audio Pas & Surfaces" )] public SoundEvent FootstepStone { get; set; }
[Property, Group( "Audio Pas & Surfaces" )] public SoundEvent FootstepWood { get; set; }
[Property, Group( "Audio Pas & Surfaces" )] public SoundEvent FootstepDefault { get; set; }
[Property, Group( "Audio Pas & Surfaces" )] public SoundEvent JumpLandSound { get; set; }
public Vector3 WishVelocity { get; set; }
public bool WantsSprint { get; set; }
public bool WantsCrouch { get; set; }
public bool IsGrounded
{
get
{
if ( Agent != null && Agent.Enabled ) return true;
return Controller?.IsOnGround ?? true;
}
}
public bool IsJumping => !IsGrounded;
public bool IsReboundAirborne { get; private set; } = false;
private float _defaultHeight;
private Fighter _fighter;
private Vector3 _lastFixedPosition;
private Vector3 _measuredVelocity;
private float _smoothMoveX;
private float _smoothMoveY;
private float _smoothSpeed;
private float _stepDistanceAccumulator = 0f;
private bool _stepLeftLeg = false;
private bool _wasGroundedLastFrame = true;
private readonly List<SkinnedModelRenderer> _cachedRenderers = new();
protected override void OnAwake()
{
Controller ??= Components.Get<CharacterController>();
Agent ??= Components.Get<NavMeshAgent>();
_fighter = Components.Get<Fighter>();
RefreshRenderers();
}
protected override void OnStart()
{
if ( Controller != null )
_defaultHeight = Controller.Height;
RefreshRenderers();
_lastFixedPosition = WorldPosition;
}
public void RefreshRenderers()
{
_cachedRenderers.Clear();
_cachedRenderers.AddRange( Components.GetAll<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants ) );
if ( AnimRenderer == null || !AnimRenderer.IsValid )
{
foreach ( var rend in _cachedRenderers )
{
if ( rend.Model != null && !rend.Model.IsError )
{
AnimRenderer = rend;
break;
}
}
AnimRenderer ??= Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants );
}
}
protected override void OnFixedUpdate()
{
if ( Time.Delta > 0.0001f )
{
var deltaMovement = WorldPosition - _lastFixedPosition;
_measuredVelocity = deltaMovement / Time.Delta;
}
_lastFixedPosition = WorldPosition;
ResolveFighterCollisions();
if ( Agent != null && Agent.Enabled ) return;
if ( Controller == null ) return;
if ( _fighter != null && _fighter.Status != FighterStatus.Active )
{
WishVelocity = Vector3.Zero;
}
Controller.Height = WantsCrouch ? (_defaultHeight * 0.55f) : _defaultHeight;
if ( Controller.IsOnGround )
{
IsReboundAirborne = false;
if ( Controller.Velocity.z < 0f )
Controller.Velocity = Controller.Velocity.WithZ( 0 );
Controller.ApplyFriction( GroundFriction );
var speed = WantsCrouch ? CrouchSpeed : (WantsSprint ? SprintSpeed : WalkSpeed);
Controller.Accelerate( WishVelocity.ClampLength( 1f ) * speed );
}
else
{
Controller.Velocity += Scene.PhysicsWorld.Gravity * Time.Delta;
Controller.Accelerate( WishVelocity.ClampLength( 1f ) * (WalkSpeed * 0.25f) );
}
Controller.Move();
}
private void ResolveFighterCollisions()
{
if ( _fighter == null || _fighter.IsDead ) return;
foreach ( var other in Scene.GetAllComponents<Fighter>() )
{
if ( other == _fighter || !other.IsValid || other.IsDead ) continue;
var delta = WorldPosition.WithZ( 0 ) - other.WorldPosition.WithZ( 0 );
var dist = delta.Length;
var targetDist = BodyRadius * 2f;
if ( dist < targetDist && dist > 0.001f )
{
var overlap = targetDist - dist;
var pushDir = delta / dist;
if ( Controller != null && Controller.Enabled && (Agent == null || !Agent.Enabled) )
{
Controller.Punch( pushDir * (overlap * PushStrength) );
}
else if ( Agent != null && Agent.Enabled )
{
WorldPosition += pushDir * (overlap * 0.5f);
}
}
}
}
protected override void OnUpdate()
{
UpdateAnimationParameters();
UpdateProceduralFootsteps();
}
private void UpdateAnimationParameters()
{
if ( _cachedRenderers.Count == 0 )
RefreshRenderers();
Vector3 currentVelocity = GetActiveVelocity();
var horizontalVel = currentVelocity.WithZ( 0 );
var speedMagnitude = horizontalVel.Length;
float targetMoveX = 0f;
float targetMoveY = 0f;
if ( speedMagnitude > MinMoveThreshold )
{
var localVel = WorldRotation.Inverse * horizontalVel;
var refSpeed = MathF.Max( 10f, BlendReferenceSpeed );
targetMoveX = Math.Clamp( (localVel.x / refSpeed) * ForwardMultiplier, -1f, 1f );
targetMoveY = Math.Clamp( (-localVel.y / refSpeed) * StrafeMultiplier, -1f, 1f );
}
var blend = Math.Clamp( SmoothingSpeed * Time.Delta, 0f, 1f );
_smoothMoveX = MathX.Lerp( _smoothMoveX, targetMoveX, blend );
_smoothMoveY = MathX.Lerp( _smoothMoveY, targetMoveY, blend );
_smoothSpeed = MathX.Lerp( _smoothSpeed, speedMagnitude, blend );
bool isGrounded = IsGrounded;
bool isJumping = IsJumping;
for ( int i = 0; i < _cachedRenderers.Count; i++ )
{
var rend = _cachedRenderers[i];
if ( !rend.IsValid ) continue;
rend.Set( "move_x", _smoothMoveX );
rend.Set( "move_y", _smoothMoveY );
rend.Set( "speed", _smoothSpeed );
rend.Set( "is_grounded", isGrounded );
rend.Set( "is_jumping", isJumping );
}
}
private Vector3 GetActiveVelocity()
{
if ( Agent != null && Agent.Enabled )
{
if ( Agent.Velocity.LengthSquared > 1f )
return Agent.Velocity;
return _measuredVelocity;
}
if ( Controller != null && Controller.Enabled )
return Controller.Velocity;
return _measuredVelocity;
}
private void UpdateProceduralFootsteps()
{
if ( _fighter != null && (_fighter.IsDead || _fighter.Status != FighterStatus.Active) )
{
_stepDistanceAccumulator = 0f;
return;
}
bool isGrounded = IsGrounded;
if ( !_wasGroundedLastFrame && isGrounded )
{
PlayLandingSound();
_stepDistanceAccumulator = 0f;
}
_wasGroundedLastFrame = isGrounded;
if ( !isGrounded ) return;
var horizontalSpeed = GetActiveVelocity().WithZ( 0 ).Length;
if ( horizontalSpeed < MinMoveThreshold )
{
_stepDistanceAccumulator = MathF.Max( 0f, _stepDistanceAccumulator - Time.Delta * 40f );
return;
}
float currentStepThreshold = WantsSprint ? StepDistanceSprint : (WantsCrouch ? StepDistanceWalk * 0.75f : StepDistanceWalk);
_stepDistanceAccumulator += horizontalSpeed * Time.Delta;
if ( _stepDistanceAccumulator >= currentStepThreshold )
{
_stepDistanceAccumulator = 0f;
_stepLeftLeg = !_stepLeftLeg;
TriggerFootstep( _stepLeftLeg );
}
}
private void TriggerFootstep( bool leftFoot )
{
var sideOffset = leftFoot ? -WorldRotation.Right * StepFootWidthOffset : WorldRotation.Right * StepFootWidthOffset;
var footOrigin = WorldPosition + sideOffset + Vector3.Up * 25f;
var tr = Scene.Trace.Ray( footOrigin, footOrigin + Vector3.Down * 60f )
.IgnoreGameObjectHierarchy( GameObject )
.Run();
var hitPos = tr.Hit ? tr.HitPosition : (WorldPosition + sideOffset);
var surfaceName = tr.Hit ? tr.Surface?.ResourceName : null;
var soundToPlay = ResolveSurfaceSound( surfaceName );
if ( soundToPlay == null ) return;
var snd = Sound.Play( soundToPlay, hitPos );
if ( Components.Get<PlayerController>() != null && snd != null )
{
snd.Volume = WantsCrouch ? 0.45f : 0.85f;
}
}
private void PlayLandingSound()
{
var tr = Scene.Trace.Ray( WorldPosition + Vector3.Up * 25f, WorldPosition + Vector3.Down * 60f )
.IgnoreGameObjectHierarchy( GameObject )
.Run();
var hitPos = tr.Hit ? tr.HitPosition : WorldPosition;
var surfaceName = tr.Hit ? tr.Surface?.ResourceName : null;
var landSound = JumpLandSound ?? ResolveSurfaceSound( surfaceName );
if ( landSound != null )
Sound.Play( landSound, hitPos );
}
private SoundEvent ResolveSurfaceSound( string surfaceName )
{
if ( string.IsNullOrEmpty( surfaceName ) )
return FootstepDefault ?? FootstepStone;
var lower = surfaceName.ToLowerInvariant();
if ( lower.Contains( "sand" ) || lower.Contains( "dirt" ) || lower.Contains( "gravel" ) )
return FootstepSand ?? FootstepDefault;
if ( lower.Contains( "wood" ) || lower.Contains( "plank" ) )
return FootstepWood ?? FootstepDefault;
if ( lower.Contains( "stone" ) || lower.Contains( "rock" ) || lower.Contains( "concrete" ) || lower.Contains( "tile" ) )
return FootstepStone ?? FootstepDefault;
return FootstepDefault ?? FootstepStone;
}
public void Jump()
{
if ( Controller != null && Controller.IsOnGround && _fighter?.Status == FighterStatus.Active )
{
Controller.Punch( Vector3.Up * JumpImpulse );
AnimRenderer?.Set( "jump", true );
}
}
public void ApplyWallKick( Vector3 viewForward )
{
IsReboundAirborne = true;
var backDir = -viewForward.Normal;
var impulse = backDir * WallKickBackwardForce + Vector3.Up * WallKickUpwardForce;
if ( Controller != null && Controller.Enabled && (Agent == null || !Agent.Enabled) )
{
Controller.Punch( impulse );
}
else if ( Agent != null && Agent.Enabled )
{
WorldPosition += impulse.WithZ( 0 ) * 0.015f;
}
if ( WallKickSound != null )
{
Sound.Play( WallKickSound, WorldPosition );
}
}
public void ConsumeReboundAirborne()
{
IsReboundAirborne = false;
}
public void ApplyKnockback( Vector3 force )
{
if ( Controller != null && Controller.Enabled && (Agent == null || !Agent.Enabled) )
{
Controller.Punch( force );
}
else
{
WorldPosition += force.WithZ( 0 ) * 0.012f;
}
AnimRenderer?.Set( "hit_flinch", true );
}
}