[SOLUTION] How to adapt Custom Footstep System to match player step time and WalkSpeed

I’m trying to match the Footstep to sync with the player step time (when the player moves each step) and the WalkSpeed.

Here’s the current System:
Server:

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

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

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

local walking = false
local volume = 0.65

local function SetupFootstep(ID, playbackSpeed)
	local sound = Instance.new("Sound")
	sound.Name = "Footstep"
	sound.SoundId = ID
	sound.Volume = volume
	sound.Pitch = 1
	sound.PlaybackSpeed = playbackSpeed
	sound.RollOffMode = Enum.RollOffMode.Linear 
	sound.RollOffMaxDistance = 100
	sound.RollOffMinDistance = 5
	sound.Parent = Character:WaitForChild("HumanoidRootPart")
	sound:Play()
end

Humanoid.Running:Connect(function(Speed)
	if Speed > 0 then
		walking = true
	else
		walking = false
	end
end)

while task.wait() do
	local time = 0.5

	if walking then
		local speed = Character.HumanoidRootPart.AssemblyLinearVelocity.Magnitude

		if speed > 1 then
			time = 500 / speed / 100 
		end

		local minSpeed = 0.5
		local maxSpeed = 0.8
		local minPlayback = 0.8
		local maxPlayback = 1.4

		local speedAlpha = (speed - minSpeed) / (maxSpeed - minSpeed)
		speedAlpha = math.clamp(speedAlpha, 0, 1) -- Clamp between 0 and 1

		-- Linearly interpolate to find the current playback speed
		local playbackSpeed = minPlayback + speedAlpha * (maxPlayback - minPlayback)

		local MaterialTable = FootstepModule:GetTableFromMaterial(Humanoid.FloorMaterial)
		if MaterialTable then
			local RandomFootstep = FootstepModule:GetRandomFootstep(MaterialTable)
			-- Pass the calculated playback speed when creating the footstep sound
			SetupFootstep(RandomFootstep, playbackSpeed)
		end
	end
	task.wait(time)
end

Module:

-- Types
export type SoundTable = {string}
export type SoundIds = {[string]: SoundTable}

local FOOTSTEP = {}

