Skip to content

Instantly share code, notes, and snippets.

@MaximumADHD
Last active February 21, 2026 17:33
Show Gist options
  • Select an option

  • Save MaximumADHD/5eda577a9e0ba4611f3fe2fe6ae72790 to your computer and use it in GitHub Desktop.

Select an option

Save MaximumADHD/5eda577a9e0ba4611f3fe2fe6ae72790 to your computer and use it in GitHub Desktop.
Rewrite of Roblox's Animate script to be more flexible/robust and easier to read.
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Animate
-- maximum_adhd
-- September 21st, 2022
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
--!strict
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local player = Players.LocalPlayer -- NOTE: nil if running on the server!
local parent: Instance = assert(script.Parent, "Animate script has no parent!")
local character = assert(if parent:IsA("Model") then parent else nil, "Parent of Animate must be a Model!")
local humanoid = assert(character:FindFirstChildOfClass("Humanoid"), "No Humanoid found for the Animate script to use!")
local animator = humanoid:FindFirstChildOfClass("Animator")
if not animator then
if RunService:IsServer() then
animator = Instance.new("Animator")
assert(animator).Parent = humanoid
else
-- Unsafe, but inherited legacy behavior.
animator = humanoid :: any
end
end
local defaultHipHeight = 2
local defaultFadeTime = 0.3
local smallButNotZero = 1e-4
local baseRunSpeed = 16 / 1.25
local baseWalkSpeed = 8 / 1.25
local overlapParams = OverlapParams.new()
overlapParams.FilterDescendantsInstances = { character }
overlapParams.MaxParts = 5
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Animation Data
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
type AnimDef = {
Id: string,
Weight: number,
Animation: Animation?,
Track: AnimationTrack?,
}
type AnimSet = {
TotalWeight: number,
[number]: AnimDef,
}
type TrackController = {
Speed: number?,
Weight: number?,
FadeTime: number?,
TimePosition: number?,
Priority: Enum.AnimationPriority?,
CustomSet: AnimSet,
DefaultSet: AnimSet,
ActiveTrack: AnimationTrack?,
}
type Locomotion = {
Velocity: Vector2,
Speed: number,
}
local self = {
Animations = {} :: {
[string]: TrackController,
},
Defaults = {} :: {
[string]: { AnimDef },
},
Emotes = {} :: {
[string]: boolean,
},
Callbacks = {} :: {
[Enum.HumanoidStateType]: () -> (),
},
DidLoop = {} :: {
[AnimationTrack]: boolean,
},
LocomotionMap = {} :: {
[string]: Locomotion,
},
}
self.Emotes = {
wave = false,
point = false,
dance = true,
dance2 = true,
dance3 = true,
laugh = false,
cheer = false,
}
self.Defaults = {
idle = {
{ Id = "rbxassetid://507766666", Weight = 1 },
{ Id = "rbxassetid://507766951", Weight = 1 },
{ Id = "rbxassetid://507766388", Weight = 9 },
},
walk = {
{ Id = "rbxassetid://507777826", Weight = 10 },
},
run = {
{ Id = "rbxassetid://507767714", Weight = 10 },
},
swim = {
{ Id = "rbxassetid://507784897", Weight = 10 },
},
swimidle = {
{ Id = "rbxassetid://507785072", Weight = 10 },
},
jump = {
{ Id = "rbxassetid://507765000", Weight = 10 },
},
fall = {
{ Id = "rbxassetid://507767968", Weight = 10 },
},
climb = {
{ Id = "rbxassetid://507765644", Weight = 10 },
},
sit = {
{ Id = "rbxassetid://2506281703", Weight = 10 },
},
toolnone = {
{ Id = "rbxassetid://507768375", Weight = 10 },
},
toolslash = {
{ Id = "rbxassetid://522635514", Weight = 10 },
},
toollunge = {
{ Id = "rbxassetid://522638767", Weight = 10 },
},
wave = {
{ Id = "rbxassetid://507770239", Weight = 10 },
},
point = {
{ Id = "rbxassetid://507770453", Weight = 10 },
},
dance = {
{ Id = "rbxassetid://507771019", Weight = 10 },
{ Id = "rbxassetid://507771955", Weight = 10 },
{ Id = "rbxassetid://507772104", Weight = 10 },
},
dance2 = {
{ Id = "rbxassetid://507776043", Weight = 10 },
{ Id = "rbxassetid://507776720", Weight = 10 },
{ Id = "rbxassetid://507776879", Weight = 10 },
},
dance3 = {
{ Id = "rbxassetid://507777268", Weight = 10 },
{ Id = "rbxassetid://507777451", Weight = 10 },
{ Id = "rbxassetid://507777623", Weight = 10 },
},
laugh = {
{ Id = "rbxassetid://507770818", Weight = 10 },
},
cheer = {
{ Id = "rbxassetid://507770677", Weight = 10 },
},
}
self.LocomotionMap = {
run = {
Speed = baseRunSpeed,
Velocity = Vector2.yAxis * baseRunSpeed,
},
walk = {
Speed = baseWalkSpeed,
Velocity = Vector2.yAxis * baseWalkSpeed,
},
}
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Helper Functions
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
local function newAnimSet(): AnimSet
return { TotalWeight = 0 }
end
local function signedAngle(a: Vector2, b: Vector2)
return -math.atan2(a.X * b.Y - a.Y * b.X, a.X * b.X + a.Y * b.Y)
end
local function registerCallback(stateType: Enum.HumanoidStateType, callback: () -> ())
self.Callbacks[stateType] = callback
end
local function getTrackController(name: string): TrackController
local controller = self.Animations[name]
return assert(controller, "Unknown animation name: " .. tostring(name))
end
local function getNumber(object: Instance, key: string): number?
local valueObject = object:FindFirstChild(key)
if valueObject and valueObject:IsA("NumberValue") then
return valueObject.Value
end
local attribute = object:GetAttribute(key)
if typeof(attribute) == "number" then
return attribute
end
return nil
end
local function getHeightScale(): number
if not humanoid.AutomaticScalingEnabled then
return 1
end
local dampening = getNumber(script, "ScaleDampeningPercent")
if dampening then
return 1 + (humanoid.HipHeight - defaultHipHeight) * dampening / defaultHipHeight
else
return humanoid.HipHeight / defaultHipHeight
end
end
local function getTimePosition(controller: TrackController): number?
local track = controller.ActiveTrack
if track and track.IsPlaying and track.WeightTarget > smallButNotZero then
return track.TimePosition
end
return
end
local function loadAnimation(animDef: AnimDef): AnimationTrack
if not animDef.Track then
local anim = animDef.Animation
if anim == nil then
anim = Instance.new("Animation")
assert(anim)
anim.AnimationId = assert(animDef.Id)
animDef.Animation = anim
end
-- Debug flag to use the Humanoid for loading animations
-- so NetworkSettings.ShowActiveAnimationAsset works.
-- TODO: That should just work with Animator...?
local animator: Animator = if script:GetAttribute("DebugHumanoidLoadAnimation")
then humanoid :: any
else animator
local track = animator:LoadAnimation(assert(anim))
track.Priority = Enum.AnimationPriority.Core
track.DidLoop:Connect(function()
self.DidLoop[track] = true
end)
animDef.Track = track
end
return assert(animDef.Track)
end
local function getAnimSetForAnimation(customAnim: Animation): (AnimSet?, string?)
local set = customAnim.Parent
if set and set:IsA("StringValue") then
if set.Parent ~= script then
return
end
local id = set.Name
local controller = self.Animations[id]
if not controller then
controller = {
DefaultSet = newAnimSet(),
CustomSet = newAnimSet(),
}
self.Animations[id] = controller
end
return controller.CustomSet, id
end
return
end
local function rollAnimation(controller: TrackController): (AnimationTrack?, boolean?)
local animSet = if #controller.CustomSet == 0
then controller.DefaultSet
else controller.CustomSet
if #animSet == 1 then
local first = animSet[1]
return loadAnimation(first), false
elseif #animSet == 0 then
return nil, nil
end
local totalWeight = assert(animSet.TotalWeight)
local roll = math.random() * totalWeight
local currWeight = 0
local currAnim = 1
while currWeight < totalWeight do
local animDef = assert(animSet[currAnim])
local nextWeight = currWeight + animDef.Weight
if roll >= currWeight and roll < nextWeight then
return loadAnimation(animDef), true
end
currAnim += 1
currWeight = nextWeight
end
return nil, nil
end
local function canEmote()
local moveDir = humanoid.MoveDirection
local state = humanoid:GetState()
if moveDir ~= Vector3.zero then
return false
end
if state ~= Enum.HumanoidStateType.Running then
return false
end
return true
end
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Animation Set Registration
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
local function checkAnimationAdded(anim: Instance)
if anim:IsA("Animation") then
local animSet, setId = getAnimSetForAnimation(anim)
if animSet then
local linearVelocity = anim:GetAttribute("LinearVelocity")
local weight = getNumber(anim, "Weight") or 1
local animDef: AnimDef = {
Id = anim.AnimationId,
Animation = anim,
Weight = weight,
}
animSet.TotalWeight += weight
loadAnimation(animDef)
if setId and typeof(linearVelocity) == "Vector2" then
self.LocomotionMap[setId] = {
Velocity = linearVelocity,
Speed = linearVelocity.Magnitude,
}
end
table.insert(animSet, animDef)
end
end
end
local function checkAnimationRemoving(anim: Instance)
if anim:IsA("Animation") then
local animSet = getAnimSetForAnimation(anim)
if animSet then
local dropIndex: number?
for i, animDef in ipairs(animSet) do
if animDef.Animation == anim then
animSet.TotalWeight -= animDef.Weight
dropIndex = i
break
end
end
if dropIndex then
table.remove(animSet, dropIndex)
end
end
end
end
-- Register default animations
for id, animDefs in self.Defaults do
local default = newAnimSet()
for i, animDef in animDefs do
default.TotalWeight += animDef.Weight
table.insert(default, animDef)
loadAnimation(animDef)
end
self.Animations[id] = {
DefaultSet = default,
CustomSet = newAnimSet(),
}
end
-- Register custom animations
for i, desc: Instance in script:GetDescendants() do
checkAnimationAdded(desc)
end
script.DescendantAdded:Connect(checkAnimationAdded)
script.DescendantRemoving:Connect(checkAnimationRemoving)
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Humanoid State Callbacks
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
local normalizedWalkSpeed = 0.5
local normalizedRunSpeed = 1
local toolAnimEvent: string?
local currentEmote: string?
local jumpAnimTime = 0
local toolAnimTime = 0
local function onJumping()
jumpAnimTime = os.clock() + 0.4
end
local function onFreefall()
local now = os.clock()
local target = if now < jumpAnimTime
then getTrackController("jump")
else getTrackController("fall")
target.Weight = 1
target.FadeTime = 0.1
end
local function onSeated()
local sit = getTrackController("sit")
sit.FadeTime = 0.4
sit.Weight = 1
sit.Speed = 0
end
local function onClimbing()
local rootPart = humanoid.RootPart
local speed = 0
if rootPart then
local velocity = rootPart.AssemblyLinearVelocity
speed = velocity.Y / 5
end
local climb = getTrackController("climb")
climb.Speed = speed
climb.Weight = 1
end
local function onSwimming()
local rootPart = humanoid.RootPart
local speed: number = 0
if rootPart then
local velocity = rootPart.AssemblyLinearVelocity
speed = velocity.Magnitude
end
local swim = getTrackController("swim")
swim.FadeTime = 0.4
local swimIdle = getTrackController("swimidle")
swimIdle.FadeTime = 0.4
if speed > 1 then
swim.Speed = speed / 10
swim.Weight = 1
else
swimIdle.Weight = 1
end
end
local function onRunning()
local rootPart = humanoid.RootPart
local strafe = Vector2.zero
local speed = 0
if rootPart then
local rootHeight = rootPart.Size.Y / 2
local hipHeight = humanoid.HipHeight
local floorOffset = Vector3.yAxis * (hipHeight + rootHeight)
local floor = rootPart.Position - floorOffset
local parts = workspace:GetPartBoundsInRadius(floor, 1, overlapParams)
local velocity = rootPart.AssemblyLinearVelocity
local compensated = {}
for i, part in parts do
local root = assert(if part:IsA("BasePart")
then part.AssemblyRootPart
else nil)
if not compensated[root] then
velocity -= root:GetVelocityAtPosition(floor)
compensated[root] = true
end
end
local cf = rootPart.CFrame
local strafeX = cf.RightVector:Dot(velocity)
local strafeY = cf.LookVector:Dot(velocity)
strafe = Vector2.new(strafeX, strafeY)
speed = strafe.Magnitude
end
local heightScale = getHeightScale()
local runSpeed = (speed / baseRunSpeed) / heightScale
if speed > heightScale / 2 then
local fadeInRun = (runSpeed - normalizedWalkSpeed) / (normalizedRunSpeed - normalizedWalkSpeed)
local runAnimSpeed = math.max(1, math.log(fadeInRun + 1))
local runAnimWeight = math.clamp(fadeInRun, smallButNotZero, 1)
local walkAnimWeight = math.clamp(if runAnimWeight == smallButNotZero
then runSpeed / normalizedWalkSpeed
else 1 - fadeInRun,
smallButNotZero,
1
)
local activeMotion = {}
local lower, upper = false, false
local loX, loY = false, false
local upX, upY = false, false
for id, locomotion in self.LocomotionMap do
local vel = locomotion.Velocity
if strafe:Dot(vel) > 0 then
activeMotion[id] = locomotion
end
if not lower then
loX = loX or vel.X < 0
loY = loY or vel.Y < 0
lower = loX and loY
end
if not upper then
upX = upX or vel.X > 0
upY = upY or vel.Y > 0
upper = upX and upY
end
end
if lower and upper then
local timePos = 0
local runWeights = {}
local runTotalWeight = 0
local walkWeights = {}
local walkTotalWeight = 0
for id, motion in activeMotion do
local vel = motion.Velocity
local angle = signedAngle(strafe, vel)
local weight = math.clamp(1 - math.abs(angle), 0, 1)
local motionSpeed = motion.Speed
local runDist = math.abs(motionSpeed - baseRunSpeed)
local walkDist = math.abs(motionSpeed - baseWalkSpeed)
if runDist < walkDist then
runWeights[id] = weight
runTotalWeight += weight
else
walkWeights[id] = weight
walkTotalWeight += weight
end
local controller = getTrackController(id)
local trackTimePos = getTimePosition(controller)
if trackTimePos then
timePos = math.max(timePos, trackTimePos)
end
end
for runId, runWeight in runWeights do
local run = getTrackController(runId)
local weight = (runWeight / runTotalWeight) * runAnimWeight
run.Weight = math.clamp(weight, smallButNotZero, 1)
run.TimePosition = timePos
run.Speed = runAnimSpeed
end
for walkId, walkWeight in walkWeights do
local walk = getTrackController(walkId)
local weight = (walkWeight / walkTotalWeight) * walkAnimWeight
walk.Weight = math.clamp(weight, smallButNotZero, 1)
walk.TimePosition = timePos
end
else
-- Not all quadrants of motion represented!
-- Fallback to legacy run/walk behavior.
local run = getTrackController("run")
run.Weight = runAnimWeight
run.Speed = runAnimSpeed
local walk = getTrackController("walk")
walk.Weight = walkAnimWeight
end
else
local idle = getTrackController("idle")
idle.Weight = 1
end
end
local function onPlayEmote(anim: Animation)
if typeof(anim) ~= "Instance" then
return false
end
if not anim:IsA("Animation") then
return false
end
if not canEmote() then
return false
end
local assetId: string? = anim.AnimationId:match("%d+$")
if not assetId then
return false
end
local emoteId = "emote_" .. assert(assetId)
local emote = self.Animations[emoteId]
if emote == nil then
emote = {
DefaultSet = newAnimSet(),
CustomSet = newAnimSet(),
}
table.insert(emote.DefaultSet, {
Id = "rbxassetid://" .. assetId,
Animation = anim,
Weight = 1,
})
self.Animations[emoteId] = emote
end
currentEmote = emoteId
return true
end
local function onChatted(msg: string)
local emote
if msg:sub(1, 3) == "/e " then
emote = msg:sub(4)
elseif msg:sub(1, 7) == "/emote " then
emote = msg:sub(8)
end
if self.Emotes[emote] ~= nil and canEmote() then
currentEmote = emote
end
end
-- Jumping is fast, listen directly!
humanoid.Jumping:Connect(onJumping)
if player then
-- Listen for emote chat messages.
player.Chatted:Connect(onChatted)
end
task.spawn(function()
-- PlayEmote is dispatched via BindableFunction.
local playEmote = script:WaitForChild("PlayEmote", math.huge)
if playEmote and playEmote:IsA("BindableFunction") then
playEmote.OnInvoke = onPlayEmote
end
end)
-- Register everything else as a state update callback.
registerCallback(Enum.HumanoidStateType.Seated, onSeated)
registerCallback(Enum.HumanoidStateType.Running, onRunning)
registerCallback(Enum.HumanoidStateType.Climbing, onClimbing)
registerCallback(Enum.HumanoidStateType.Freefall, onFreefall)
registerCallback(Enum.HumanoidStateType.Swimming, onSwimming)
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Runtime Logic
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Clear any existing animation tracks
-- Fixes issue with characters that are moved in and out of the Workspace accumulating tracks
assert(animator)
for i, track: AnimationTrack in animator:GetPlayingAnimationTracks() do
track:Stop(0)
track:Destroy()
end
local function updateToolAnimation(tool: Tool)
local now = os.clock()
local targetAnim = "toolnone"
local toolanim = tool:FindFirstChild("toolanim")
if toolanim and toolanim:IsA("StringValue") then
toolAnimEvent = toolanim.Value
toolAnimTime = now + 0.3
toolanim.Parent = nil
end
if now < toolAnimTime then
if toolAnimEvent == "Slash" then
targetAnim = "toolslash"
elseif toolAnimEvent == "Lunge" and not humanoid.Sit then
targetAnim = "toollunge"
end
elseif toolAnimEvent then
toolAnimEvent = nil
end
local toolControl = getTrackController(targetAnim)
toolControl.Priority = Enum.AnimationPriority.Action
toolControl.FadeTime = 0.1
toolControl.Weight = 1
end
local function updateAnimations()
local tool = character:FindFirstChildWhichIsA("Tool")
if tool and tool.RequiresHandle then
updateToolAnimation(tool)
end
if currentEmote and canEmote() then
local emote = getTrackController(currentEmote)
emote.Weight = 1
else
local state = humanoid:GetState()
local callback = self.Callbacks[state]
if callback then
callback()
end
currentEmote = nil
end
for id, controller in self.Animations do
local speed = controller.Speed
local weight = controller.Weight
local fadeTime = controller.FadeTime
local track = controller.ActiveTrack
if speed or weight then
local validTrack = if track
then track.IsPlaying
else false
local newTrack: AnimationTrack?
local rollable: boolean?
if validTrack and track and self.DidLoop[track] then
newTrack, rollable = rollAnimation(controller)
if rollable then
validTrack = false
end
self.DidLoop[track] = nil
end
if not validTrack then
if track and track ~= newTrack then
track:Stop(fadeTime or defaultFadeTime)
end
track = newTrack or rollAnimation(controller)
controller.ActiveTrack = track
end
if not track then
continue
end
local timePos = controller.TimePosition
assert(track)
if track.IsPlaying then
local priority = controller.Priority
if weight then
controller.Weight = nil
if weight == smallButNotZero and track.WeightTarget ~= smallButNotZero then
track:AdjustWeight(smallButNotZero, fadeTime or defaultFadeTime)
elseif math.abs(weight - track.WeightTarget) > 0.01 then
track:AdjustWeight(weight, fadeTime or defaultFadeTime)
end
end
if speed then
controller.Speed = nil
if math.abs(speed - track.Speed) > 0.01 and track.WeightTarget > smallButNotZero then
track:AdjustSpeed(speed)
end
end
if priority then
track.Priority = priority
controller.Priority = nil
end
if timePos then
track.TimePosition = timePos
end
else
track:Play(fadeTime or defaultFadeTime, weight or 1, speed or 1)
track.TimePosition = timePos or 0
end
if timePos then
controller.TimePosition = nil
end
elseif track and track.IsPlaying then
track:Stop(fadeTime or defaultFadeTime)
end
if fadeTime then
controller.FadeTime = nil
end
end
end
RunService.Heartbeat:Connect(updateAnimations)
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
@HedTB
Copy link
Copy Markdown

HedTB commented Oct 3, 2022

me when the

@vrs2210
Copy link
Copy Markdown

vrs2210 commented Aug 7, 2024

the

@2049dev
Copy link
Copy Markdown

2049dev commented Mar 30, 2025

this is like 100 lines more than the default one but good job

@vrs2210
Copy link
Copy Markdown

vrs2210 commented Mar 31, 2025

@2049dev It doesn't matter if it has more lines, it is as the title says, it's flexible/robust and easier to read.

@MaximumADHD
Copy link
Copy Markdown
Author

might not be as performant though lol

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment