Sync Footstep Noise to WalkSpeed

Hi

I saw this topic today and was surprised to know that the default roblox sound script doesn’t take Humanoid.WalkSpeed in consideration, so decided to fork and add that functionality to it.
This works for the custom walk/climb/swim sounds as well, you’ll just have to configure the pitch in the SOUND_DATA (defined on line 4)

Comparison

Default sound script behavior
Forked sound script behavior

Steps

  1. Insert a LocalScript in the StarterPlayerScripts and make sure to call it RbxCharacterSounds
  2. Copy and paste the source code inside
Source Code
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local SOUND_DATA : { [string]: {[string]: any}} = {
	Climbing = {
		SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3",
		Looped = true,
		Pitch = 1,
	},
	Died = {
		SoundId = "rbxasset://sounds/uuhhh.mp3",
	},
	FreeFalling = {
		SoundId = "rbxasset://sounds/action_falling.mp3",
		Looped = true,
	},
	GettingUp = {
		SoundId = "rbxasset://sounds/action_get_up.mp3",
	},
	Jumping = {
		SoundId = "rbxasset://sounds/action_jump.mp3",
	},
	Landing = {
		SoundId = "rbxasset://sounds/action_jump_land.mp3",
	},
	Running = {
		SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3",
		Looped = true,
		Pitch = 1.85,
	},
	Splash = {
		SoundId = "rbxasset://sounds/impact_water.mp3",
	},
	Swimming = {
		SoundId = "rbxasset://sounds/action_swim.mp3",
		Looped = true,
		Pitch = 1.6,
	},
}

local DEFAULT_RUN_SOUND_SPEED = SOUND_DATA.Running.Pitch
local DEFAULT_SWIM_SOUND_SPEED = SOUND_DATA.Swimming.Pitch
local DEFAULT_CLIMB_SOUND_SPEED = SOUND_DATA.Climbing.Pitch

local DEFAULT_WALKSPEED = 16
local RUNNING_DIVISOR = DEFAULT_WALKSPEED / DEFAULT_RUN_SOUND_SPEED
local SWIMMING_DIVISOR = DEFAULT_WALKSPEED / DEFAULT_SWIM_SOUND_SPEED
local CLIMBING_DIVISOR = DEFAULT_WALKSPEED / DEFAULT_CLIMB_SOUND_SPEED

-- wait for the first of the passed signals to fire
local function waitForFirst(...) -- RBXScriptSignal
	local shunt: BindableEvent = Instance.new("BindableEvent")
	local slots = {...}

	local function fire(...)
		for i = 1, #slots do
			slots[i]:Disconnect()
		end

		return shunt:Fire(...)
	end

	for i = 1, #slots do -- RBXScriptSignal
		slots[i] = slots[i]:Connect(fire) -- Change to RBXScriptConnection
	end

	return shunt.Event:Wait()
end

-- map a value from one range to another
local function map(x: number, inMin: number, inMax: number, outMin: number, outMax: number): number
	return (x - inMin)*(outMax - outMin)/(inMax - inMin) + outMin
end

local function playSound(sound: Sound)
	sound.TimePosition = 0
	sound.Playing = true
end

local function shallowCopy(t)
	local out = {}
	for k, v in pairs(t) do
		out[k] = v
	end
	return out
end