FOOTSTEP.SoundIds = {
	["Bass"] = {
		"rbxassetid://9126748907",
		"rbxassetid://9126748813",
		"rbxassetid://9126748580",
		"rbxassetid://9126748691",
		"rbxassetid://9126748431",
		"rbxassetid://9126748324",
		"rbxassetid://9126748239",
		"rbxassetid://9126748185",
		"rbxassetid://9126748045",
		"rbxassetid://9126747958"
	},
	
	["Carpet"] = {
		"rbxassetid://9126748130",
		"rbxassetid://9126747861",
		"rbxassetid://9126747720",
		"rbxassetid://9126747529",
		"rbxassetid://9126747412",
		"rbxassetid://9126747283",
		"rbxassetid://9126746732",
		"rbxassetid://9126746837",
		"rbxassetid://9126747132",
		"rbxassetid://9126746984",
		"rbxassetid://9126746598",
		"rbxassetid://9126746481",
		"rbxassetid://9126746371",
		"rbxassetid://9126746291"
	},
	
	["Concrete"] = {
		"rbxassetid://9126746167",
		"rbxassetid://9126746098",
		"rbxassetid://9126745995",
		"rbxassetid://9126745877",
		"rbxassetid://9126745774",
		"rbxassetid://9126745574",
		"rbxassetid://9126745336",
		"rbxassetid://9126745241",
		"rbxassetid://9126745445",
		"rbxassetid://9126745052",
		"rbxassetid://9126745141",
		"rbxassetid://9126745676",
		"rbxassetid://9126744969",
		"rbxassetid://9126744894",
		"rbxassetid://9126744639",
		"rbxassetid://9126744789",
		"rbxassetid://9126744481"
	},
	
	["Dirt"] = {
		"rbxassetid://9126744390",
		"rbxassetid://9126744718",
		"rbxassetid://9126744263",
		"rbxassetid://9126744157",
		"rbxassetid://9126744066",
		"rbxassetid://9126744009",
		"rbxassetid://9126743796",
		"rbxassetid://9126743938",
		"rbxassetid://9126743711",
		"rbxassetid://9126743879",
		"rbxassetid://9126743613",
		"rbxassetid://9126743481",
		"rbxassetid://9126743338",
		"rbxassetid://9126743086"
	},
	
	["Glass"] = {
		"rbxassetid://9126742971",
		"rbxassetid://9126742461",
		"rbxassetid://9126742875",
		"rbxassetid://9126742786",
		"rbxassetid://9126743193",
		"rbxassetid://9126742680",
		"rbxassetid://9126742582",
		"rbxassetid://9126742510"
	},
	
	["Grass"] = {
		"rbxassetid://9126742396",
		"rbxassetid://9126741427",
		"rbxassetid://9126742333",
		"rbxassetid://9126742215",
		"rbxassetid://9126742271",
		"rbxassetid://9126742031",
		"rbxassetid://9126741934",
		"rbxassetid://9126742105",
		"rbxassetid://9126741826",
		"rbxassetid://9126741594",
		"rbxassetid://9126741512",
		"rbxassetid://9126741741",
		"rbxassetid://9126741674"
	},
	
	["Gravel"] = {
		"rbxassetid://9126741273",
		"rbxassetid://9126740393",
		"rbxassetid://9126741200",
		"rbxassetid://9126741051",
		"rbxassetid://9126741128",
		"rbxassetid://9126740951",
		"rbxassetid://9126740802",
		"rbxassetid://9126740724",
		"rbxassetid://9126740524",
		"rbxassetid://9126740623"
	},
	
	["Ladder"] = {
		"rbxassetid://9126740217",
		"rbxassetid://9126739039",
		"rbxassetid://9126740133",
		"rbxassetid://9126739947",
		"rbxassetid://9126740044",
		"rbxassetid://9126740305",
		"rbxassetid://9126739834",
		"rbxassetid://9126739622",
		"rbxassetid://9126739505",
		"rbxassetid://9126739406",
		"rbxassetid://9126739332",
		"rbxassetid://9126739229"
	},
	
	["Metal_Auto"] = {
		"rbxassetid://9126739090",
		"rbxassetid://9126738967",
		"rbxassetid://9126738896",
		"rbxassetid://9126738732",
		"rbxassetid://9126738543",
		"rbxassetid://9126738634"
	},
	
	["Metal_Chainlink"] = {
		"rbxassetid://9126738423",
		"rbxassetid://9126737791",
		"rbxassetid://9126738338",
		"rbxassetid://9126738197",
		"rbxassetid://9126738113",
		"rbxassetid://9126738032",
		"rbxassetid://9126737943",
		"rbxassetid://9126737853"
	},
	
	["Metal_Grate"] = {
		"rbxassetid://9126737728",
		"rbxassetid://9126736554",
		"rbxassetid://9126737597",
		"rbxassetid://9126737668",
		"rbxassetid://9126737506",
		"rbxassetid://9126737412",
		"rbxassetid://9126737315",
		"rbxassetid://9126737212",
		"rbxassetid://9126736947",
		"rbxassetid://9126737081",
		"rbxassetid://9126736863",
		"rbxassetid://9126736806",
		"rbxassetid://9126736642",
		"rbxassetid://9126736721"
	},
	
	["Metal_Solid"] = {
		"rbxassetid://9126736470",
		"rbxassetid://9126734921",
		"rbxassetid://9126736274",
		"rbxassetid://9126736354",
		"rbxassetid://9126736186",
		"rbxassetid://9126736049",
		"rbxassetid://9126735913",
		"rbxassetid://9126735734",
		"rbxassetid://9126735546",
		"rbxassetid://9126735474",
		"rbxassetid://9126735265",
		"rbxassetid://9126735374",
		"rbxassetid://9126735161",
		"rbxassetid://9126735028",
		"rbxassetid://9126735089",
		"rbxassetid://9126734972"
	},
	
	["Mud"] = {
		"rbxassetid://9126734842",
		"rbxassetid://9126734314",
		"rbxassetid://9126734778",
		"rbxassetid://9126734710",
		"rbxassetid://9126734613",
		"rbxassetid://9126734499",
		"rbxassetid://9126734365",
		"rbxassetid://9126734432",
		"rbxassetid://9126734244"
	},
	
	["Rubber"] = {
		"rbxassetid://9126734172",
		"rbxassetid://9126733896",
		"rbxassetid://9126734560",
		"rbxassetid://9126734010",
		"rbxassetid://9126733324",
		"rbxassetid://9126733766",
		"rbxassetid://9126733614",
		"rbxassetid://9126733493"
	},
	
	["Sand"] = {
		"rbxassetid://9126733118",
		"rbxassetid://9126733408",
		"rbxassetid://9126733225",
		"rbxassetid://9126732675",
		"rbxassetid://9126732571",
		"rbxassetid://9126732962",
		"rbxassetid://9126732962",
		"rbxassetid://9126732457",
		"rbxassetid://9126732862",
		"rbxassetid://9126732776",
		"rbxassetid://9126732334",
		"rbxassetid://9126732253"
	},

	["Snow"] = {
		"rbxassetid://9126732128",
		"rbxassetid://9126731099",
		"rbxassetid://9126732016",
		"rbxassetid://9126731951",
		"rbxassetid://9126731877",
		"rbxassetid://9126731632",
		"rbxassetid://9126731493",
		"rbxassetid://9126731343",
		"rbxassetid://9126731790",
		"rbxassetid://9126731243",
		"rbxassetid://9126731169",
		"rbxassetid://9126730861"
	},
	
	["Tile"] = {
		"rbxassetid://9126730713",
		"rbxassetid://9126730782",
		"rbxassetid://9126731037",
		"rbxassetid://9126730980",
		"rbxassetid://9126730651",
		"rbxassetid://9126730563",
		"rbxassetid://9126730279",
		"rbxassetid://9126730403",
		"rbxassetid://9126730056",
		"rbxassetid://9126730172",
		"rbxassetid://9126729836",
		"rbxassetid://9126730472",
		"rbxassetid://9126729938",
		"rbxassetid://9126729706"
	},
	
	["Wood"] = {
		"rbxassetid://9126931624",
		"rbxassetid://9126931515",
		"rbxassetid://9126931417",
		"rbxassetid://9126931322",
		"rbxassetid://9126931699",
		"rbxassetid://9126931235",
		"rbxassetid://9126931169",
		"rbxassetid://9126931026",
		"rbxassetid://9126930953",
		"rbxassetid://9126930885",
		"rbxassetid://9126930789",
		"rbxassetid://9126930647",
		"rbxassetid://9126930516",
		"rbxassetid://9126930598",
		"rbxassetid://9126930718"
	}
}

