Optimized character animations and sounds controller

I made a script that is just official Roblox’s scripts(RbxCharacterSounds and Animate) combined into one script plus it is more optimized.

Script advantages over official scripts

improvements:

  • faster and less memory usage
  • includes R6 and R15 functionality
  • 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.171 0.054-0.067
Local + 1 player 0.161-0.181 0.061-0.074
Local + 7 players 0.212-0.23 0.106-0.117
Local spawn cost ~0.338 ~0.043
player spawn cost ~0.011 ~0.013
Load time (s) Official Roblox scripts My Script
Local player 0.0020963 0.0011019
Local + 1 player 0.002163 0.0011304
Local + 7 players 0.00263 0.0013014
Local spawn cost 0.00181 0.0009985
player spawn cost 0.0000667 0.0000285

Benchmarks date: 09.06.2024

Downloads

Marketplace

Optimized animations and sounds controller.rbxm (9.2 KB)

code
--[[
forum post: https://devforum.roblox.com/t/optimized-animations-and-sounds-controller/2525206
author: maksgame19
version: 2.0
]]
local Players = game:FindService("Players") or game:GetService("Players")

local LEGACY_CHAT = (game:FindService("TextChatService") or game:GetService("TextChatService")).ChatVersion == Enum.ChatVersion.LegacyChatService
local ALLOW_CUSTOM_ANIMS = (game:FindService("StarterPlayer") or game:GetService("StarterPlayer")).AllowCustomAnimations

local localPlayer = Players.localPlayer
local LOCAL_PLAYER_ID = localPlayer.UserId

local cachedAnims = {}--stores default animations to prevent making new ones
local plrsFall = {}

local rig

local function disconnectAll(conns)
	for _,conn in conns do
		conn:Disconnect()
	end
end
local function getChildren(conns,parent,func)--easy way of getting instances almost instantly
	for	_,child in parent:GetChildren() do
		func(child)
	end
	table.insert(conns, parent.ChildAdded:Connect(func))