local function initializeSoundSystem(player: Player, humanoid: Humanoid, rootPart: BasePart)
	local sounds: {[string]: Sound} = {}

	-- initialize sounds
	for name: string, props: {[string]: any} in pairs(SOUND_DATA) do
		local sound: Sound = Instance.new("Sound")
		sound.Name = name

		-- set default values
		sound.Archivable = false
		sound.EmitterSize = 5
		sound.MaxDistance = 150
		sound.Volume = 0.65

		for propName, propValue: any in pairs(props) do
			sound[propName] = propValue
		end

		sound.Parent = rootPart
		sounds[name] = sound
	end

	local playingLoopedSounds: {[Sound]: boolean?} = {}

	local function stopPlayingLoopedSounds(except: Sound?)
		for sound in pairs(shallowCopy(playingLoopedSounds)) do
			if sound ~= except then
				sound.Playing = false
				playingLoopedSounds[sound] = nil
			end
		end
	end

	-- state transition callbacks.
	local stateTransitions: {[Enum.HumanoidStateType]: () -> ()} = {
		[Enum.HumanoidStateType.FallingDown] = function()
			stopPlayingLoopedSounds()
		end,

		[Enum.HumanoidStateType.GettingUp] = function()
			stopPlayingLoopedSounds()
			playSound(sounds.GettingUp)
		end,

		[Enum.HumanoidStateType.Jumping] = function()
			stopPlayingLoopedSounds()
			playSound(sounds.Jumping)
		end,

		[Enum.HumanoidStateType.Swimming] = function()
			local verticalSpeed = math.abs(rootPart.Velocity.Y)
			if verticalSpeed > 0.1 then
				sounds.Splash.Volume = math.clamp(map(verticalSpeed, 100, 350, 0.28, 1), 0, 1)
				playSound(sounds.Splash)
			end
			stopPlayingLoopedSounds(sounds.Swimming)
			sounds.Swimming.Playing = true
			playingLoopedSounds[sounds.Swimming] = true
		end,

		[Enum.HumanoidStateType.Freefall] = function()
			sounds.FreeFalling.Volume = 0
			stopPlayingLoopedSounds(sounds.FreeFalling)
			playingLoopedSounds[sounds.FreeFalling] = true
		end,

		[Enum.HumanoidStateType.Landed] = function()
			stopPlayingLoopedSounds()
			local verticalSpeed = math.abs(rootPart.Velocity.Y)
			if verticalSpeed > 75 then
				sounds.Landing.Volume = math.clamp(map(verticalSpeed, 50, 100, 0, 1), 0, 1)
				playSound(sounds.Landing)
			end
		end,

		[Enum.HumanoidStateType.Running] = function()
			stopPlayingLoopedSounds(sounds.Running)
			sounds.Running.Playing = true
			playingLoopedSounds[sounds.Running] = true
		end,

		[Enum.HumanoidStateType.Climbing] = function()
			local sound = sounds.Climbing
			if math.abs(rootPart.Velocity.Y) > 0.1 then
				sound.Playing = true
				stopPlayingLoopedSounds(sound)
			else
				stopPlayingLoopedSounds()
			end
			playingLoopedSounds[sound] = true
		end,

		[Enum.HumanoidStateType.Seated] = function()
			stopPlayingLoopedSounds()
		end,

		[Enum.HumanoidStateType.Dead] = function()
			stopPlayingLoopedSounds()
			playSound(sounds.Died)
		end,
	}

	-- updaters for looped sounds
	local loopedSoundUpdaters: {[Sound]: (number, Sound, Vector3) -> ()} = {
		[sounds.Climbing] = function(dt: number, sound: Sound, vel: Vector3)
			sound.Playing = vel.Magnitude > 0.1
		end,

		[sounds.FreeFalling] = function(dt: number, sound: Sound, vel: Vector3): ()
			if vel.Magnitude > 75 then
				sound.Volume = math.clamp(sound.Volume + 0.9*dt, 0, 1)
			else
				sound.Volume = 0
			end
		end,

		[sounds.Running] = function(dt: number, sound: Sound, vel: Vector3)
			sound.Playing = vel.Magnitude > 0.5 and humanoid.MoveDirection.Magnitude > 0.5
		end,
	}

	-- state substitutions to avoid duplicating entries in the state table
	local stateRemap: {[Enum.HumanoidStateType]: Enum.HumanoidStateType} = {
		[Enum.HumanoidStateType.RunningNoPhysics] = Enum.HumanoidStateType.Running,
	}

	local activeState: Enum.HumanoidStateType = stateRemap[humanoid:GetState()] or humanoid:GetState()

	local stateChangedConn = humanoid.StateChanged:Connect(function(_, state)
		state = stateRemap[state] or state

		if state ~= activeState then
			local transitionFunc: () -> () = stateTransitions[state]

			if transitionFunc then
				transitionFunc()
			end

			activeState = state
		end
	end)

	local steppedConn = RunService.Stepped:Connect(function(_, worldDt: number)
		-- update looped sounds on stepped
		for sound in pairs(playingLoopedSounds) do
			local updater: (number, Sound, Vector3) -> () = loopedSoundUpdaters[sound]

			if updater then
				updater(worldDt, sound, rootPart.Velocity)
			end
		end
	end)

	local function adjustPlaybackSpeed()
		local walkSpeed = humanoid.WalkSpeed
		
		sounds.Running.PlaybackSpeed = walkSpeed / RUNNING_DIVISOR
		sounds.Swimming.PlaybackSpeed = walkSpeed / SWIMMING_DIVISOR
		sounds.Climbing.PlaybackSpeed = walkSpeed / CLIMBING_DIVISOR
	end

	adjustPlaybackSpeed()
	local speedChangedConn = humanoid:GetPropertyChangedSignal('WalkSpeed'):Connect(adjustPlaybackSpeed)

	local humanoidAncestryChangedConn: RBXScriptConnection
	local rootPartAncestryChangedConn: RBXScriptConnection
	local characterAddedConn: RBXScriptConnection

	local function terminate()
		speedChangedConn:Disconnect()
		stateChangedConn:Disconnect()
		steppedConn:Disconnect()
		humanoidAncestryChangedConn:Disconnect()
		rootPartAncestryChangedConn:Disconnect()
		characterAddedConn:Disconnect()
	end

	humanoidAncestryChangedConn = humanoid.AncestryChanged:Connect(function(_, parent)
		if not parent then
			terminate()
		end
	end)

	rootPartAncestryChangedConn = rootPart.AncestryChanged:Connect(function(_, parent)
		if not parent then
			terminate()
		end
	end)

	characterAddedConn = player.CharacterAdded:Connect(terminate)
