I’m trying to make it so that footstep sounds are synced with the running animation, via animation events. To do this, I’ve forked the default sound script, and modified it.
The problem is, the script just ignores the animation events, and uses the default method.
The reason I don’t just modify the sound effect speed, is that doing so causes the footsteps to be too fast at higher movement speeds, and certain footstep loops are out of sync with the default footstep loop.
I’ve looked all over the DevHub, and found no fixes.
The script:
--!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://2816517541",
Volume = .03,
},
Landing = {
SoundId = "rbxasset://sounds/action_jump_land.mp3",
},
Running = {
SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3",
Looped = false,
Pitch = 1.85,
},
Splash = {
SoundId = "rbxasset://sounds/impact_water.mp3",
},
Swimming = {
SoundId = "rbxasset://sounds/action_swim.mp3",
Looped = true,
Pitch = 1.6,
},
}
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()
local tracks = humanoid:WaitForChild("Animator"):GetPlayingAnimationTracks()
stopPlayingLoopedSounds(sounds.Running)
if tracks["RunWithEvents"] ~= nil then
print("hes here bois")
if tracks["RunWithEvents"].IsPlaying == true then
print("It's playing")
tracks["RunWithEvents"]:GetMarkerReachedSignal("Footstep"):Connect(function()
print("played footstep")
playSound(sounds.Running)
end)
else
print("he's gone bois")
end
end
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 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
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)
Let me know if you need more information.