Player/Player.Footsteps.cs

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.

NetworkingFile Access
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 );
		}
	}
}