end

local function playerAdded(player: Player)
	local function characterAdded(character)
		-- Avoiding memory leaks in the face of Character/Humanoid/RootPart lifetime has a few complications:
		-- * character deparenting is a Remove instead of a Destroy, so signals are not cleaned up automatically.
		-- ** must use a waitForFirst on everything and listen for hierarchy changes.
		-- * the character might not be in the dm by the time CharacterAdded fires
		-- ** constantly check consistency with player.Character and abort if CharacterAdded is fired again
		-- * Humanoid may not exist immediately, and by the time it's inserted the character might be deparented.
		-- * RootPart probably won't exist immediately.
		-- ** by the time RootPart is inserted and Humanoid.RootPart is set, the character or the humanoid might be deparented.

		if not character.Parent then
			waitForFirst(character.AncestryChanged, player.CharacterAdded)
		end

		if player.Character ~= character or not character.Parent then
			return
		end

		local humanoid = character:FindFirstChildOfClass("Humanoid")
		while character:IsDescendantOf(game) and not humanoid do
			waitForFirst(character.ChildAdded, character.AncestryChanged, player.CharacterAdded)
			humanoid = character:FindFirstChildOfClass("Humanoid")
		end

		if player.Character ~= character or not character:IsDescendantOf(game) then
			return
		end

		-- must rely on HumanoidRootPart naming because Humanoid.RootPart does not fire changed signals
		local rootPart = character:FindFirstChild("HumanoidRootPart")
		while character:IsDescendantOf(game) and not rootPart do
			waitForFirst(character.ChildAdded, character.AncestryChanged, humanoid.AncestryChanged, player.CharacterAdded)
			rootPart = character:FindFirstChild("HumanoidRootPart")
		end

		if rootPart and humanoid:IsDescendantOf(game) and character:IsDescendantOf(game) and player.Character == character then
			initializeSoundSystem(player, humanoid, rootPart)
		end
	end

	if player.Character then
		characterAdded(player.Character)
	end
	player.CharacterAdded:Connect(characterAdded)
end

Players.PlayerAdded:Connect(playerAdded)
for _, player in ipairs(Players:GetPlayers()) do
	playerAdded(player)
end

Test Place

sound_sync.rbxl (44.8 KB)