FOOTSTEP.MaterialMap = {
	[Enum.Material.Slate] =         FOOTSTEP.SoundIds.Concrete,
	[Enum.Material.Concrete] =      FOOTSTEP.SoundIds.Concrete,
	[Enum.Material.Brick] =         FOOTSTEP.SoundIds.Concrete,
	[Enum.Material.Cobblestone] =   FOOTSTEP.SoundIds.Concrete,
	[Enum.Material.Sandstone] =     FOOTSTEP.SoundIds.Concrete,
	[Enum.Material.Rock] =          FOOTSTEP.SoundIds.Concrete,
	[Enum.Material.Basalt] =        FOOTSTEP.SoundIds.Concrete,
	[Enum.Material.CrackedLava] =   FOOTSTEP.SoundIds.Concrete,
	[Enum.Material.Asphalt] =       FOOTSTEP.SoundIds.Concrete,
	[Enum.Material.Limestone] =     FOOTSTEP.SoundIds.Concrete,
	[Enum.Material.Pavement] =      FOOTSTEP.SoundIds.Concrete,
	[Enum.Material.Plastic] =       FOOTSTEP.SoundIds.Tile,
	[Enum.Material.Marble] =        FOOTSTEP.SoundIds.Tile,
	[Enum.Material.Neon] =          FOOTSTEP.SoundIds.Tile,
	[Enum.Material.Granite] =       FOOTSTEP.SoundIds.Tile,
	[Enum.Material.Wood] =          FOOTSTEP.SoundIds.Wood,
	[Enum.Material.WoodPlanks] =    FOOTSTEP.SoundIds.Wood,
	[Enum.Material.Water] =         FOOTSTEP.SoundIds.Glass,
	[Enum.Material.CorrodedMetal] = FOOTSTEP.SoundIds.Metal_Solid,
	[Enum.Material.DiamondPlate] =  FOOTSTEP.SoundIds.Metal_Solid,
	[Enum.Material.Metal] =         FOOTSTEP.SoundIds.Metal_Solid,	
	[Enum.Material.Foil] =          FOOTSTEP.SoundIds.Metal_Grate,
	[Enum.Material.Ground] =        FOOTSTEP.SoundIds.Dirt,
	[Enum.Material.Grass] =         FOOTSTEP.SoundIds.Grass,
	[Enum.Material.LeafyGrass] =    FOOTSTEP.SoundIds.Grass,	
	[Enum.Material.Fabric] =        FOOTSTEP.SoundIds.Carpet,
	[Enum.Material.Pebble] =        FOOTSTEP.SoundIds.Gravel,	
	[Enum.Material.Snow] =          FOOTSTEP.SoundIds.Snow,
	[Enum.Material.Sand] =          FOOTSTEP.SoundIds.Sand,
	[Enum.Material.Salt] =          FOOTSTEP.SoundIds.Sand,
	[Enum.Material.Ice] =           FOOTSTEP.SoundIds.Glass,
	[Enum.Material.Glacier] =       FOOTSTEP.SoundIds.Glass,
	[Enum.Material.Glass] =         FOOTSTEP.SoundIds.Glass,
	[Enum.Material.SmoothPlastic] = FOOTSTEP.SoundIds.Rubber,
	[Enum.Material.ForceField] =    FOOTSTEP.SoundIds.Rubber,
	[Enum.Material.Mud] =           FOOTSTEP.SoundIds.Mud
}

