I made a small and efficient script that controls animations and sounds of characters. I tried my best to implement all features from official Animate and RbxCharacterSounds and I think I did it, but there could be things I didn’t include.
Script advantages over official scripts
- faster and less memory usage
- includes R6 and R15 code
- smaller
bugs fixed:
- walk and run animations desynchronization
- jump sound doesn’t always play
- walk/run animation play after character landed(reported by: Scou1yy)
Benchmarks
Character configuration:
Rig type - R15
Custom animations - all
ChatVersiom - LegacyChatService
Memory usage (mb) | Official Roblox scripts | My Script |
---|---|---|
Local player | 0.149-0.156 | 0.051-0.072 |
Local + 1 player | 0.16-0.167 | 0.057-0.09 |
Local + 7 players | 0.213-0.224 | 0.092-0.176 |
Local spawn cost | ~0.281 | ~0.023 |
player spawn cost | ~0.013 | ~0.009 |
Load time (s) | Official Roblox scripts | My Script |
---|---|---|
Local player | 0,017799 | 0.009158 |
Local + 1 player | 0,017846 | 0.009635 |
Local + 7 players | 0,019888 | 0.013137 |
Local spawn cost | 0,006376 | 0.003332 |
player spawn cost | 0.001159 | 0.001144 |
benchmarks date: 13.08.2023
Downloads
Better RbxCharacterSounds and Animate.rbxm (10.7 KB)
code
---------------------------
-- author: maksgame19 --
-- version: 1.0 --
---------------------------
local Players = game.Players or game:GetService("Players")
local playersData = {}
local legacyChat = (game.TextChatService or game:GetService("TextChatService")).ChatVersion == Enum.ChatVersion.LegacyChatService
local allowCustomAnims = (game.StarterPlayer or game:GetService("StarterPlayer")).AllowCustomAnimations
local localPlrID = Players.localPlayer.UserId
local freefall, idle1, idle2, jumping, running, seated, walk, swimmingIdle, swimming, defaultTool, climbing, emotes--animations
local customState, localPlayerTable, oldAnim
local function startSound(playerTable, sound)
local oldSound = playerTable.oldSound
if oldSound ~= sound then
if oldSound then
oldSound:Pause()--can't do "oldSound.Playing = false" because it will privent from fireing Paused event
end
if sound then
sound.Playing = true
end
playerTable.oldSound = sound
end
end
local function startSoundAndAnim(sound, anim, transitionTime, state)
startSound(localPlayerTable, sound)
if oldAnim ~= anim then
if oldAnim then
oldAnim:Stop(transitionTime)
end
if anim then
anim:Play(transitionTime)
end
customState = state
oldAnim = anim
end
end
local function startEmote(emote)
if emote then
startSoundAndAnim(nil, emote, 0.5, "Idle")
emote.Stopped:Wait()
task.wait()
if customState == "Idle" and emote == oldAnim then
startSoundAndAnim(nil, idle1, 0.1, "Idle")
end
end
end
local function adjustWalkAndRunProperties(walkWaight, runWaight, speed)
walk:AdjustWeight(walkWaight)
running:AdjustWeight(runWaight)
walk:AdjustSpeed(speed)
running:AdjustSpeed(speed)
end
local localStateFunctions = {
[Enum.HumanoidStateType.Jumping] = function()
local sounds = localPlayerTable.sounds
startSoundAndAnim(sounds.freefall, jumping, 0.1, "jump")
local jumpingSound = sounds.jumping
jumpingSound.TimePosition = 0
jumpingSound.Playing = true
end,
[Enum.HumanoidStateType.Freefall] = function(rootPart)
local freefallSound = localPlayerTable.sounds.freefall
freefallSound.Volume = 0
if customState == "jump" then
task.wait(0.3)--jump duration
if not freefallSound.IsPlaying then return end
end
startSoundAndAnim(freefallSound, freefall, 0.2)
local thread = task.spawn(function()
while rootPart.assemblyLinearVelocity.Y > -75 do
task.wait(0.05)
end
for i = 0, 1, 0.05 do
freefallSound.Volume = i
task.wait(0.05)
end
end)
freefallSound.Paused:Wait()
task.cancel(thread)
end,
[Enum.HumanoidStateType.Seated] = function()
startSoundAndAnim(nil, seated, 0.5)
end,
}
local stateFunctions = {
[Enum.HumanoidStateType.Jumping] = function(playerTable)
local jump = playerTable.sounds.jumping
jump.TimePosition = 0
jump.Playing = true
end,
[Enum.HumanoidStateType.Freefall] = function(playerTable, rootPart)
local freefall = playerTable.sounds.freefall
freefall.Volume = 0
startSound(playerTable)
task.wait(0.3)
if freefall.IsPlaying then
local thread = task.spawn(function()
repeat
freefall.Volume = 0
while rootPart.assemblyLinearVelocity.Y > -75 do
task.wait(0.1)
end
for i = 0, 1, 0.1 do
freefall.Volume = i
task.wait(0.1)
end
until rootPart.assemblyLinearVelocity.Y < -75
end)
freefall.Paused:Wait()
task.cancel(thread)
end
end,
[Enum.HumanoidStateType.Swimming] = function(playerTable, rootPart)
local sounds = playerTable.sounds
startSound(playerTable, sounds.swimming)
rootPart = math.abs(rootPart.assemblyLinearVelocity.Y)
if rootPart > 0.1 then
local splash = sounds.splash
splash.Volume = rootPart * 0.00288 - 0.008 --map a value from one range to another (velY - 100)*(1 - 0.28)/(350 - 100) + 0.28
splash:Play()
end
end,
[Enum.HumanoidStateType.GettingUp] = function(playerTable)
playerTable.sounds.gettingUp.Playing = true
end,
[Enum.HumanoidStateType.Landed] = function(playerTable, rootPart)
rootPart = -rootPart.assemblyLinearVelocity.Y
if rootPart > 75 then
local landing = playerTable.sounds.landing
landing.Volume = rootPart/50 - 1--map a value from one range to another (velY - 50)*(1 - 0)/(100 - 50) + 0
landing.Playing = true
end
end,
[Enum.HumanoidStateType.Dead] = function(playerTable)
startSound(playerTable)
playerTable.sounds.died.Playing = true
if playerTable.garbage then--for some reason running event in R15 code doesn't get disconnected so we need to do that manualy
localPlayerTable.garbage:Disconnect()
end
playersData[playerTable.plrID] = nil
end,
[Enum.HumanoidStateType.Climbing] = function() end,
[Enum.HumanoidStateType.Running] = function() end,
}
local function createSystem(roots, char, plrID, localplr)
local humanoid, rootPart = roots.Humanoid, roots.HumanoidRootPart
local _, activeState = pcall(function() return humanoid:GetState() end)
playersData[plrID] = {}
local playerTable = playersData[plrID]
playerTable.plrID = plrID
playerTable.sounds = {}
local sounds = playerTable.sounds
local soundsRes = script.Sounds:Clone()
for _, sound in soundsRes:GetChildren() do
sound.Parent = rootPart
sounds[sound.Name] = sound
end
soundsRes.Parent = nil
if not localplr then
--connections
humanoid.StateChanged:Connect(function(_, state)--state tracker
activeState = state
local func = stateFunctions[activeState]
if func then
func(playerTable, rootPart)
else
startSound(playerTable)
end
end)
humanoid.running:Connect(function(vel)
if humanoid.MoveDirection.Magnitude > 0 then
local runSound = sounds.running
startSound(playerTable, runSound)
runSound.PlaybackSpeed = 1.85
else
startSound(playerTable)
end
end)
humanoid.climbing:Connect(function(vel)
if vel ~= 0 then
local runSound = sounds.running
startSound(playerTable, runSound)
sounds.running.PlaybackSpeed = 1
else
startSound(playerTable)
end
end)
--initialize first sound
local func = stateFunctions[activeState]
if func then
func(playerTable, rootPart)
end
else --local player handler
local animate = roots.Animate
local rig = humanoid.RigType.Name
local animsPack = script[rig]
local animator = roots.Animator
localPlayerTable = playerTable
emotes = {}
local function loadNewAnim(anim, priority)
anim = animator:LoadAnimation(anim)
anim.Priority = priority
return anim
end
--load animations
for _, emote in animsPack.emotes:GetChildren() do
local name = emote.Name
emotes[name] = loadNewAnim(emote, Enum.AnimationPriority.Core)
if string.sub(name, 1, 5) ~= "dance" then--roblox set looped to true in every emote animation but only dance's should be looped
emotes[name].Looped = false
end
end
defaultTool, seated = loadNewAnim(animsPack.defaultTool, Enum.AnimationPriority.Idle), loadNewAnim(animsPack.seated, Enum.AnimationPriority.Core)
if legacyChat and rig == "R15" or not legacyChat then
roots.PlayEmote.OnInvoke = function(emote)--custom emote
if customState == "Idle" then
local loadedEmote = emotes[emote]
if loadedEmote then
startEmote(loadedEmote)
return true
elseif emote:IsA("Instance") then
local name = emote.Name
emotes[name] = loadNewAnim(emote, Enum.AnimationPriority.Core)
loadedEmote = emotes[name]
loadedEmote.Looped = false
startEmote(loadedEmote)
return true
end
end
end
end
humanoid.StateChanged:Connect(function(_, state)--state tracker
activeState = state
local localFunc = localStateFunctions[activeState]
if localFunc then
localFunc(rootPart)
return
end
local func = stateFunctions[activeState]
if func then
func(localPlayerTable, rootPart)
else
startSoundAndAnim()
end
end)
char.ChildAdded:Connect(function(child)
if child:IsA("Tool") and child:FindFirstChild("Handle") then
defaultTool:Play()
end
end)
char.ChildRemoved:Connect(function(child)
if child:IsA("Tool") and child:FindFirstChild("Handle") then
defaultTool:Stop()
end
end)
if rig == "R15" then
local humanoidDescription = roots.HumanoidDescription
local oldHipHeight = humanoid.HipHeight
local scale = 6.4 * oldHipHeight--16 / 1.25 * 2 / (1 + (oldHipHeight - 2) * 1 / 2)
local walkIsPlaying
localStateFunctions[Enum.HumanoidStateType.Swimming] = nil
local function Connections()
localPlayerTable.garbage = humanoid.running:Connect(function(vel)
if humanoid.MoveDirection.Magnitude > 0 then
local runSound = sounds.running
startSoundAndAnim(runSound, running, 0.1)
runSound.PlaybackSpeed = 1.85
if not walkIsPlaying then
walkIsPlaying = true
walk:Play()
end
if humanoid.HipHeight ~= oldHipHeight then
oldHipHeight = humanoid.HipHeight
scale = 6.4 * oldHipHeight --16 / 1.25 / (1 + (oldHipHeight - 2) * 1 / 2)
end
vel /= scale
if vel <= 0.5 then
adjustWalkAndRunProperties(1, 0.0001, vel / 0.5)
elseif vel < 1 then
vel = vel/0.5 - 1 --(vel-0.5)/0.5
adjustWalkAndRunProperties(1 - vel, vel , 1)
else
adjustWalkAndRunProperties(0.0001, 1, vel)
end
if walkIsPlaying == true then
running.Stopped:Wait()
walk:Stop()
walkIsPlaying = nil
end
else
startSoundAndAnim(nil, idle1, 0.2, "Idle")
end
end)
humanoid.climbing:Connect(function(vel)
if vel ~= 0 then
local runSound = sounds.running
startSoundAndAnim(runSound, climbing, 0.1)
climbing:AdjustSpeed(vel / 5)
runSound.PlaybackSpeed = 1
else
startSound(localPlayerTable)
climbing:AdjustSpeed(0)
end
end)
humanoid.swimming:Connect(function(vel)
if vel > 1 then
startSoundAndAnim(sounds.swimming, swimming, 0.3)
swimming:AdjustSpeed(vel / 10)
else
startSoundAndAnim(sounds.swimming, swimmingIdle, 0.5)
end
end)
idle1.DidLoop:Connect(function()
if math.random(1, 10) == 1 then
idle1:Stop()
idle2:Play()
oldAnim = idle2
end
end)
idle2.DidLoop:Connect(function()
idle2:Stop()
idle1:Play()
oldAnim = idle1
end)
end
--load the rest needed animations
if allowCustomAnims and humanoidDescription then
local descriptionAnims = {["ClimbAnimation"] = false, ["FallAnimation"] = false, ["IdleAnimation"] = true, ["JumpAnimation"] = false, ["RunAnimation"] = false, ["WalkAnimation"] = false, ["SwimAnimation"] = true}
local connections, foundAnims = {}, {}
local requiredAnims, count = 0, 0
for idx,anim in descriptionAnims do
if humanoidDescription[idx] ~= 0 then
if anim then
requiredAnims += 2
else
requiredAnims += 1
end
end
end
if requiredAnims == 0 then
climbing, freefall, idle1, idle2, jumping, running, walk, swimming, swimmingIdle = loadNewAnim(animsPack.climbing, Enum.AnimationPriority.Core), loadNewAnim(animsPack.freefall, Enum.AnimationPriority.Core), loadNewAnim(animsPack.idle1, Enum.AnimationPriority.Core), loadNewAnim(animsPack.idle2, Enum.AnimationPriority.Core), loadNewAnim(animsPack.jumping, Enum.AnimationPriority.Core), loadNewAnim(animsPack.running, Enum.AnimationPriority.Core), loadNewAnim(animsPack.walk, Enum.AnimationPriority.Core), loadNewAnim(animsPack.swimming, Enum.AnimationPriority.Core), loadNewAnim(animsPack.swimmingIdle, Enum.AnimationPriority.Core)
Connections()
return
end
local function checkChild(parent)
if not parent:IsA("Animation") then
for _,child in parent:GetChildren() do
checkChild(child)
end
table.insert(connections, parent.ChildAdded:Connect(function(child)
checkChild(child)
end))
return
elseif parent.Parent.Name == "pose" or parent.Parent.Name == "mood" then
return
end
foundAnims[parent.Name] = parent
count += 1
if count == requiredAnims then-- all Animations found
for _,conn in connections do
conn:Disconnect()
end
climbing, freefall, idle1, idle2, jumping, running, walk, swimming, swimmingIdle = loadNewAnim(foundAnims.ClimbAnim or animsPack.climbing, Enum.AnimationPriority.Core), loadNewAnim(foundAnims.FallAnim or animsPack.freefall, Enum.AnimationPriority.Core), loadNewAnim(foundAnims.Animation1 or animsPack.idle1, Enum.AnimationPriority.Core), loadNewAnim(foundAnims.Animation2 or animsPack.idle2, Enum.AnimationPriority.Core), loadNewAnim(foundAnims.JumpAnim or animsPack.jumping, Enum.AnimationPriority.Core), loadNewAnim(foundAnims.RunAnim or animsPack.running, Enum.AnimationPriority.Core), loadNewAnim(foundAnims.WalkAnim or animsPack.walk, Enum.AnimationPriority.Core), loadNewAnim(foundAnims.Swim or animsPack.swimming, Enum.AnimationPriority.Core), loadNewAnim(foundAnims.SwimIdle or animsPack.swimmingIdle, Enum.AnimationPriority.Core)
Connections()
end
end
checkChild(animate)
else
climbing, freefall, idle1, idle2, jumping, running, walk, swimming, swimmingIdle = loadNewAnim(animsPack.climbing, Enum.AnimationPriority.Core), loadNewAnim(animsPack.freefall, Enum.AnimationPriority.Core), loadNewAnim(animsPack.idle1, Enum.AnimationPriority.Core), loadNewAnim(animsPack.idle2, Enum.AnimationPriority.Core), loadNewAnim(animsPack.jumping, Enum.AnimationPriority.Core), loadNewAnim(animsPack.running, Enum.AnimationPriority.Core), loadNewAnim(animsPack.walk, Enum.AnimationPriority.Core), loadNewAnim(animsPack.swimming, Enum.AnimationPriority.Core), loadNewAnim(animsPack.swimmingIdle, Enum.AnimationPriority.Core)
Connections()
end
else--R6
localStateFunctions[Enum.HumanoidStateType.Swimming] = function(velY)
startSoundAndAnim(sounds.swimming, running, 0.4)
velY = math.abs(velY.assemblyLinearVelocity.Y)
if velY > 0.1 then
local splash = sounds.splash
splash.Volume = velY * 0.00288 - 0.008 --map a value from one range to another (velY - 100)*(1 - 0.28)/(350 - 100) + 0.28
splash:Play()
end
end
--load the rest needed animations
climbing, freefall, idle1, idle2, jumping, running = loadNewAnim(animsPack.climbing, Enum.AnimationPriority.Core), loadNewAnim(animsPack.freefall, Enum.AnimationPriority.Core), loadNewAnim(animsPack.idle1, Enum.AnimationPriority.Core), loadNewAnim(animsPack.idle2, Enum.AnimationPriority.Core), loadNewAnim(animsPack.jumping, Enum.AnimationPriority.Core), loadNewAnim(animsPack.running, Enum.AnimationPriority.Core)
--connections
humanoid.running:Connect(function(vel)
if humanoid.MoveDirection.Magnitude > 0 then
print(1)
local runSound = sounds.running
startSoundAndAnim(runSound, running, 0.1)
running:AdjustSpeed(vel / 14.5)
runSound.PlaybackSpeed = 1.85
else
startSoundAndAnim(nil, idle1, 0.1, "Idle")
end
end)
humanoid.climbing:Connect(function(vel)
if vel ~= 0 then
local runSound = sounds.running
startSoundAndAnim(runSound, climbing, 0.1)
climbing:AdjustSpeed(vel / 12)
runSound.PlaybackSpeed = 1
else
startSound(localPlayerTable)
climbing:AdjustSpeed(0)
end
end)
idle1.DidLoop:Connect(function()
if math.random(1, 10) == 1 then
idle1:Stop()
idle2:Play()
oldAnim = idle2
end
end)
idle2.DidLoop:Connect(function()
idle2:Stop()
idle1:Play()
oldAnim = idle1
end)
end
--initialize first animation and/or sound
local func = localStateFunctions[activeState]
if func then
func(rootPart)
else
startSoundAndAnim(nil, idle1, nil, "Idle")
end
end
end
local function WaitForInstances(char, plrId)-- instantly get instances that just loaded
local requiredInstances, instancesToFind, localplr, allInstancesFound
local foundInstances, connections = {}, {}
local count = 0
if plrId ~= localPlrID then
localplr = false
requiredInstances = 2
instancesToFind = {
["Humanoid"] = true,
["HumanoidRootPart"] = true
}
else
localplr = true
requiredInstances = 6
instancesToFind = {
["Animate"] = "PlayEmote",
["Humanoid"] = {
["Animator"] = true,
["HumanoidDescription"] = true
},
["HumanoidRootPart"] = true
}
end
local function checkChild(parent, instancesToFind)
local name = parent.Name
local nextInstancesToFind = instancesToFind[name] or name == instancesToFind
if not nextInstancesToFind then
return
end
foundInstances[name] = parent
count += 1
if count == requiredInstances then -- all instances found
allInstancesFound = true
for _,conn in connections do
conn:Disconnect()
end
createSystem(foundInstances, char, plrId, localplr)
elseif nextInstancesToFind ~= true then
for _,child in parent:GetChildren() do
checkChild(child, nextInstancesToFind)
end
table.insert(connections, parent.ChildAdded:Connect(function(child)
checkChild(child, nextInstancesToFind)
end))
end
end
for _,child in char:GetChildren() do
checkChild(child, instancesToFind)
end
table.insert(connections, char.ChildAdded:Connect(function(child)
checkChild(child, instancesToFind)
end))
--timeout
task.wait(0.1)
if not allInstancesFound then
if count == 5 and not foundInstances.HumanoidDescription then -- there is a possibility that player will load without custom animation
for _,conn in connections do
conn:Disconnect()
end
createSystem(foundInstances, char, plrId, localplr)
else
for _,conn in connections do
conn:Disconnect()
end
warn("needed instances doesn't exist")
end
end
end
local function setup(plr)
local char = plr.Character
if char then
WaitForInstances(char, plr.UserId)
end
plr.CharacterAdded:Connect(function(char)
WaitForInstances(char, plr.UserId)
end)
end
for _, plr in Players:GetChildren() do -- a bit faster from GetPlayers
setup(plr)
end
Players.PlayerAdded:Connect(setup)
Players.PlayerRemoving:Connect(function(plr)
playersData[plr.UserId] = nil
end)
--emote executor
if legacyChat then
Players.localPlayer.Chatted:connect(function(msg)
if string.sub(msg, 1, 1) == "/" and customState == "Idle" then
msg = string.lower(msg)
if string.sub(msg, 2, 3) == "e " then
startEmote(emotes[string.sub(msg, 4)])
elseif string.sub(msg, 2, 7) == "emote " then
startEmote(emotes[string.sub(msg, 8)])
end
end
end)
end