93 Likes

This can really improve certain games that require different movement speed and can change at anytime. It would keep the experience pretty natural.

Thanks!

4 Likes

Thank you this is very useful.

4 Likes

Small detail but adds a lot! I really like it!

Great resource, I was actually thinking about re-scripting this myself. Roblox needs to re-code the default system as it’s outdated, but for now this will do! ^-^

2 Likes

I believe this was the case back in 2014, where the walking sound’s pitch changed depending on the Humanoid’s WalkSpeed. Anyway, I have a footstep script that uses Raycasting, and it could help you out.

2 Likes

your system is over-complicated, and can be simplified to 17 lines:

local Character = script.Parent
local Humanoid = Character:WaitForChild("Humanoid")
local RootPart = Character:WaitForChild("HumanoidRootPart")

local DefaultSpeed = game:GetService("StarterPlayer").CharacterWalkSpeed

local RunningDivisor = DefaultSpeed / 1.85
local SwimmingDivisor = DefaultSpeed / 1.6
local ClimbingDivisor = DefaultSpeed / 1

Humanoid:GetPropertyChangedSignal("WalkSpeed"):Connect(function()
	local WalkSpeed = Humanoid.WalkSpeed

	RootPart.Running.PlaybackSpeed = WalkSpeed / RunningDivisor
	RootPart.Swimming.PlaybackSpeed = WalkSpeed / SwimmingDivisor
	RootPart.Climbing.PlaybackSpeed = WalkSpeed / ClimbingDivisor
end)

it also doesn’t need to be named “RbxCharacterSounds”, nor does it need to be inside of a player.

you’re wasting too much space with your “fancy” variables, useless table entires and incosinstent variable name styling.

12 Likes

hey,
thanks for the criticism but you seem confused

this is a default roblox sound system not mine, and yeah my actual code is around that 17 line that you just wrote

not if you make a custom script but mine is a fork of the default roblox one, so it won’t work otherwise.

I believe you’re referring to the roblox code which I also don’t like, they use deprecated properties/functions and is pretty ugly overall. my code follows the principles of the Lua style guide

5 Likes

so why fork it while you could just dodge all of the hassle and make a custom one?

I don’t like having a lot of tiny scripts scattered around the game, it’s especially bad if a lot of those scripts affect the same thing - character sounds in this case. It’s easier to manage when you handle every part of a specific system in a single script.

3 Likes

it’s not the best to keep everything in a single script, it’s a common mistake people make and it makes it hell to develop more systems.

true
if your script becomes too hard to read/manage you need to split it using ModuleScripts.

by single script I meant a single (Local)Script for each system, not counting modules as they don’t make the management harder, creating another script is what causes issues because if you ever need communication between those two you’ll have to use BindableEvents or another module that could’ve easily been avoided.

anyways, this was a quick contribution in an attempt to improve the default roblox sound script. I don’t want to keep discussing best practices :sweat_smile:

2 Likes

how do I add multiple footsteps sounds to ur script without breaking it and Great job? :+1:

It’s not really overly-complicated in my opinion.

I took a look and it seems pretty good, raycasting down from the character foot is what I use as well for movement particles/dust and other effects.
thanks for sharing!

You’re welcome; I suggest using 1.5-2 for the raycasting distance, seeing as using smaller values will detect more footsteps, though will cause spamming.

3 Likes

Hey man sorry to knock on an old post here, but anytime I try to put a custom sound instead of the default sound, I’m returned with a error stating. that Roblox couldn’t download the sound data, and the id is correct, but when I return it to the default sound it works as normal. would know of any reason why this is happening?

have you tried playing the sound in the explorer without running the game? try pasting the ID in the Sound instance and see if that gives you the same error.

DM me if you need more help

Here is a newer version with custom footsteps:

--!nonstrict
-- Roblox character sound script

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local AtomicBinding = require(script:WaitForChild("AtomicBinding"))

local function loadFlag(flag: string)
	local success, result = pcall(function()
		return UserSettings():IsUserFeatureEnabled(flag)
	end)
	return success and result
end