-- This function produces a folder under a specified parent.
function FOOTSTEP:CreateSoundGroup(parent:Instance?, name:string?, soundProperties:{string:any}?, isFolder:boolean?) : SoundGroup|Folder
	if not parent then warn("Parent not specified, Footstep folder parented to workspace") end
	isFolder = isFolder or false
	local soundProperties = soundProperties or {}
	parent = parent or workspace

	local SoundGroup = nil
	if not isFolder then
		SoundGroup = Instance.new("SoundGroup")
		SoundGroup.Volume = 1
		SoundGroup.Name = name or "Footsteps"
	else
		SoundGroup = Instance.new("Folder")
		SoundGroup.Name = name or "Footsteps"
	end

	for soundMaterial, soundList in pairs(FOOTSTEP.SoundIds) do
		local sectionGroup = nil
		if not isFolder then
			sectionGroup = Instance.new("SoundGroup")
			sectionGroup.Volume = 1
			sectionGroup.Name = soundMaterial
		else
			sectionGroup = Instance.new("Folder")
			sectionGroup.Name = soundMaterial
		end

		for i, soundId in ipairs(soundList) do
			local soundEffect = Instance.new("Sound")
			soundEffect.Name = string.format("%s_%02i", soundMaterial:lower(), i)
			if not isFolder then
				soundEffect.SoundGroup = sectionGroup
			end
			for property, value in pairs(soundProperties) do
				soundEffect[property] = value
			end
			soundEffect.SoundId = soundId
			soundEffect.Parent = sectionGroup
		end
		sectionGroup.Parent = SoundGroup
	end

	SoundGroup.Parent = parent
	return SoundGroup
end