end
local function createSystem(roots, plrSounds, char, plrID, localPlr)
	local humanoid, rootPart = roots.Humanoid, roots.HumanoidRootPart
	local minSpeed = humanoid.WalkSpeed * .03
	local conns = {}
	local oldSound, universalPlay
	-- reuse sounds
	for _, sound in plrSounds do
		sound.Parent = rootPart
		sound.RollOffMinDistance = 5
		sound.RollOffMaxDistance = 150
		sound.Volume = .65
	end
	local died,freefall,gettingUp,jumping,landing,running,splash,swimming = unpack(plrSounds)
	
	local fallData = {sound=freefall, root=rootPart}
	
	local function playSound(sound)
		if oldSound == sound then return end
		if oldSound then
			oldSound:Pause()
		end
		if sound then
			sound.Playing = true
		end
		oldSound = sound
	end

	local stateFuncs = {
		[Enum.HumanoidStateType.GettingUp] = function() 
			gettingUp:Play() 
		end,
		[Enum.HumanoidStateType.Landed] = function()
			local velY = -rootPart.assemblyLinearVelocity.Y
			if velY <= 75 then return end
			landing.Volume = velY*.02-1--map a value from one range to another (velY - 50)*(1 - 0)/(100 - 50) + 0
			landing:Play()
		end,
		[Enum.HumanoidStateType.Climbing] = function()
			running.PlaybackSpeed = 1
		end,
		[Enum.HumanoidStateType.Running] = function()
			running.PlaybackSpeed = 1.85
		end,
		[Enum.HumanoidStateType.Swimming] = function()
			playSound(swimming)
			local vel = math.abs(rootPart.assemblyLinearVelocity.Y)
			if vel > .1 then
				splash.Volume = vel*.00288-.008 --map a value from one range to another (velY - 100)*(1 - .28)/(350 - 100) + .28
				splash:Play()
			end
		end,
		[Enum.HumanoidStateType.Dead] = function()
			died:Play()
		end
	}

	table.insert(conns, freefall.Paused:Connect(function()
		plrsFall[plrID] = nil
	end))
	table.insert(conns, humanoid:GetPropertyChangedSignal("WalkSpeed"):Connect(function()
		minSpeed = humanoid.WalkSpeed * .03
	end))
	humanoid.StateChanged:Connect(function(_, state)--state tracker
		(stateFuncs[state] or universalPlay)()
	end)
	char.AncestryChanged:Once(function()--clear
		plrsFall[plrID] = nil
		universalPlay()
		disconnectAll(conns)
	end)

	if not localPlr then
		universalPlay = playSound
		stateFuncs[Enum.HumanoidStateType.Jumping] = function()
			jumping:play()
		end
		stateFuncs[Enum.HumanoidStateType.Freefall] = function()
			freefall.Volume = 0
			playSound(freefall)
			plrsFall[plrID] = fallData
		end
		--connections
		humanoid.climbing:Connect(function(vel)
			playSound(vel ~= 0 and running)
		end)
		table.insert(conns, humanoid.running:Connect(function(vel)--for some reason running event isn't disconnected automaticly so we need to do that manualy
			playSound(vel > minSpeed and running)
		end))
		return
	end

	--local player handler
	local animator = roots.Animator
	local anims, emotes, oldAnim, defaultTool, limiter, fallTransitionTime

	stateFuncs[Enum.HumanoidStateType.Jumping] = function()
		universalPlay(nil, "jump", .1)
		jumping:Play()
	end
	stateFuncs[Enum.HumanoidStateType.Seated] = function()
		universalPlay(nil, "seat", .5)
	end
	stateFuncs[Enum.HumanoidStateType.Freefall] = function()
		local jump = anims.jump
		if jump.IsPlaying then
			limiter = true
			task.wait(.25)
			if not (jump.IsPlaying and limiter) then return end
			limiter = nil
		end
		freefall.Volume = 0
		universalPlay(freefall, "fall", fallTransitionTime)
		plrsFall[plrID] = fallData
	end

	function universalPlay(sound, anim, transitionTime)
		playSound(sound)
		anim = anims[anim]
		if oldAnim ~= anim then
			if oldAnim then
				oldAnim:Stop(transitionTime)
			end
			if anim then
				anim:Play(transitionTime)
			end
			oldAnim = anim
		end
		return anim
	end
	local function startEmote(emote)
		if not emote then return true end
		universalPlay()
		oldAnim = emote
		emote:Play(0.1)
		if emote.Looped then return end
		local idx = #conns + 1
		conns[idx] = emote.Stopped:Once(function()
			conns[idx] = nil
			task.wait()
			if oldAnim == emote then
				universalPlay(nil, "idle1", .1)
			end
		end)
	end
	local function loadAnim(anim, priority)
		anim = animator:LoadAnimation(anim)
		anim.Priority = priority or Enum.AnimationPriority.Core
		return anim
	end
	local function makeAnim(anims, priority)
		for animName, anim in anims do
			local existingAnim = cachedAnims[animName]
			if existingAnim then
				anim = existingAnim
			else
				local newAnim = Instance.new("Animation")
				newAnim.AnimationId = "http://www.roblox.com/asset/?id=" .. anim
				anim = newAnim
				cachedAnims[animName] = newAnim
			end
			anims[animName] = loadAnim(anim, priority)
		end
		return anims
	end
	local function idleLoop()
		table.insert(conns, anims.idle1.DidLoop:Connect(function()
			if math.random(1, 10) ~= 1 then return end
			universalPlay(nil,"idle2", .1)
		end))
		table.insert(conns, anims.idle2.DidLoop:connect(function()
			universalPlay(nil,"idle1", .1)
		end))
	end

	if rig ~= humanoid.RigType.Name then--remove unused animations
		rig = humanoid.RigType.Name
		table.clear(cachedAnims)
	end
	if LEGACY_CHAT and rig == "R15" or not LEGACY_CHAT then--emote executor
		roots.PlayEmote.OnInvoke = function(emote)
			if not (anims.idle2.IsPlaying or anims.idle1.IsPlaying) then return end
			local loadedEmote
			if emote:IsA("Instance") then
				loadedEmote = loadAnim(emote)
				loadedEmote.Looped = false
			else
				loadedEmote = emotes[emote]
			end
			return not startEmote(loadedEmote)
		end
	end
	if LEGACY_CHAT then--emote executor
		table.insert(conns, localPlayer.Chatted:connect(function(msg)
			if string.sub(msg, 1, 1) ~= "/" or not (anims.idle2.IsPlaying or anims.idle1.IsPlaying) then return end
			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
	if rig == "R15" then
		local scale = 2 / humanoid.HipHeight
		fallTransitionTime = .2
		emotes = makeAnim({cheer=507770677,dance=507771019,dance2=507776043,dance3=507777268,laugh=507770818,point=507770453,wave=507770239})
		defaultTool = makeAnim({507768375}, Enum.AnimationPriority.Idle)[1]

		local function adjustWalkAndRunProperties(walkWaight, runWaight, speed)
			local walk, run = anims.walk, anims.run
			walk:AdjustWeight(walkWaight)
			run:AdjustWeight(runWaight)
			walk:AdjustSpeed(speed)
			run:AdjustSpeed(speed)
		end
		
		do--Animations loader
			local humanDesc = roots.HumanoidDescription
			local animPack = {climb=507765644,fall=507767968,idle1=507766388,idle2=507766666,jump=507765000,run=913376220,walk=913402848,swim=913384386,swimidle=913389285,seated=2506281703}
			local requiredAmount,animationsToFind

			local function Continue()
				idleLoop()
				table.insert(conns, anims.run.Stopped:Connect(function()
					anims.walk:Stop()
				end))
			end

			if ALLOW_CUSTOM_ANIMS and humanDesc and (function()
					animationsToFind = {climb="ClimbAnimation",fall="FallAnimation",idle="IdleAnimation","IdleAnimation",jump="JumpAnimation",run="RunAnimation",swim="SwimAnimation",swimidle="SwimAnimation",walk="WalkAnimation"}
					requiredAmount = 0
					for name,property in animationsToFind do
						if humanDesc[property] == 0 then
							animationsToFind[name] = nil
							continue
						end
						requiredAmount += 1
						animPack[name] = nil
						if name == "idle" then
							animPack.idle1, animPack.idle2 = nil
						end
					end
					animationsToFind[1] = nil
					return requiredAmount ~= 0
				end)() then
				--search/wait for custom animations
				local conns = {}
				local allAnimsFound
				anims = makeAnim(animPack)

				local function AnimFound(childName, child)
					cachedAnims[childName] = nil --removes unused animation
					anims[childName] = loadAnim(child)
					requiredAmount -= 1
					if requiredAmount ~= 0 then return end
					allAnimsFound = true
					disconnectAll(conns)
					Continue()
				end
				
				getChildren(conns, roots.Animate, function(child)
					local childName = child.Name
					if not animationsToFind[childName] then return end
					getChildren(conns, child, function(child)
						if childName ~= "idle" then
							AnimFound(childName, child)
							return
						end
						getChildren(conns, child, function(waight)
							AnimFound(waight.Value == 9 and "idle1" or "idle2",child)
						end)
					end)
				end)
				
				task.spawn(function()
					while task.wait(4) and not allAnimsFound do
						if char and char.Parent then continue end
						disconnectAll(conns)
						return
					end
				end)
			else
				anims = makeAnim(animPack)
				Continue()
			end
		end
		--connections
		table.insert(conns, humanoid:GetPropertyChangedSignal("HipHeight"):Connect(function()
			scale = 2 / humanoid.HipHeight 
		end))
		table.insert(conns, humanoid.running:Connect(function(vel)--run/walk speed formula 2/HipHeight*.078125*vel
			if vel <= minSpeed then
				universalPlay(nil, "idle1", .2)
				return 
			end
			universalPlay(running, "run", .2)
			local walk = anims.walk
			if not walk.IsPlaying then
				walk:Play(.2)
			end
			vel *= scale * .078125--scale * (1.25 / 16) * vel
			if vel <= .5 then
				adjustWalkAndRunProperties(1, .0001, vel * 2)
			elseif vel < 1 then
				vel = vel * 2 - 1
				adjustWalkAndRunProperties(1 - vel, vel , 1)
			else
				adjustWalkAndRunProperties(.0001, 1, vel)
			end
		end))
		table.insert(conns, humanoid.climbing:Connect(function(vel)
			if vel == 0 then
				universalPlay(nil, "climb"):AdjustSpeed(0)
				return
			end
			universalPlay(running, "climb", .1):AdjustSpeed(vel * scale * .2)
		end))
		table.insert(conns, humanoid.swimming:Connect(function(vel)
			vel *= scale
			if vel <= 1 then
				universalPlay(swimming, "swimidle", .4)
				return 
			end
			universalPlay(swimming, "swim", .4):AdjustSpeed(vel * .1)
		end))
	else--R6
		fallTransitionTime = .3
		--load needed animation
		anims=makeAnim({climb=180436334,fall=180436148,idle1=180435571,idle2=180435792,jump=125750702,run=180426354,seat=178130996})
		emotes=makeAnim({cheer=129423030,dance=182435998,dance2=182436842,dance3=182436935,laugh=129423131,point=128853357,wave=128777973})
		defaultTool=makeAnim({182393478}, Enum.AnimationPriority.Idle)[1]
		
		stateFuncs[Enum.HumanoidStateType.Swimming] = function()
			universalPlay(swimming, "run", .1)
			local vel = math.abs(rootPart.assemblyLinearVelocity.Y)
			if vel > .1 then
				splash.Volume = vel*.00288-.008 --map a value from one range to another (velY - 100)*(1 - .28)/(350 - 100) + .28
				splash:Play()
			end
		end

		--connections
		table.insert(conns, humanoid.running:Connect(function(vel)
			if vel <= minSpeed then
				universalPlay(nil, "idle1", .1)
				return
			end
			universalPlay(running, "run", .1):AdjustSpeed(vel / char:GetScale() / 14.5)
		end))
		table.insert(conns, humanoid.climbing:Connect(function(vel)
			if vel == 0 then
				universalPlay(nil, "climb"):AdjustSpeed(0)
				return 
			end 
			universalPlay(running, "climb", .1):AdjustSpeed(vel / char:GetScale() / 12)
		end))
		idleLoop()
	end

	for _,emote in {emotes.cheer,emotes.laugh,emotes.point,emotes.wave} do
		emote.Looped = false
	end
	
	--R15/R6 connection
	table.insert(conns, char.ChildAdded:Connect(function(child)
		if not (child:IsA("Tool") and child:FindFirstChild("Handle")) then return end
		defaultTool:Play()
		task.wait()
		child.AncestryChanged:Wait()
		defaultTool:Stop()
	end))
end
local function characterAdded(char, plrId, plrSounds)--instantly get instances that ware parented
	local localPlr = plrId == LOCAL_PLAYER_ID
	local count = 0
	local foundIns, conns = {}, {}
	local allInstancesFound, instancesToFind, requiredAmount

	if localPlr then
		instancesToFind = {Animate=true,Humanoid=true,PlayEmote=true,Animator=true,HumanoidDescription=true,HumanoidRootPart=true}
		requiredAmount = 6
	else
		instancesToFind = {Humanoid=true,HumanoidRootPart=true}
		requiredAmount = 2
	end

	local function checkChild(parent)
		local name = parent.Name
		if not instancesToFind[name] then return end
		foundIns[name] = parent
		count += 1
		if count == requiredAmount or not foundIns.HumanoidDescription and requiredAmount == 5 then
			--all instances found
			allInstancesFound = true
			disconnectAll(conns)
			createSystem(foundIns, plrSounds, char, plrId, localPlr)
			--trigger first sound and/or animation 
			local humanoid = foundIns.Humanoid
			local state = humanoid:GetState()
			humanoid:ChangeState(Enum.HumanoidStateType.Seated)
			task.wait()
			humanoid:ChangeState(state)
			return 
		end
		getChildren(conns,parent,checkChild)
	end
	getChildren(conns,char,checkChild)
	
	while task.wait(4) and not allInstancesFound do
		if char and char.Parent then continue end
		disconnectAll(conns)
		return
	end
end
local function playerAdded(plr)
	local char = plr.Character
	local id = plr.UserId

	local plrSounds = {}
	for idx, soundName in {"uuhhh","action_falling","action_get_up","action_jump","action_jump_land","action_footsteps_plastic","impact_water","action_swim"} do
		local sound = Instance.new("Sound")
		sound.Name = soundName
		sound.RollOffMinDistance = 5
		sound.RollOffMaxDistance = 150
		sound.Volume = .65
		sound.SoundId = "rbxasset://sounds/" .. soundName .. ".mp3"
		plrSounds[idx] = sound
	end
	local swim = plrSounds[8]
	swim.Looped = true
	swim.PlaybackSpeed = 1.6

	plrSounds[2].Looped = true--freefall
	plrSounds[6].Looped = true--running
	if char then
		task.spawn(characterAdded, char, id, plrSounds)
	end
	plr.CharacterAdded:Connect(function(char)
		characterAdded(char, id, plrSounds)
	end)
end

for _, plr in Players:GetChildren() do
	playerAdded(plr)
end

Players.PlayerAdded:Connect(playerAdded)
Players.PlayerRemoving:Connect(function(plr)
	plrsFall[plr.UserId] = nil
end)
;(game:FindService("RunService") or game:GetService("RunService")).Heartbeat:Connect(function(dt)
	for id,tab in plrsFall do
		local sound = tab.sound
		if tab.root.AssemblyLinearVelocity.Y > -75 then
			sound.Volume = 0
			continue
		end
		local volume = sound.Volume+.9*dt
		if volume < 1 then
			sound.Volume = volume
			continue
		end
		plrsFall[id] = nil
		sound.Volume = 1
	end
end)
53 Likes

Roblox should switch to this
Their lua scripts are weak

7 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

6 Likes

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

5 Likes

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

2 Likes

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.

2 Likes

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

2 Likes

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

2 Likes

Animate.rbxm (2.0 KB)

2 Likes

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

1 Like

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

1 Like

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.

1 Like

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

2 Likes

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

1 Like

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.

1 Like

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

2 Likes

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.

1 Like

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

1 Like

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.

2 Likes

New version 2.0

Bug fixes

  • When getting services, script could give an error.
  • Events wasn’t disconnected when player fallen into void.
  • some custom animations couldn’t be found by the script
  • Fixed some visual bugs

Improvements

  • Default sounds and animations are no longer stored under script
    • Sounds are created only once by script when a player joins into the game.
    • Animations are created only when it is necessary and then cached
  • Improved code and readability
  • Decrease in code size

Maybe it doesn’t look like a lot of changes, but changes are big almost every part of the code has changed, and it took me a lot of time, but I think that was worth it. For now, script doesn’t have this new strafe animations feature, I’m planing to implement that in the future also I will soon post new benchmarks.

2 Likes