local SOUND_DATA : { [string]: {[string]: any}} = {
	Climbing = {
		SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3",
		Looped = true,
		Pitch = 1,
	},
	Died = {
		SoundId = "rbxasset://sounds/uuhhh.mp3",
	},
	FreeFalling = {
		SoundId = "rbxasset://sounds/action_falling.mp3",
		Looped = true,
	},
	GettingUp = {
		SoundId = "rbxasset://sounds/action_get_up.mp3",
	},
	Jumping = {
		SoundId = "rbxassetid://2452829528",
		Volume = .03,
	},
	Landing = {
		SoundId = "rbxasset://sounds/action_jump_land.mp3",
	},
	Running = {
		SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3",
		Looped = true,
		Pitch = 1.85,
	},
	Splash = {
		SoundId = "rbxasset://sounds/impact_water.mp3",
	},
	Swimming = {
		SoundId = "rbxasset://sounds/action_swim.mp3",
		Looped = true,
		Pitch = 1.6,
	},
}

local DEFAULT_RUN_SOUND_SPEED = SOUND_DATA.Running.Pitch
local DEFAULT_SWIM_SOUND_SPEED = SOUND_DATA.Swimming.Pitch
local DEFAULT_CLIMB_SOUND_SPEED = SOUND_DATA.Climbing.Pitch

local DEFAULT_WALKSPEED = 16
local RUNNING_DIVISOR = DEFAULT_WALKSPEED / DEFAULT_RUN_SOUND_SPEED
local SWIMMING_DIVISOR = DEFAULT_WALKSPEED / DEFAULT_SWIM_SOUND_SPEED
local CLIMBING_DIVISOR = DEFAULT_WALKSPEED / DEFAULT_CLIMB_SOUND_SPEED

local MaterialTable = {
	[Enum.Material.Plastic] = 315915457;
	[Enum.Material.Metal] = 5639245164;
	[Enum.Material.Granite] = 3444211679;
	[Enum.Material.Fabric] = 133705377;
	[Enum.Material.Concrete] = 5761648082;
	[Enum.Material.Grass] = 344063420;
	[Enum.Material.Ground] = 9083822528;
	[Enum.Material.Sand] = 619188333;
	[Enum.Material.WoodPlanks] = 9083826864;
	[Enum.Material.Wood] = 9083826864
}
local MaterialVol = {
	[Enum.Material.Metal] = .05;
	[Enum.Material.Granite] = .05;
	[Enum.Material.Fabric] = .05;
	[Enum.Material.Concrete] = .05;
	[Enum.Material.Grass] = .25;
	[Enum.Material.Ground] = .35;
	[Enum.Material.Sand] = .65;
	[Enum.Material.WoodPlanks] = .25;
	[Enum.Material.Wood] = .25
}


-- wait for the first of the passed signals to fire
local function waitForFirst(...) -- RBXScriptSignal
	local shunt: BindableEvent = Instance.new("BindableEvent")
	local slots = {...}

	local function fire(...)
		for i = 1, #slots do
			slots[i]:Disconnect()
		end

		return shunt:Fire(...)
	end

	for i = 1, #slots do -- RBXScriptSignal
		slots[i] = slots[i]:Connect(fire) -- Change to RBXScriptConnection
	end

	return shunt.Event:Wait()
end

-- map a value from one range to another
local function map(x: number, inMin: number, inMax: number, outMin: number, outMax: number): number
	return (x - inMin)*(outMax - outMin)/(inMax - inMin) + outMin
end

local function playSound(sound: Sound)
	sound.TimePosition = 0
	sound.Playing = true
end

local function shallowCopy(t)
	local out = {}
	for k, v in pairs(t) do
		out[k] = v
	end
	return out
end

