Player component partial handling footstep sounds. It receives footstep events, checks ground surface, traces if needed, looks up surface sound collection, and plays left/right foot audio with volume, mixer routing and debug overlays.
using Sandbox;
using Sandbox.Audio;
namespace BrickJam;
public sealed partial class Player
{
[Property, Feature( "Animator" ), Group( "Footsteps" )] public bool EnableFootstepSounds { get; set; } = true;
[Property, Feature( "Animator" ), Group( "Footsteps" )] public float FootstepVolume { get; set; } = 1;
[Property, Feature( "Animator" ), Group( "Footsteps" )] public MixerHandle FootstepMixer { get; set; }
/// <summary>
/// Draw debug overlay on footsteps
/// </summary>
public bool DebugFootsteps;
TimeSince _timeSinceStep;
private void OnFootstepEvent( SceneModel.FootstepEvent e )
{
if ( !IsOnGround ) return;
if ( !EnableFootstepSounds ) return;
if ( _timeSinceStep < 0.2f ) return;
_timeSinceStep = 0;
float volume = e.Volume * WishVelocity.Length.Remap( 0, 400, 0, 1 );
if ( volume <= 0.1f ) return;
PlayFootstepSound( e.Transform.Position, volume, e.FootId );
}
/// <summary>
/// Play a footstep sound at the given world position. Will only play if the player has a GroundSurface.
/// </summary>
public void PlayFootstepSound( Vector3 worldPosition, float volume, int foot )
{
// The controller only sets GroundSurface for the local player; for everyone else (and as a
// fallback) trace down at the foot to find what we're standing on.
var surface = GroundSurface;
if ( !surface.IsValid() )
{
var tr = Scene.Trace.Ray( worldPosition + Vector3.Up * 8f, worldPosition - Vector3.Up * 16f )
.IgnoreGameObjectHierarchy( GameObject )
.WithoutTags( "player", "npc", "loot", "nocollide" )
.Run();
surface = tr.Surface;
}
if ( !surface.IsValid() ) return;
var soundEvent = foot == 0 ? surface.SoundCollection.FootLeft : surface.SoundCollection.FootRight;
if ( soundEvent is null )
{
if ( DebugFootsteps )
{
DebugOverlay.Sphere( new Sphere( worldPosition, volume ), duration: 10, color: Color.Orange, overlay: true );
}
return;
}
var handle = GameObject.PlaySound( soundEvent, 0 );
if ( !handle.IsValid() ) return;
handle.FollowParent = false;
handle.TargetMixer = FootstepMixer.GetOrDefault();
handle.Volume *= volume * FootstepVolume;
if ( DebugFootsteps )
{
DebugOverlay.Sphere( new Sphere( worldPosition, volume ), duration: 10, overlay: true );
DebugOverlay.Text( worldPosition, $"{soundEvent.ResourceName}", size: 14, flags: TextFlag.LeftTop, duration: 10, overlay: true );
}
}
}