Animate 2 - A more memory-efficient alternative

With the help of @InfiniteYield I managed to make a version of Animate 2 that’s compatible with Roblox’s animation packages/bundles (although I was unable to upload it to the marketplace due to an unknown error, possibly due to the fact that this version uses InsertService)

This is what you need to do:

  1. Add a RemoteFunction named AnimationIDRequest to ReplicatedStorage
  2. Add a server Script to ServerScriptService and inside of it write:
--!strict
local InsertService = game:GetService("InsertService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local animationIDRequest = ReplicatedStorage.AnimationIDRequest

local function onAnimationIDRequest(_, assetID: number)
	if typeof(assetID) == "number" then
		local success, model = pcall(InsertService.LoadAsset, InsertService, assetID)

		if success then
			for _, instance in model:GetDescendants() do
				if instance:IsA("Animation") then return instance.AnimationId end
			end
		end
	end
end

animationIDRequest.OnServerInvoke = onAnimationIDRequest
  1. Replace the code inside of my Animate LocalScript with:
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TextChatService = game:GetService("TextChatService")

local DEFAULT_FADE_TIME: number = 0.1

local character: Model = script.Parent
local humanoid = character:WaitForChild("Humanoid"):: Humanoid

local animationTracks: {[string]: AnimationTrack} = {}

do
	local animationIDRequest = ReplicatedStorage:WaitForChild("AnimationIDRequest")

	local animator = humanoid:WaitForChild("Animator"):: Animator

	local humanoidDescription = humanoid:WaitForChild("HumanoidDescription"):: HumanoidDescription

	local animation = Instance.new("Animation")

	local function getAnimationID(descriptionID: number, defaultID: string): string
		if descriptionID == 0 then return defaultID end

		local animationID = animationIDRequest:InvokeServer(descriptionID)

		return if animationID then animationID else defaultID
	end

	local animationData: {[string]: {any}} = {
		Cheer = {"rbxassetid://507770677", Enum.AnimationPriority.Idle},
		Climb = {getAnimationID(humanoidDescription.ClimbAnimation, "rbxassetid://507765644"), Enum.AnimationPriority.Core},
		Dance = {"rbxassetid://507772104", Enum.AnimationPriority.Core},
		Dance2 = {"rbxassetid://507776879", Enum.AnimationPriority.Core},
		Dance3 = {"rbxassetid://507777623", Enum.AnimationPriority.Core},
		Fall = {getAnimationID(humanoidDescription.FallAnimation, "rbxassetid://507767968"), Enum.AnimationPriority.Core},
		Idle = {getAnimationID(humanoidDescription.IdleAnimation, "rbxassetid://507766388"), Enum.AnimationPriority.Core},
		Laugh = {"rbxassetid://507770818", Enum.AnimationPriority.Idle},
		Lunge = {"rbxassetid://522638767", Enum.AnimationPriority.Movement},
		Point = {"rbxassetid://507770453", Enum.AnimationPriority.Idle},
		Run = {getAnimationID(humanoidDescription.RunAnimation, "rbxassetid://913376220"), Enum.AnimationPriority.Core},
		Sit = {"rbxassetid://2506281703", Enum.AnimationPriority.Core},
		Slash = {"rbxassetid://522635514", Enum.AnimationPriority.Movement},
		Swim = {getAnimationID(humanoidDescription.SwimAnimation, "rbxassetid://913384386"), Enum.AnimationPriority.Core},
		SwimIdle = {"rbxassetid://913389285", Enum.AnimationPriority.Core},
		Tool = {"rbxassetid://507768375", Enum.AnimationPriority.Idle},
		Wave = {"rbxassetid://507770239", Enum.AnimationPriority.Idle}}

	for name, data in animationData do
		animation.AnimationId = data[1]

		local animationTrack = animator:LoadAnimation(animation)
		animationTrack.Priority = data[2]

		animationTracks[name] = animationTrack
	end

	animation:Destroy()
end

local animationTrack = animationTracks.Idle
animationTrack:Play(0)

local childAddedConnection: RBXScriptConnection?

local round = math.round

local function play(newAnimationTrack: AnimationTrack, fadeTime: number?)
	if newAnimationTrack.IsPlaying then return end

	local fadeTime = fadeTime or DEFAULT_FADE_TIME

	animationTrack:Stop(fadeTime)
	animationTrack = newAnimationTrack
	animationTrack:Play(fadeTime)
end

local function onClimbing(speed: number)
	play(animationTracks.Climb)

	animationTracks.Climb:AdjustSpeed(round(speed) / 11)
end

local function onFreeFalling(active: boolean)
	if active then play(animationTracks.Fall) end
end

local function onRunning(speed: number)
	local speed = round(speed)

	if speed > 0 then
		play(animationTracks.Run)

		animationTracks.Run:AdjustSpeed(speed / 16)
	else
		play(animationTracks.Idle)
	end
end

local function onSeated(active: boolean)
	if active then play(animationTracks.Sit) end
end

local function onSwimming(speed: number)
	local speed = round(speed)

	if speed > 2 then
		play(animationTracks.Swim)

		animationTracks.Swim:AdjustSpeed(speed / 12)
	else
		play(animationTracks.SwimIdle)
	end
end

local function onChildAdded(child: Instance)
	if child:IsA("Tool") and child:FindFirstChild("Handle") then
		animationTracks.Tool:Play(DEFAULT_FADE_TIME)

		childAddedConnection = child.ChildAdded:Connect(function(child: Instance)
			if child:IsA("StringValue") and child.Name == "toolanim" then
				if child.Value == "Slash" then
					animationTracks.Slash:Play(0)
				elseif child.Value == "Lunge" then
					animationTracks.Lunge:Play(0, 1, 6)
				end

				child:Destroy()
			end
		end)
	end
end

local function onChildRemoved(child: Instance)
	if child:IsA("Tool") and child:FindFirstChild("Handle") then
		if childAddedConnection then
			childAddedConnection:Disconnect()
			childAddedConnection = nil
		end

		animationTracks.Tool:Stop(DEFAULT_FADE_TIME)
	end
end

humanoid.Climbing:Connect(onClimbing)
humanoid.FreeFalling:Connect(onFreeFalling)
humanoid.Running:Connect(onRunning)
humanoid.Seated:Connect(onSeated)
humanoid.Swimming:Connect(onSwimming)
character.ChildAdded:Connect(onChildAdded)
character.ChildRemoved:Connect(onChildRemoved)

if TextChatService.ChatVersion == Enum.ChatVersion.TextChatService
and TextChatService.CreateDefaultCommands
and TextChatService.CreateDefaultTextChannels
then
	local rbxEmoteCommand = Instance.new("TextChatCommand")
	rbxEmoteCommand.Name = "RBXEmoteCommand"
	rbxEmoteCommand.PrimaryAlias = "/emote"
	rbxEmoteCommand.SecondaryAlias = "/e"

	local textChatCommands = TextChatService:WaitForChild("TextChatCommands")
	textChatCommands:WaitForChild("RBXEmoteCommand"):Destroy()
	rbxEmoteCommand.Parent = textChatCommands

	local rbxSystem: TextChannel = TextChatService:WaitForChild("TextChannels"):WaitForChild("RBXSystem")

	rbxEmoteCommand.Triggered:Connect(function(_, unfilteredText: string)
		local emote = string.split(unfilteredText, " ")[2]

		if emote == "cheer" then
			animationTracks.Cheer:Play(DEFAULT_FADE_TIME)
		elseif emote == "dance" then
			play(animationTracks.Dance)
		elseif emote == "dance2" then
			play(animationTracks.Dance2)
		elseif emote == "dance3" then
			play(animationTracks.Dance3)
		elseif emote == "laugh" then
			animationTracks.Laugh.Looped = false
			animationTracks.Laugh:Play(DEFAULT_FADE_TIME)
		elseif emote == "point" then
			animationTracks.Point.Looped = false
			animationTracks.Point:Play(DEFAULT_FADE_TIME)
		elseif emote == "wave" then
			animationTracks.Wave.Looped = false
			animationTracks.Wave:Play(DEFAULT_FADE_TIME)
		else
			rbxSystem:DisplaySystemMessage("<font color='#FF4040'>You do not own that emote.</font>")
		end
	end)
end

Dance2 is playing forever on others’ view for some reason???

The issue I showcased isnt due to custom bundles. I actually just enabled the option to use roblox’s default animation pack, inside of gamesettings (shown below), I expected animation packs not to work, but I am just saying the animation in the animate 2 script for Run is different, and I think uses an older version of the script.

Though to support bundles wouldnt be much difficult and wont require the steps you did provide (which is nice though, by the way), as roblox ApplyDescription actually creates children in the current Animate script with changes to the animations of each type. Example shown in video below. Basically when roblox loads the character if the player has a custom animation bundle and the game allows it, itll place a bunch of IntValues under the Animate script, so you can update Animate 2 just to use those Animations. this will make it more complex, and is actually part of the reason Roblox’s default one is 1k lines almost, as Roblox will first check if the IntValue exists or is added and load the entire list, along with weights to essentially dictate the randomness of a certain animation playing, an example would be Idle animations, which usually have 2 or more per animation bundle, with a basic looping one, and a couple (or at least 1) Idle animation that plays occasionally, like the idle animation where the player looks around and then goes back to the looped idle animation.


Image shows Game Settings in studio with a choice for Standard Animation on player avatars. In my original reply about differences in the run animation was bc the Standard Animation option here has a different Run animation than the default inside of Animate 2.


This video I use basically an empty Animate script to showcase the fact that roblox will place a bunch of IntValues in the Animate script when a HumanoidDescription is applied, and when you change the animations via Humanoid methods to do so. I also showcase that with Standard animations selected, it actually doesnt add any of those IntValues (other than a basic Dampening one I have no clue what it does in the default script), This means the animation ids in the roblox standard Animate script are defaults and Animate 2 uses a different one for Run, as my original reply showcases.

Roblox’s default Animate script also creates children IntValues inside of itself, assumably so different scripts outside of the Animate script can modify the other animations aswell. (Such as it providing a slash animation IntValue, even though animation packs dont have a slash animation)

1 Like

I was able to replicate this issue

I strongly suspect this is a bug since I can verify that Animate 2 was working correctly until the most recent update, so I will make a report as soon as possible

If anyone has more information about this, please let me know. Thank you

1 Like

My intent for Animate 2 is to be a more lightweight, memory-efficient and relatively simple alternative to the default animation script, but this came at a cost of removing some of the functionality as you correctly noted :sweat_smile:

The ideal use-case for Animate 2 are games with a set animation style (as in, predetermined by the game developer), which won’t require the other capabilities the default animation script provides thus will save a bit of memory and hopefully also gain a slight performance improvement by using Animate 2

March 24th, 2024 Update
Bug fix:

  • Found a solution to the problem of animations replicating incorrectly for other players

@sonicdoidao2004 You’ll enjoy this update :grin:


More details about the bug fix:

1 Like

I don’t think this works anymore with Roblox’s new TextChatService. I’ve been looking everywhere for an alternative and nothing worked as good as the Animate script for emotes.

Just now, I’ve ran a test (both in Studio and using the Player), and Animate 2 worked with TextChatService without encountering any issues. Are you experiencing a bug when using Animate 2 with TextChatService enabled?


The emotes I tested were: dance, dance2, dance3, laugh, point, wave, cheer

Hey! Sorry, I should’ve added more to my reply, yes, the default emotes work, however when you try and add more custom emotes (including the normal Animate script) it no longer allows it, I created a post yesterday regarding it. If you figure out a solution it would be awesome :pray:

1 Like

Currently doing my best to try to add custom emote support, but I’m stuck in a dead end due to this error:

Failed to load animation with sanitized ID rbxassetid://3576968026: AnimationClip loaded is not valid.

Which is odd since the animation ID is for an emote created by Roblox (and is equipped in my inventory), which should be exempt from the sanitized ID error. The default animation script doesn’t encounter this error when it loads the animations, so I’m investigating it to try to see how it avoids it

I also tried using Humanoid:PlayEmote, but it kept returning false no matter what I tried

1 Like

It’s so frustrating!! my experience relied a lot on emotes, so I’m not sure what else to do other than create a gui with buttons to temporarily to play them.

1 Like

I’ve just released an update cause I managed to understand how the default animation script was handling emotes (It was using the PlayEmote BindableEvent BindableFunction), and testing custom emotes in Studio seems to be working correctly for me

1 Like

I’ve just tested it, I still seem to have the “You do not own this emote” error in the chat even though it’s a custom animation, I’ve set the id in the script, am I setting it up wrong? Sorry for bothering you.

just added the “bk” as a test

when I type “/e bk” in the chat

1 Like

I understand the issue much better now, but unfortunately this is a situation where trying to fix this issue is creating a new problem

As I previously mentioned (and I learned today while trying to fix this issue), the default animation script uses a BindableFunction to handle emotes. The BindableFunction seems to be invoked from a core script, so I won’t be able to make any changes there. The script that’s invoking the BindableFunction passes either a string, or an animation, as an argument. It passes a string when you use one of the “regular” emotes, and an animation when you use one of the emotes you have equipped to your character

I tried handling emote command logic myself, which worked when playing the “regular” emotes, but kept giving me that error when I tried playing the equipped emotes, and the only solution I found to avoid that error is to use the BindableFunction, which unfortunately displays the “You do not own that emote” warning when you try to play a custom emote

Essentially, I don’t think there’s currently a way to handle “regular”, equipped, and custom emotes combined at the moment, without some kind of error or warning showing up


@iProjectionix

This version might allow you to use your custom emotes, and it also supports the “regular” emotes, but doesn’t support equipped emotes:

--!strict
local TextChatService = game:GetService("TextChatService")

local DEFAULT_FADE_TIME: number = 0.1

local character: Model = script.Parent
local humanoid = character:WaitForChild("Humanoid"):: Humanoid

local animationTracks: {[string]: AnimationTrack} = {}

do
	local animator = humanoid:WaitForChild("Animator"):: Animator

	local animationData: {[string]: {any}} = {
		cheer = {"rbxassetid://507770677", Enum.AnimationPriority.Idle},
		Climb = {"rbxassetid://507765644", Enum.AnimationPriority.Core},
		dance = {"rbxassetid://507772104", Enum.AnimationPriority.Core},
		dance2 = {"rbxassetid://507776879", Enum.AnimationPriority.Core},
		dance3 = {"rbxassetid://507777623", Enum.AnimationPriority.Core},
		Fall = {"rbxassetid://507767968", Enum.AnimationPriority.Core},
		Idle = {"rbxassetid://507766388", Enum.AnimationPriority.Core},
		laugh = {"rbxassetid://507770818", Enum.AnimationPriority.Idle},
		Lunge = {"rbxassetid://522638767", Enum.AnimationPriority.Movement},
		point = {"rbxassetid://507770453", Enum.AnimationPriority.Idle},
		Run = {"rbxassetid://913376220", Enum.AnimationPriority.Core},
		Sit = {"rbxassetid://2506281703", Enum.AnimationPriority.Core},
		Slash = {"rbxassetid://522635514", Enum.AnimationPriority.Movement},
		Swim = {"rbxassetid://913384386", Enum.AnimationPriority.Core},
		SwimIdle = {"rbxassetid://913389285", Enum.AnimationPriority.Core},
		Tool = {"rbxassetid://507768375", Enum.AnimationPriority.Idle},
		wave = {"rbxassetid://507770239", Enum.AnimationPriority.Idle},

		bk = {"rbxassetid://129393686778054", Enum.AnimationPriority.Idle}}

	for name, data in animationData do
		local animation = Instance.new("Animation")
		animation.AnimationId = data[1]

		local animationTrack = animator:LoadAnimation(animation)
		animationTrack.Priority = data[2]

		animationTracks[name] = animationTrack
		animation:Destroy()
	end
end

local animationTrack = animationTracks.Idle
animationTrack:Play(0)

local childAddedConnection: RBXScriptConnection?

local round = math.round

local function play(newAnimationTrack: AnimationTrack, fadeTime: number?)
	if newAnimationTrack.IsPlaying then return end

	local fadeTime = fadeTime or DEFAULT_FADE_TIME

	animationTrack:Stop(fadeTime)
	animationTrack = newAnimationTrack
	animationTrack:Play(fadeTime)
end

local function onClimbing(speed: number)
	play(animationTracks.Climb)

	animationTracks.Climb:AdjustSpeed(round(speed) / 11)
end

local function onFreeFalling(active: boolean)
	if active then play(animationTracks.Fall) end
end

local function onRunning(speed: number)
	local speed = round(speed)

	if speed > 0 then
		play(animationTracks.Run)

		animationTracks.Run:AdjustSpeed(speed / 16)
	else
		play(animationTracks.Idle)
	end
end

local function onSeated(active: boolean)
	if active then play(animationTracks.Sit) end
end

local function onSwimming(speed: number)
	local speed = round(speed)

	if speed > 2 then
		play(animationTracks.Swim)

		animationTracks.Swim:AdjustSpeed(speed / 12)
	else
		play(animationTracks.SwimIdle)
	end
end

local function onChildAdded(child: Instance)
	if child:IsA("Tool") and child:FindFirstChild("Handle") then
		animationTracks.Tool:Play(DEFAULT_FADE_TIME)

		childAddedConnection = child.ChildAdded:Connect(function(child: Instance)
			if child:IsA("StringValue") and child.Name == "toolanim" then
				if child.Value == "Slash" then
					animationTracks.Slash:Play(0)
				elseif child.Value == "Lunge" then
					animationTracks.Lunge:Play(0, 1, 6)
				end

				child:Destroy()
			end
		end)
	end
end

local function onChildRemoved(child: Instance)
	if child:IsA("Tool") and child:FindFirstChild("Handle") then
		if childAddedConnection then
			childAddedConnection:Disconnect()
			childAddedConnection = nil
		end

		animationTracks.Tool:Stop(DEFAULT_FADE_TIME)
	end
end

humanoid.Climbing:Connect(onClimbing)
humanoid.FreeFalling:Connect(onFreeFalling)
humanoid.Running:Connect(onRunning)
humanoid.Seated:Connect(onSeated)
humanoid.Swimming:Connect(onSwimming)
character.ChildAdded:Connect(onChildAdded)
character.ChildRemoved:Connect(onChildRemoved)

if TextChatService.ChatVersion == Enum.ChatVersion.TextChatService
	and TextChatService.CreateDefaultCommands
	and TextChatService.CreateDefaultTextChannels
then
	local rbxEmoteCommand = Instance.new("TextChatCommand")
	rbxEmoteCommand.Name = "RBXEmoteCommand"
	rbxEmoteCommand.PrimaryAlias = "/emote"
	rbxEmoteCommand.SecondaryAlias = "/e"

	local textChatCommands = TextChatService:WaitForChild("TextChatCommands")
	textChatCommands:WaitForChild("RBXEmoteCommand"):Destroy()
	rbxEmoteCommand.Parent = textChatCommands

	local rbxSystem: TextChannel = TextChatService:WaitForChild("TextChannels"):WaitForChild("RBXSystem")

	rbxEmoteCommand.Triggered:Connect(function(_, unfilteredText: string)
		local emote = string.split(unfilteredText, " ")[2]
		local animationTrack = animationTracks[emote]

		if animationTrack then
			if string.find(emote, "dance") then
				play(animationTrack)
			else
				animationTrack.Looped = false
				animationTrack:Play(DEFAULT_FADE_TIME)
			end
		else
			rbxSystem:DisplaySystemMessage("<font color='#FF4040'>You do not own that emote.</font>")
		end
	end)
end

Ahh you didn’t have to do that, it worked like charm, thank you ever so much!!!

1 Like

Hey i am currently using this, and it does not seem to be working, i assume i have done something wrong, Animations are not playing at ALL

It’s quite late where I live at the moment, so I won’t be able to help you until tomorrow

It needs to be named Animate in-order to replace the default animation script, and make sure that it’s a direct child of StarterCharacterScripts, not StarterPlayerScripts or somewhere else

If you’ve made any changes to it, you can try reimporting it from the marketplace

Well, here is the issue aha, i’m respecting all the requirements in order for it to work as it should but turns out it does not. the animations are not loading at all, im just a brick walking

Are there any errors showing up in the output?

If you replaced the default animation IDs with your own, make sure that the animations were published under the same person or group that owns the game, otherwise they’ll fail to load

I also recommend temporarily disabling any other scripts which are handling the player’s animations, if you have any, as one of them could be blocking Animate 2 from running correctly

Also make sure that you don’t have any plugins running which edit the source code of scripts, as they could be accidentally breaking it

One last recommendation: If you have set Workspace’s SandboxedInstanceMode to Experimental, you’ll need to set Animate 2’s permissions in-order for it to be able to work

What’s wrong with the default one?