local function initializeSoundSystem(instances)
	local player = instances.player
	local humanoid = instances.humanoid
	local rootPart = instances.rootPart

	local sounds: {[string]: Sound} = {}

	-- initialize sounds
	for name: string, props: {[string]: any} in pairs(SOUND_DATA) do
		local sound: Sound = Instance.new("Sound")
		sound.Name = name

		-- set default values
		sound.Archivable = false
		sound.RollOffMinDistance = 5
		sound.RollOffMaxDistance = 150
		sound.Volume = 0.65

		for propName, propValue: any in pairs(props) do
			(sound :: any)[propName] = propValue
		end

		sound.Parent = rootPart
		sounds[name] = sound
	end

	local playingLoopedSounds: {[Sound]: boolean?} = {}

	local function stopPlayingLoopedSounds(except: Sound?)
		for sound in pairs(shallowCopy(playingLoopedSounds)) do
			if sound ~= except then
				sound.Playing = false
				playingLoopedSounds[sound] = nil
			end
		end
	end

	-- state transition callbacks.
	local stateTransitions: {[Enum.HumanoidStateType]: () -> ()} = {
		[Enum.HumanoidStateType.FallingDown] = function()
			stopPlayingLoopedSounds()
		end,

		[Enum.HumanoidStateType.GettingUp] = function()
			stopPlayingLoopedSounds()
			playSound(sounds.GettingUp)
		end,

		[Enum.HumanoidStateType.Jumping] = function()
			stopPlayingLoopedSounds()
			playSound(sounds.Jumping)
		end,

		[Enum.HumanoidStateType.Swimming] = function()
			local verticalSpeed = math.abs(rootPart.AssemblyLinearVelocity.Y)
			if verticalSpeed > 0.1 then
				sounds.Splash.Volume = math.clamp(map(verticalSpeed, 100, 350, 0.28, 1), 0, 1)
				playSound(sounds.Splash)
			end
			stopPlayingLoopedSounds(sounds.Swimming)
			sounds.Swimming.Playing = true
			playingLoopedSounds[sounds.Swimming] = true
		end,

		[Enum.HumanoidStateType.Freefall] = function()
			sounds.FreeFalling.Volume = 0
			stopPlayingLoopedSounds(sounds.FreeFalling)
			playingLoopedSounds[sounds.FreeFalling] = true
		end,

		[Enum.HumanoidStateType.Landed] = function()
			stopPlayingLoopedSounds()
			local verticalSpeed = math.abs(rootPart.AssemblyLinearVelocity.Y)
			if verticalSpeed > 75 then
				sounds.Landing.Volume = math.clamp(map(verticalSpeed, 50, 100, 0, 1), 0, 1)
				playSound(sounds.Landing)
			end
		end,

		[Enum.HumanoidStateType.Running] = function()
			stopPlayingLoopedSounds(sounds.Running)
			sounds.Running.Playing = true
			playingLoopedSounds[sounds.Running] = true
		end,

		[Enum.HumanoidStateType.Climbing] = function()
			local sound = sounds.Climbing
			if math.abs(rootPart.AssemblyLinearVelocity.Y) > 0.1 then
				sound.Playing = true
				stopPlayingLoopedSounds(sound)
			else
				stopPlayingLoopedSounds()
			end
			playingLoopedSounds[sound] = true
		end,

		[Enum.HumanoidStateType.Seated] = function()
			stopPlayingLoopedSounds()
		end,

		[Enum.HumanoidStateType.Dead] = function()
			stopPlayingLoopedSounds()
			playSound(sounds.Died)
		end,
	}

	-- updaters for looped sounds
	local loopedSoundUpdaters: {[Sound]: (number, Sound, Vector3) -> ()} = {
		[sounds.Climbing] = function(dt: number, sound: Sound, vel: Vector3)
			sound.Playing = vel.Magnitude > 0.1
		end,

		[sounds.FreeFalling] = function(dt: number, sound: Sound, vel: Vector3): ()
			if vel.Magnitude > 75 then
				sound.Volume = math.clamp(sound.Volume + 0.9*dt, 0, 1)
			else
				sound.Volume = 0
			end
		end,

		[sounds.Running] = function(dt: number, sound: Sound, vel: Vector3)
			sound.Playing = vel.Magnitude > 0.5 and humanoid.MoveDirection.Magnitude > 0.5
		end,
	}

	-- state substitutions to avoid duplicating entries in the state table
	local stateRemap: {[Enum.HumanoidStateType]: Enum.HumanoidStateType} = {
		[Enum.HumanoidStateType.RunningNoPhysics] = Enum.HumanoidStateType.Running,
	}

	local activeState: Enum.HumanoidStateType = stateRemap[humanoid:GetState()] or humanoid:GetState()

	local function transitionTo(state)
		local transitionFunc: () -> () = stateTransitions[state]

		if transitionFunc then
			transitionFunc()
		end

		activeState = state
	end

	transitionTo(activeState)

	local stateChangedConn = humanoid.StateChanged:Connect(function(_, state)
		state = stateRemap[state] or state

		if state ~= activeState then
			transitionTo(state)
		end
	end)

	local steppedConn = RunService.Stepped:Connect(function(_, worldDt: number)
		-- update looped sounds on stepped
		for sound in pairs(playingLoopedSounds) do
			local updater: (number, Sound, Vector3) -> () = loopedSoundUpdaters[sound]

			if updater then
				updater(worldDt, sound, rootPart.AssemblyLinearVelocity)
			end
		end
	end)
	local function adjustPlaybackSpeed()
		local walkSpeed = humanoid.WalkSpeed

		sounds.Running.PlaybackSpeed = walkSpeed / RUNNING_DIVISOR
		sounds.Swimming.PlaybackSpeed = walkSpeed / SWIMMING_DIVISOR
		sounds.Climbing.PlaybackSpeed = walkSpeed / CLIMBING_DIVISOR
	end
	local function setRunSound()
		local FloorMaterial = humanoid.FloorMaterial
		if MaterialTable[FloorMaterial] then
			sounds.Running.SoundId = "rbxassetid://"..MaterialTable[FloorMaterial]
			sounds.Running.Volume = MaterialVol[FloorMaterial]
		else
			sounds.Running.SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3"
			sounds.Running.Volume = .35
		end
	end
	
	adjustPlaybackSpeed()
	local speedChangedConn = humanoid:GetPropertyChangedSignal('WalkSpeed'):Connect(adjustPlaybackSpeed)
	local speedChangedConn = humanoid:GetPropertyChangedSignal('FloorMaterial'):Connect(setRunSound)
	local function terminate()
		stateChangedConn:Disconnect()
		steppedConn:Disconnect()

		-- Unparent all sounds and empty sounds table
		-- This is needed in order to support the case where initializeSoundSystem might be called more than once for the same player,
		-- which might happen in case player character is unparented and parented back on server and reset-children mechanism is active.
		for name: string, sound: Sound in pairs(sounds) do
			sound:Destroy()
		end
		table.clear(sounds)
	end

	return terminate