-- Gets a sound list table from the material
function FOOTSTEP:GetTableFromMaterial(EnumItem: Enum.Material | string) : { [string]: {string} }
	if typeof(EnumItem) == "string" then
		EnumItem = Enum.Material[EnumItem]
	end
	return FOOTSTEP.MaterialMap[EnumItem]
end

-- Picks a random footstep sound from the given table
function FOOTSTEP:GetRandomFootstep(SoundTable: {string}) : string
	return SoundTable[math.random(#SoundTable)]
end

return FOOTSTEP
2 Likes


Do you see how the Footsteps aren’t matching each time the player steps?

1 Like

Hey there, I actually just coded one of these recently. You can just use .Touched on the left foot and right foot and use a debounce that waits for the left and right (so its a flipper, left touches and then it waits for the right to touch, then flips it back listening to the other). I implemented that logic and it works flawlessly, no need to worry about WalkSpeed or animations. Just ensure its not checking anything outside of the humanoid being in the running state.

2 Likes

Do you think you could share an Example of your System?

I too just coded one recently (a week or two ago).

Alternatively, you can just tinker with the playback speed of the sound until it matches up nicely with any given WalkSpeed and then figure out the correct scaling based on the speed of the character or animation (that’s what I did for Castaway).

480p video of footsteps in my game with no other sfx/music on:

Personally tried it out myself out of curiosity, and this worked. Do keep in mind that it also depends on your walking animation. If the animation you have makes your feet make contact with the ground more than once in every strike, the sounds may bug out.

local client = game:GetService("Players").LocalPlayer
local character = client.Character

local LFoot:BasePart = character:WaitForChild("LeftFoot")
local RFoot:BasePart = character:WaitForChild("RightFoot")

local LDebounce = false
local RDebounce = false

local sfx = game:GetService("SoundService").Footstep
local humanoid: Humanoid = character:WaitForChild("Humanoid")

LFoot.Touched:Connect(function(part)
	if humanoid:GetState() ~= Enum.HumanoidStateType.Running then return end
	
	if LDebounce then return
	end
	LDebounce = true
	RDebounce = false
	sfx:Play()
end)

RFoot.Touched:Connect(function(part)
	if humanoid:GetState() ~= Enum.HumanoidStateType.Running then return end
	if RDebounce then return end
	RDebounce = true
	LDebounce = false
	sfx:Play()
end)

heres the entire script lol

note that this script is also a sprint/walk script cuz i combined them but you can just read whats relevant

--[[
    Footstep on Touch + Foot Hitboxes
    LocalScript in StarterPlayerScripts
    
    mewow! 7/6/2025
]]

-- Constants (tweak these)
local DefaultWalkSpeed       = 16
local WalkSpeed              = 5
local SprintSpeed            = 24
local DefaultFOV             = 70
local SprintFOV              = 80
local FOVTweenTime           = 0.2

local FootstepVolume         = 0.5
local DefaultFootstepSoundId = "rbxassetid://74510715852116"
local FootDebounceTime       = 0.1   -- seconds per foot

-- How far below the foot and how narrow the hitbox is
local HitboxOffsetY          = 0.05  -- studs below the bottom of the foot
local HitboxHeight           = 0.1   -- thickness of the hitbox
local HitboxScaleXZ          = 0.6   -- scale of the hitbox relative to foot X/Z

-- Map each Material to an array of SoundIds
local FootstepSoundsMap = {
	[Enum.Material.Grass]      = {"rbxassetid://18640165054","rbxassetid://120323715170091"},
	[Enum.Material.LeafyGrass] = {"rbxassetid://18640165054","rbxassetid://120323715170091"},
	[Enum.Material.Concrete]   = {"rbxassetid://131139887478412","rbxassetid://135768413719763"},
	[Enum.Material.Wood]       = {"rbxassetid://85045757933968"},
	[Enum.Material.WoodPlanks] = {"rbxassetid://85045757933968","rbxassetid://102384199436643"},
	
}

-- Services
local Players = game:GetService("Players")
local ContextActionService = game:GetService("ContextActionService")
local TweenService = game:GetService("TweenService")
local ContentProvider  = game:GetService("ContentProvider")
local SoundService  = game:GetService("SoundService")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- modules
local ClientNetworking = require(ReplicatedStorage:WaitForChild("ClientNetworking"))

-- State
local player      = Players.LocalPlayer
local camera      = workspace.CurrentCamera
local humanoid, rootPart, leftFoot, rightFoot
local leftHitbox, rightHitbox
local walking, sprinting = false, false

-- Footstep cache
local CacheFootstepSoundName = "FootstepCache"
local cacheFolder
local soundCache = {}  -- [soundId] = Sound instance

-- Next foot to play
local nextFoot = "Left"
local footDebounce = { Left = false, Right = false }

-- Connection storage for cleanup
local cons = {}

-- Helpers

local function UpdateSpeed()
	if sprinting then
		humanoid.WalkSpeed = SprintSpeed
	elseif walking then
		humanoid.WalkSpeed = WalkSpeed
	else
		humanoid.WalkSpeed = DefaultWalkSpeed
	end
end

local function UpdateFOV()
	local targetFOV = sprinting and SprintFOV or DefaultFOV
	TweenService:Create(camera, TweenInfo.new(FOVTweenTime), {
		FieldOfView = targetFOV
	}):Play()
end

local function playFootstep()
	local mat = humanoid.FloorMaterial
	local ids = FootstepSoundsMap[mat]
	local sid = (ids and #ids > 0 and ids[math.random(#ids)]) or DefaultFootstepSoundId
	local base = soundCache[sid]
	if base then
		local s = base:Clone()
		s.Parent        = rootPart
		s.PlaybackSpeed = math.random(90,110)/100
		s:Play()
		s.Ended:Connect(function() s:Destroy() end)
		ClientNetworking.Fire("Footsteps", sid) -- networking replication
	end
end

local function playOtherFootstep(otherRoot : BasePart, sid : string) -- plays when another player steps
	local base = soundCache[sid]
	if base then
		local s = base:Clone()
		s.Parent        = otherRoot 
		s.PlaybackSpeed = math.random(90,110)/100
		s:Play()
		s.Ended:Connect(function() s:Destroy() end)
	end
end

local function buildSoundCache()
	local toCache = { [DefaultFootstepSoundId] = true }
	-- gather all IDs
	for _, list in FootstepSoundsMap do
		for _, id in list do
			toCache[id] = true
		end
	end
	-- create cache
	for sid, _ in toCache do
		local snd = Instance.new("Sound")
		snd.Name    = CacheFootstepSoundName
		snd.SoundId = sid
		snd.Volume  = FootstepVolume
		snd.Parent  = cacheFolder
		ContentProvider:PreloadAsync({ snd })
		soundCache[sid] = snd
	end
end

-- Foot touch handler
local function onFootTouched(footName, otherPart)
	if nextFoot ~= footName or footDebounce[footName] then
		return
	end
	if otherPart:IsDescendantOf(player.Character) then
		return
	end
	if humanoid.MoveDirection.Magnitude <= 0.1 then
		return
	end
	if humanoid:GetState() == Enum.HumanoidStateType.Freefall then
		return
	end
	
	playFootstep()
	
	footDebounce[footName] = true
	task.delay(FootDebounceTime, function()
		footDebounce[footName] = false
	end)
	
	nextFoot = (footName == "Left") and "Right" or "Left"
end

-- Create and weld a hitbox under a foot
local function createFootHitbox(foot, footName)
	-- cleanup old
	if footName == "Left" and leftHitbox then
		leftHitbox:Destroy()
	elseif footName == "Right" and rightHitbox then
		rightHitbox:Destroy()
	end
	
	local size = foot.Size
	local hx, hy, hz = size.X * HitboxScaleXZ, HitboxHeight, size.Z * HitboxScaleXZ
	local hitbox = Instance.new("Part")
	hitbox.Name         = footName.."Hitbox"
	hitbox.Size         = Vector3.new(hx, hy, hz)
	hitbox.Transparency = 1
	hitbox.CanCollide   = false
	hitbox.CanQuery     = false
	hitbox.CanTouch     = true
	hitbox.Massless     = true
	hitbox.Parent       = foot
	
	local offsetY = -(size.Y/2 + hy/2 + HitboxOffsetY)
	hitbox.CFrame = foot.CFrame * CFrame.new(0, offsetY, 0)
	
	local weld = Instance.new("WeldConstraint")
	weld.Part0 = foot
	weld.Part1 = hitbox
	weld.Parent = hitbox
	
	table.insert(cons, hitbox.Touched:Connect(function(other)
		onFootTouched(footName, other)
	end))
	
	if footName == "Left" then
		leftHitbox = hitbox
	else
		rightHitbox = hitbox
	end
end

-- Clear all connections
local function clearConnections()
	for _, con in cons do
		con:Disconnect()
	end
	cons = {}
end

-- Input actions
ContextActionService:BindAction("ToggleWalk", function(_, state)
	if state == Enum.UserInputState.Begin then
		walking = not walking
		if not walking and sprinting then
			sprinting = false
			UpdateFOV()
		end
		UpdateSpeed()
	end
end, false, Enum.KeyCode.LeftControl)

ContextActionService:BindAction("ToggleSprint", function(_, state)
	if state == Enum.UserInputState.Begin then
		sprinting = not sprinting
		UpdateSpeed()
		UpdateFOV()
	end
end, false, Enum.KeyCode.LeftShift)

-- Character setup
local function OnCharacterAdded(char)
	clearConnections()
	
	humanoid  = char:WaitForChild("Humanoid")
	rootPart  = char:WaitForChild("HumanoidRootPart")
	leftFoot  = char:WaitForChild("LeftFoot")
	rightFoot = char:WaitForChild("RightFoot")
	
	-- disable default sounds
	repeat task.wait() until rootPart:FindFirstChildOfClass("Sound")
	for _, snd in rootPart:GetDescendants() do
		if not snd then continue end
		if snd:IsA("Sound") then
			snd.Volume = 0
		end
	end
	
	-- build cache
	if cacheFolder then cacheFolder:Destroy() end
	cacheFolder = Instance.new("Folder", rootPart)
	cacheFolder.Name = "FootstepCacheFolder"
	soundCache = {}
	buildSoundCache()
	
	-- create hitboxes
	createFootHitbox(leftFoot,  "Left")
	createFootHitbox(rightFoot, "Right")
	
	-- init
	nextFoot = "Left"
	footDebounce.Left, footDebounce.Right = false, false
	UpdateSpeed()
	UpdateFOV()
	
	humanoid.ApplyDescriptionFinished:Once(function()
		OnCharacterAdded(char)
	end)
	
end

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

-- server connections

ClientNetworking.Connect("Footsteps", function(otherPlayer : Player, sid : string)
	if otherPlayer == player then
		return
	end
	local char = otherPlayer.Character
	if char.PrimaryPart then
		-- means they're streamed in
		playOtherFootstep(char.PrimaryPart, sid)
	end
end)
1 Like

why not try something like this

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

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

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

local walking = false
local volume = 0.65
local stepLength = 2 -- adjust to match player scale

local footstepTimer = 0
local footstepInterval = 0.5

local function SetupFootstep(ID, playbackSpeed)
	local sound = Instance.new("Sound")
	sound.Name = "Footstep"
	sound.SoundId = ID
	sound.Volume = volume
	sound.Pitch = 1
	sound.PlaybackSpeed = playbackSpeed
	sound.RollOffMode = Enum.RollOffMode.Linear
	sound.RollOffMaxDistance = 100
	sound.RollOffMinDistance = 5
	sound.Parent = Character:WaitForChild("HumanoidRootPart")
	sound:Play()
end

Humanoid.Running:Connect(function(Speed)
	walking = Speed > 0
end)

RunService.Heartbeat:Connect(function(dt)
	if walking then
		local speed = Humanoid.WalkSpeed
		if speed < 0.1 then return end -- avoid division by zero

		footstepInterval = stepLength / speed

		footstepTimer = footstepTimer + dt
		if footstepTimer >= footstepInterval then
			footstepTimer = 0

			local playbackSpeed = math.clamp(speed / 16, 0.8, 1.4) -- scale playback speed (16 is default WalkSpeed)

			local MaterialTable = FootstepModule:GetTableFromMaterial(Humanoid.FloorMaterial)
			if MaterialTable then
				local RandomFootstep = FootstepModule:GetRandomFootstep(MaterialTable)
				SetupFootstep(RandomFootstep, playbackSpeed)
			end
		end
	else
		footstepTimer = 0
	end
end)
2 Likes

Thanks to @SubTo_Lurrz I manage to fix the issue with my own additions applied to the Footstep System!

--// SERVICES
local RunService = game:GetService("RunService")

--// MODULES
local FOOTSTEP = require(script.Footstep)

--// PLAYER & CHARACTER
local Character = script.Parent
local Humanoid = Character:WaitForChild("Humanoid")
local HRP = Character:WaitForChild("HumanoidRootPart")

--// FOOTSTEP CONFIGURATION
local walking = false
local volume = 0.65
local stepLength = 3

--// FOOTSTEP TIMERS
local footstepTimer = 0
local footstepInterval = 0.5

--// FOOTSTEP VARIABLES
local currentFootstepSound = nil

--// FOOTSTEP INCREASE/DECREASE SPEED LOGIC
local function FootstepSpeed()
	local speed = HRP.AssemblyLinearVelocity.Magnitude
	local minSpeed = 6
	local maxSpeed = 10
	local minPlayback = 0.8
	local maxPlayback = 1.0

	local alpha = (speed - minSpeed) / (maxSpeed - minSpeed)
	alpha = math.clamp(alpha, 0, 1)
	return minPlayback + (alpha * (maxPlayback - minPlayback))
end

--// SETUP FOOTSTEP
local function SetupFootstep(ID)
	if currentFootstepSound and currentFootstepSound.Parent then
		currentFootstepSound.PlaybackSpeed = FootstepSpeed()
		return
	end

	local sound = Instance.new("Sound")
	sound.Name = "Footstep"
	sound.SoundId = ID
	sound.Volume = volume
	sound.PlaybackSpeed = FootstepSpeed()
	sound.RollOffMode = Enum.RollOffMode.Linear
	sound.RollOffMaxDistance = 100
	sound.RollOffMinDistance = 5
	sound.Looped = true
	sound.Parent = HRP
	sound:Play()

	currentFootstepSound = sound
end

--// FOOTSTEP CLEANUP
local function ClearFootstep()
	if currentFootstepSound then
		currentFootstepSound:Stop()
		currentFootstepSound:Destroy()
		currentFootstepSound = nil
	end
end

--// DETECT IF PLAYER IS MOVING
Humanoid.Running:Connect(function(speed)
	local wasWalking = walking
	walking = speed > 0

	if walking and not wasWalking then
		footstepTimer = 0
	elseif not walking and wasWalking then
		ClearFootstep()
	end
end)

--// MAIN FOOTSTEP LOOP
RunService.Heartbeat:Connect(function(dt)
	if walking then
		local velocity = HRP.AssemblyLinearVelocity.Magnitude
		if velocity < 0.1 then return end

		footstepInterval = stepLength / velocity
		footstepTimer += dt

		if footstepTimer >= footstepInterval then
			footstepTimer = 0

			local MaterialTable = FOOTSTEP:GetTableFromMaterial(Humanoid.FloorMaterial)
			if MaterialTable then
				local footstep = FOOTSTEP:GetRandomFootstep(MaterialTable)
				SetupFootstep(footstep)
			end
		else
			if currentFootstepSound then
				currentFootstepSound.PlaybackSpeed = FootstepSpeed()
			end
		end
	else
		footstepTimer = 0
		ClearFootstep()
	end
end)
1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.