Better RbxCharacterSounds and Animate script

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

Marketplace

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
35 Likes

Roblox should switch to this
Their lua scripts are weak

5 Likes

Very nice! But quick stupid question, does this also fix the R6 “jump while moving in the air and stop at the last millisecond moving while fully landed" bug or like, do i have to add it myself? (i’m not sure if its still around, lol)

I’m talking about this, if it helps. How to fix this R6 walk animation bug

5 Likes

I didn’t know about that and yes script had this bug but I fixed it just now, Thanks

3 Likes

I have the Ninja Jump animation, and the default jump animation plays over it instead. Just a small bug to fix.

1 Like

I’m not sure what’s the issue, maybe you have turned off custom animations in your place settings if not go to explorer, and after your character spawns find Animate script(game.Workspace.CHARACTERNAME.Animete) and post it here.

1 Like

Every other animation plays just fine and has my custom animation ID, but not the jump animation.

1 Like

That isn’t helpful at all I need to know what animations are inside that script.

1 Like

Animate.rbxm (2.0 KB)

1 Like

Did you name jump animation to “!ID!” or it was like that?

I didn’t do anything, so it must have been like that.

weird, the name should be “JumpAnim” but maybe it was a one time bug as this animation was made by the game and not my script, anyway I fixed another bug that could be causing the jump animation problem, maybe it will be the solution, you can test it.

Im gonna remake this with my own coding style and logic
What features does robloxs animate script have

1 Like

This is awesome, I’ve been going through a lot of default roblox scripts recently trying to improve them and this is a big help

This is very cool. However, it seems like the “toolanim” slash that linked swords use doesn’t work on your version of the animations script. It would be very useful if that were added in.

If you want swords to work with my script use this
ClassicSword.rbxm (9.3 KB)

1 Like

After optimizing everything I noticed the most costly local script when idle is the animate, so I will be trying this out. What I don’t like is how its architecture and functionalitity is soo different than the original.such as being able to use multiple idle animations, and adding animations dynmically to the directories to do things like make a low health animation.

Do you plan to update this with the new strafing animations?

Right now I’m working on new version, but I will post it without those strafing animations because this is new to me and I need to learn how it works to implement it, so this will take some time also in new version I fixed some mayor issues, soon I will upload a new version and then will try to work on strafing animations.

1 Like