end

local binding = AtomicBinding.new({
	humanoid = "Humanoid",
	rootPart = "HumanoidRootPart",
}, initializeSoundSystem)

local playerConnections = {}

local function characterAdded(character)
	binding:bindRoot(character)
end

local function characterRemoving(character)
	binding:unbindRoot(character)
end

local function playerAdded(player: Player)
	local connections = playerConnections[player]
	if not connections then
		connections = {}
		playerConnections[player] = connections
	end

	if player.Character then
		characterAdded(player.Character)
	end
	table.insert(connections, player.CharacterAdded:Connect(characterAdded))
	table.insert(connections, player.CharacterRemoving:Connect(characterRemoving))
end

local function playerRemoving(player: Player)
	local connections = playerConnections[player]
	if connections then
		for _, conn in ipairs(connections) do
			conn:Disconnect()
		end
		playerConnections[player] = nil
	end

	if player.Character then
		characterRemoving(player.Character)
	end
end

for _, player in ipairs(Players:GetPlayers()) do
	task.spawn(playerAdded, player)
end
Players.PlayerAdded:Connect(playerAdded)
Players.PlayerRemoving:Connect(playerRemoving)
2 Likes

A simpler, more precise solution to all of this is just to insert a named keyframe/marker in your sprinting/walking animation and then play a sound every time that keyframe is reached. This is a surefire way to 100% ensure synced footstep sounds.

2 Likes