animation-system

Roblox Animation Systems

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "animation-system" with this command: npx skills add taozhuo/game-dev-skills/taozhuo-game-dev-skills-animation-system

Roblox Animation Systems

When implementing animations, follow these patterns for smooth, performant character and object animations.

Animation Basics

Loading and Playing Animations

local function setupAnimations(character) local humanoid = character:WaitForChild("Humanoid") local animator = humanoid:WaitForChild("Animator")

-- Create animation instance
local walkAnim = Instance.new("Animation")
walkAnim.AnimationId = "rbxassetid://123456789"

-- Load animation track
local walkTrack = animator:LoadAnimation(walkAnim)

-- Configure track
walkTrack.Priority = Enum.AnimationPriority.Movement
walkTrack.Looped = true

-- Play with parameters
walkTrack:Play(
    0.1,  -- Fade in time
    1,    -- Weight (0-1)
    1     -- Speed multiplier
)

return walkTrack

end

Animation Priorities

-- Priority order (lowest to highest): -- Core < Idle < Movement < Action < Action2 < Action3 < Action4

local function setAnimationPriority(track, priority) track.Priority = priority end

-- Example priority usage idleTrack.Priority = Enum.AnimationPriority.Idle walkTrack.Priority = Enum.AnimationPriority.Movement attackTrack.Priority = Enum.AnimationPriority.Action -- Action always overrides Movement, Movement overrides Idle

Animation Events (Keyframe Markers)

-- Add markers in Animation Editor, then listen: local function setupAnimationEvents(track) -- Listen for specific marker track:GetMarkerReachedSignal("Footstep"):Connect(function(paramValue) playFootstepSound() end)

track:GetMarkerReachedSignal("DamageFrame"):Connect(function()
    applyDamage()
end)

track:GetMarkerReachedSignal("SpawnVFX"):Connect(function(vfxName)
    spawnEffect(vfxName)
end)

end

-- Animation completion track.Stopped:Connect(function() print("Animation stopped or completed") end)

-- Check if playing if track.IsPlaying then -- Animation is active end

Animation Controller

State-Based Animation Controller

local AnimationController = {} AnimationController.__index = AnimationController

function AnimationController.new(character) local self = setmetatable({}, AnimationController)

self.character = character
self.humanoid = character:WaitForChild("Humanoid")
self.animator = self.humanoid:WaitForChild("Animator")
self.tracks = {}
self.currentState = "Idle"
self.stateAnimations = {}

return self

end

function AnimationController:loadAnimation(name, animationId, config) config = config or {}

local animation = Instance.new("Animation")
animation.AnimationId = animationId

local track = self.animator:LoadAnimation(animation)
track.Priority = config.priority or Enum.AnimationPriority.Movement
track.Looped = config.looped or false

self.tracks[name] = track
return track

end

function AnimationController:setState(stateName, fadeTime) fadeTime = fadeTime or 0.1

if self.currentState == stateName then return end

-- Stop current state animation
local currentTrack = self.tracks[self.currentState]
if currentTrack and currentTrack.IsPlaying then
    currentTrack:Stop(fadeTime)
end

-- Play new state animation
local newTrack = self.tracks[stateName]
if newTrack then
    newTrack:Play(fadeTime)
end

self.currentState = stateName

end

function AnimationController:playOneShot(name, fadeTime, weight, speed) local track = self.tracks[name] if track then track:Play(fadeTime or 0.1, weight or 1, speed or 1) end return track end

-- Usage local controller = AnimationController.new(character) controller:loadAnimation("Idle", "rbxassetid://idle", {looped = true, priority = Enum.AnimationPriority.Idle}) controller:loadAnimation("Walk", "rbxassetid://walk", {looped = true, priority = Enum.AnimationPriority.Movement}) controller:loadAnimation("Attack", "rbxassetid://attack", {priority = Enum.AnimationPriority.Action})

controller:setState("Idle") -- When moving: controller:setState("Walk") -- Attack (plays on top): controller:playOneShot("Attack")

Movement-Based Animation Selection

local function setupMovementAnimations(character) local humanoid = character:WaitForChild("Humanoid") local animator = humanoid:WaitForChild("Animator") local hrp = character:WaitForChild("HumanoidRootPart")

local animations = {
    idle = loadAnimation(animator, "rbxassetid://idle"),
    walk = loadAnimation(animator, "rbxassetid://walk"),
    run = loadAnimation(animator, "rbxassetid://run"),
    jump = loadAnimation(animator, "rbxassetid://jump"),
    fall = loadAnimation(animator, "rbxassetid://fall")
}

-- Set looping
animations.idle.Looped = true
animations.walk.Looped = true
animations.run.Looped = true
animations.fall.Looped = true

local currentAnim = nil

local function updateAnimation()
    local velocity = hrp.AssemblyLinearVelocity
    local horizontalSpeed = Vector3.new(velocity.X, 0, velocity.Z).Magnitude
    local isGrounded = humanoid.FloorMaterial ~= Enum.Material.Air

    local targetAnim

    if not isGrounded then
        if velocity.Y > 1 then
            targetAnim = animations.jump
        else
            targetAnim = animations.fall
        end
    elseif horizontalSpeed &#x3C; 0.5 then
        targetAnim = animations.idle
    elseif horizontalSpeed &#x3C; 12 then
        targetAnim = animations.walk
        -- Adjust speed based on movement
        animations.walk:AdjustSpeed(horizontalSpeed / 8)
    else
        targetAnim = animations.run
        animations.run:AdjustSpeed(horizontalSpeed / 16)
    end

    if targetAnim ~= currentAnim then
        if currentAnim then
            currentAnim:Stop(0.2)
        end
        targetAnim:Play(0.2)
        currentAnim = targetAnim
    end
end

RunService.Heartbeat:Connect(updateAnimation)

end

Animation Blending

Weight-Based Blending

local BlendedAnimator = {}

function BlendedAnimator.new(animator) return { animator = animator, layers = {} } end

function BlendedAnimator:addLayer(name, animationId, priority) local animation = Instance.new("Animation") animation.AnimationId = animationId

local track = self.animator:LoadAnimation(animation)
track.Priority = priority or Enum.AnimationPriority.Movement
track.Looped = true

self.layers[name] = {
    track = track,
    weight = 0,
    targetWeight = 0
}

track:Play(0, 0)  -- Start at weight 0
return track

end

function BlendedAnimator:setLayerWeight(name, weight, blendTime) local layer = self.layers[name] if not layer then return end

layer.targetWeight = math.clamp(weight, 0, 1)

if blendTime and blendTime > 0 then
    -- Smooth blend
    local startWeight = layer.weight
    local startTime = os.clock()

    local conn
    conn = RunService.Heartbeat:Connect(function()
        local elapsed = os.clock() - startTime
        local t = math.min(elapsed / blendTime, 1)

        layer.weight = startWeight + (layer.targetWeight - startWeight) * t
        layer.track:AdjustWeight(layer.weight)

        if t >= 1 then
            conn:Disconnect()
        end
    end)
else
    layer.weight = layer.targetWeight
    layer.track:AdjustWeight(layer.weight)
end

end

-- Usage: Blend between walk and limp local blender = BlendedAnimator.new(animator) blender:addLayer("Walk", "rbxassetid://walk", Enum.AnimationPriority.Movement) blender:addLayer("Limp", "rbxassetid://limp", Enum.AnimationPriority.Movement)

-- Normal walking blender:setLayerWeight("Walk", 1, 0.3) blender:setLayerWeight("Limp", 0, 0.3)

-- Injured (blend to limp) blender:setLayerWeight("Walk", 0.3, 0.5) blender:setLayerWeight("Limp", 0.7, 0.5)

Additive Animation Blending

-- Additive animations add on top of base animation local function setupAdditiveBlending(animator) local baseWalk = loadAnimation(animator, "rbxassetid://walk") local leanLeft = loadAnimation(animator, "rbxassetid://lean_left") local leanRight = loadAnimation(animator, "rbxassetid://lean_right")

baseWalk.Looped = true
leanLeft.Looped = true
leanRight.Looped = true

baseWalk:Play()
leanLeft:Play(0, 0)  -- Start at 0 weight
leanRight:Play(0, 0)

-- Update lean based on input
local function updateLean(turnAmount)
    -- turnAmount: -1 (left) to 1 (right)
    if turnAmount &#x3C; 0 then
        leanLeft:AdjustWeight(math.abs(turnAmount))
        leanRight:AdjustWeight(0)
    else
        leanLeft:AdjustWeight(0)
        leanRight:AdjustWeight(turnAmount)
    end
end

return updateLean

end

Procedural Animation

Procedural Head Look

local function setupHeadLook(character, target) local neck = character:FindFirstChild("Neck", true) if not neck then return end

local originalC0 = neck.C0

RunService.RenderStepped:Connect(function()
    if not target then
        neck.C0 = originalC0
        return
    end

    local headPos = neck.Part1.Position
    local targetPos = target.Position
    local direction = (targetPos - headPos).Unit

    -- Convert to local space
    local torsoLook = neck.Part0.CFrame.LookVector
    local torsoCFrame = neck.Part0.CFrame

    local localDirection = torsoCFrame:VectorToObjectSpace(direction)

    -- Calculate angles
    local yaw = math.atan2(localDirection.X, -localDirection.Z)
    local pitch = math.asin(localDirection.Y)

    -- Clamp to prevent unnatural rotation
    yaw = math.clamp(yaw, math.rad(-70), math.rad(70))
    pitch = math.clamp(pitch, math.rad(-40), math.rad(40))

    -- Apply rotation
    local lookCFrame = CFrame.Angles(pitch, yaw, 0)
    neck.C0 = originalC0 * lookCFrame
end)

end

Procedural Breathing

local function setupBreathing(character) local torso = character:FindFirstChild("UpperTorso") or character:FindFirstChild("Torso") if not torso then return end

local waist = character:FindFirstChild("Waist", true)
if not waist then return end

local originalC0 = waist.C0
local breathSpeed = 2  -- Cycles per second
local breathIntensity = 0.02

local time = 0

RunService.RenderStepped:Connect(function(dt)
    time = time + dt

    local breathOffset = math.sin(time * breathSpeed * math.pi * 2) * breathIntensity

    waist.C0 = originalC0 * CFrame.new(0, breathOffset, 0)
end)

end

Procedural Tail/Cape Physics

local function setupProceduralChain(parts, config) config = config or {} local stiffness = config.stiffness or 0.5 local damping = config.damping or 0.3 local gravity = config.gravity or Vector3.new(0, -10, 0)

local velocities = {}
local restOffsets = {}

-- Store rest positions
for i, part in ipairs(parts) do
    velocities[i] = Vector3.new()
    if i > 1 then
        restOffsets[i] = parts[i-1].CFrame:ToObjectSpace(part.CFrame)
    end
end

RunService.Heartbeat:Connect(function(dt)
    for i = 2, #parts do
        local part = parts[i]
        local parent = parts[i-1]

        -- Target position (relative to parent)
        local targetCFrame = parent.CFrame * restOffsets[i]
        local targetPos = targetCFrame.Position

        -- Current position
        local currentPos = part.Position

        -- Spring force toward target
        local displacement = targetPos - currentPos
        local springForce = displacement * stiffness

        -- Apply gravity
        local totalForce = springForce + gravity

        -- Update velocity with damping
        velocities[i] = velocities[i] * (1 - damping) + totalForce * dt

        -- Update position
        local newPos = currentPos + velocities[i]

        -- Maintain distance constraint
        local toParent = parent.Position - newPos
        local distance = toParent.Magnitude
        local restDistance = restOffsets[i].Position.Magnitude

        if distance > restDistance then
            newPos = parent.Position - toParent.Unit * restDistance
        end

        -- Apply
        part.CFrame = CFrame.new(newPos) * (targetCFrame - targetCFrame.Position)
    end
end)

end

Inverse Kinematics (IK)

Two-Bone IK (Arms/Legs)

local function solveTwoBoneIK(upperBone, lowerBone, target, pole) local upperLength = (lowerBone.Position - upperBone.Position).Magnitude local lowerLength = (target - lowerBone.Position).Magnitude

local origin = upperBone.Position
local targetPos = target
local polePos = pole or (origin + Vector3.new(0, 0, 1))

-- Calculate distance to target
local targetDistance = (targetPos - origin).Magnitude
local totalLength = upperLength + lowerLength

-- Clamp target to reachable distance
if targetDistance > totalLength * 0.999 then
    targetDistance = totalLength * 0.999
end

-- Law of cosines to find angles
local a = upperLength
local b = lowerLength
local c = targetDistance

-- Angle at upper joint
local upperAngle = math.acos(
    math.clamp((a*a + c*c - b*b) / (2*a*c), -1, 1)
)

-- Angle at lower joint (elbow/knee)
local lowerAngle = math.acos(
    math.clamp((a*a + b*b - c*c) / (2*a*b), -1, 1)
)

-- Direction to target
local directionToTarget = (targetPos - origin).Unit

-- Calculate pole plane
local poleDirection = (polePos - origin).Unit
local cross = directionToTarget:Cross(poleDirection)
local normal = cross:Cross(directionToTarget).Unit

-- Apply rotations
local upperRotation = CFrame.fromAxisAngle(cross, -upperAngle)
local elbowPosition = origin + upperRotation:VectorToWorldSpace(directionToTarget) * upperLength

return elbowPosition, lowerAngle

end

-- Foot IK for terrain local function setupFootIK(character) local humanoid = character:WaitForChild("Humanoid") local hrp = character:WaitForChild("HumanoidRootPart")

local leftFoot = character:FindFirstChild("LeftFoot")
local rightFoot = character:FindFirstChild("RightFoot")
local leftLeg = character:FindFirstChild("LeftLowerLeg")
local rightLeg = character:FindFirstChild("RightLowerLeg")

local rayParams = RaycastParams.new()
rayParams.FilterDescendantsInstances = {character}

RunService.RenderStepped:Connect(function()
    if humanoid.FloorMaterial == Enum.Material.Air then return end

    -- Raycast for each foot
    for _, footData in ipairs({{leftFoot, leftLeg}, {rightFoot, rightLeg}}) do
        local foot, lowerLeg = footData[1], footData[2]

        local result = workspace:Raycast(
            foot.Position + Vector3.new(0, 1, 0),
            Vector3.new(0, -2, 0),
            rayParams
        )

        if result then
            local targetY = result.Position.Y
            local offset = targetY - foot.Position.Y + 0.1

            -- Apply IK offset (simplified)
            -- In practice, you'd solve the full IK chain
        end
    end
end)

end

Custom Rigs

Motor6D Setup for Custom Rigs

local function createCustomRig(model) local root = model.PrimaryPart local parts = {}

for _, part in ipairs(model:GetDescendants()) do
    if part:IsA("BasePart") and part ~= root then
        table.insert(parts, part)
    end
end

-- Create Motor6Ds
local motors = {}

for _, part in ipairs(parts) do
    local motor = Instance.new("Motor6D")
    motor.Name = part.Name

    -- Find parent part (closest connected part toward root)
    local parentPart = findParentPart(part, root, parts)

    motor.Part0 = parentPart
    motor.Part1 = part

    -- Calculate C0 and C1 (joint positions)
    local jointPos = (parentPart.Position + part.Position) / 2
    motor.C0 = parentPart.CFrame:ToObjectSpace(CFrame.new(jointPos))
    motor.C1 = part.CFrame:ToObjectSpace(CFrame.new(jointPos))

    motor.Parent = parentPart
    motors[part.Name] = motor
end

return motors

end

-- Animate custom rig local function animateCustomRig(motors, animationData) -- animationData: {motorName = {CFrame sequence}}

local time = 0
local duration = animationData.duration or 1

RunService.RenderStepped:Connect(function(dt)
    time = (time + dt) % duration
    local t = time / duration

    for motorName, keyframes in pairs(animationData.motors or {}) do
        local motor = motors[motorName]
        if motor then
            -- Interpolate between keyframes
            local transform = interpolateKeyframes(keyframes, t)
            motor.Transform = transform
        end
    end
end)

end

Humanoid Description for NPCs

local function applyHumanoidDescription(character, description) local humanoid = character:FindFirstChildOfClass("Humanoid") if not humanoid then return end

-- Create or modify description
local desc = description or Instance.new("HumanoidDescription")

-- Body parts
desc.Head = 123456789  -- Asset ID
desc.Torso = 123456789
desc.LeftArm = 123456789
desc.RightArm = 123456789
desc.LeftLeg = 123456789
desc.RightLeg = 123456789

-- Animations
desc.IdleAnimation = 123456789
desc.WalkAnimation = 123456789
desc.RunAnimation = 123456789
desc.JumpAnimation = 123456789
desc.FallAnimation = 123456789

-- Body scales
desc.HeadScale = 1
desc.BodyTypeScale = 0.5
desc.ProportionScale = 1
desc.WidthScale = 1
desc.HeightScale = 1
desc.DepthScale = 1

humanoid:ApplyDescription(desc)

end

Animation Performance

Animation Caching

local AnimationCache = {} AnimationCache.cache = {}

function AnimationCache.load(animator, animationId) local cacheKey = tostring(animator) .. "_" .. animationId

if AnimationCache.cache[cacheKey] then
    return AnimationCache.cache[cacheKey]
end

local animation = Instance.new("Animation")
animation.AnimationId = animationId

local track = animator:LoadAnimation(animation)
AnimationCache.cache[cacheKey] = track

return track

end

function AnimationCache.clear(animator) local prefix = tostring(animator) .. "_"

for key, track in pairs(AnimationCache.cache) do
    if string.sub(key, 1, #prefix) == prefix then
        track:Stop()
        track:Destroy()
        AnimationCache.cache[key] = nil
    end
end

end

LOD for Animations

local AnimationLOD = {}

function AnimationLOD.setup(character, camera) local animator = character:WaitForChild("Humanoid"):WaitForChild("Animator") local hrp = character:WaitForChild("HumanoidRootPart")

local LOD_DISTANCES = {50, 100, 200}
local UPDATE_RATES = {1, 0.5, 0.25, 0.1}  -- Animation update rate

local lastUpdate = 0
local currentLOD = 1

RunService.Heartbeat:Connect(function()
    local distance = (hrp.Position - camera.CFrame.Position).Magnitude

    -- Determine LOD level
    local lodLevel = 1
    for i, threshold in ipairs(LOD_DISTANCES) do
        if distance > threshold then
            lodLevel = i + 1
        end
    end

    -- Update animation rate based on LOD
    if lodLevel ~= currentLOD then
        currentLOD = lodLevel

        -- Adjust all playing animations
        for _, track in ipairs(animator:GetPlayingAnimationTracks()) do
            -- Distant characters: slower animation updates
            -- This is a simplified approach; Roblox handles this internally
        end
    end
end)

end

Pooled Animation Tracks

local TrackPool = {} TrackPool.pools = {}

function TrackPool.getTrack(animator, animationId) local poolKey = animationId

if not TrackPool.pools[poolKey] then
    TrackPool.pools[poolKey] = {
        available = {},
        inUse = {}
    }
end

local pool = TrackPool.pools[poolKey]

-- Check for available track
local track = table.remove(pool.available)

if not track then
    -- Create new track
    local animation = Instance.new("Animation")
    animation.AnimationId = animationId
    track = animator:LoadAnimation(animation)
end

table.insert(pool.inUse, track)
return track

end

function TrackPool.releaseTrack(animationId, track) local pool = TrackPool.pools[animationId] if not pool then return end

track:Stop(0)

local index = table.find(pool.inUse, track)
if index then
    table.remove(pool.inUse, index)
end

table.insert(pool.available, track)

end

Animation Tools

Animation Recording

local AnimationRecorder = {}

function AnimationRecorder.record(character, duration) local humanoid = character:FindFirstChildOfClass("Humanoid") local motors = {}

-- Find all Motor6Ds
for _, motor in ipairs(character:GetDescendants()) do
    if motor:IsA("Motor6D") then
        table.insert(motors, motor)
    end
end

local keyframes = {}
local startTime = os.clock()
local recording = true

-- Record at 30 fps
local frameTime = 1/30
local lastFrame = 0

local conn
conn = RunService.Heartbeat:Connect(function()
    local elapsed = os.clock() - startTime

    if elapsed >= duration then
        recording = false
        conn:Disconnect()
        return
    end

    if elapsed - lastFrame >= frameTime then
        lastFrame = elapsed

        local frame = {
            time = elapsed,
            poses = {}
        }

        for _, motor in ipairs(motors) do
            frame.poses[motor.Name] = {
                C0 = motor.C0,
                C1 = motor.C1,
                Transform = motor.Transform
            }
        end

        table.insert(keyframes, frame)
    end
end)

-- Return promise-like
return {
    getKeyframes = function()
        while recording do
            task.wait()
        end
        return keyframes
    end
}

end

Animation Playback from Data

local function playRecordedAnimation(character, keyframes) local motors = {}

for _, motor in ipairs(character:GetDescendants()) do
    if motor:IsA("Motor6D") then
        motors[motor.Name] = motor
    end
end

local duration = keyframes[#keyframes].time
local startTime = os.clock()

local conn
conn = RunService.Heartbeat:Connect(function()
    local elapsed = os.clock() - startTime

    if elapsed >= duration then
        conn:Disconnect()
        return
    end

    -- Find surrounding keyframes
    local prevFrame, nextFrame
    for i, frame in ipairs(keyframes) do
        if frame.time &#x3C;= elapsed then
            prevFrame = frame
            nextFrame = keyframes[i + 1]
        end
    end

    if not prevFrame or not nextFrame then return end

    -- Interpolate
    local t = (elapsed - prevFrame.time) / (nextFrame.time - prevFrame.time)

    for motorName, motor in pairs(motors) do
        local prevPose = prevFrame.poses[motorName]
        local nextPose = nextFrame.poses[motorName]

        if prevPose and nextPose then
            motor.Transform = prevPose.Transform:Lerp(nextPose.Transform, t)
        end
    end
end)

return conn

end

Procedural KeyframeSequence Generation

This section covers creating animations entirely from code without using the Animation Editor.

KeyframeSequence Structure

KeyframeSequences contain Keyframes, which contain hierarchical Poses matching the rig structure.

--[[ KeyframeSequence Structure:

KeyframeSequence
├── Keyframe (Time = 0.0)
│   └── Pose "HumanoidRootPart"
│       └── Pose "LowerTorso"
│           ├── Pose "UpperTorso"
│           │   ├── Pose "Head"
│           │   ├── Pose "LeftUpperArm"
│           │   │   └── Pose "LeftLowerArm"
│           │   │       └── Pose "LeftHand"
│           │   └── Pose "RightUpperArm"
│           │       └── Pose "RightLowerArm"
│           │           └── Pose "RightHand"
│           ├── Pose "LeftUpperLeg"
│           │   └── Pose "LeftLowerLeg"
│           │       └── Pose "LeftFoot"
│           └── Pose "RightUpperLeg"
│               └── Pose "RightLowerLeg"
│                   └── Pose "RightFoot"
├── Keyframe (Time = 0.5)
│   └── ... (same hierarchy)
└── Keyframe (Time = 1.0)
    └── ... (same hierarchy)

]]

R15 Rig Hierarchy

The pose hierarchy MUST match the R15 rig structure for animations to work:

local R15_HIERARCHY = { HumanoidRootPart = { LowerTorso = { UpperTorso = { Head = {}, LeftUpperArm = { LeftLowerArm = { LeftHand = {} } }, RightUpperArm = { RightLowerArm = { RightHand = {} } }, }, LeftUpperLeg = { LeftLowerLeg = { LeftFoot = {} } }, RightUpperLeg = { RightLowerLeg = { RightFoot = {} } }, } } }

Motor6D Locations in R15

Motor6Ds are stored in the CHILD part, not the parent:

Motor6D Name Located In Connects

Root LowerTorso HumanoidRootPart → LowerTorso

Waist UpperTorso LowerTorso → UpperTorso

Neck Head UpperTorso → Head

LeftShoulder LeftUpperArm UpperTorso → LeftUpperArm

RightShoulder RightUpperArm UpperTorso → RightUpperArm

LeftElbow LeftLowerArm LeftUpperArm → LeftLowerArm

RightElbow RightLowerArm RightUpperArm → RightLowerArm

LeftWrist LeftHand LeftLowerArm → LeftHand

RightWrist RightHand RightLowerArm → RightHand

LeftHip LeftUpperLeg LowerTorso → LeftUpperLeg

RightHip RightUpperLeg LowerTorso → RightUpperLeg

LeftKnee LeftLowerLeg LeftUpperLeg → LeftLowerLeg

RightKnee RightLowerLeg RightUpperLeg → RightLowerLeg

LeftAnkle LeftFoot LeftLowerLeg → LeftFoot

RightAnkle RightFoot RightLowerLeg → RightFoot

R15 Motor6D Rotation Directions (Verified)

CRITICAL: These rotation directions were verified through actual testing. All rotations are in radians.

RightShoulder (in RightUpperArm)

  • X+: Forward/down (arm rotates forward toward chest)

  • X-: Back/up (arm rotates backward)

  • Y+: Twist arm inward (palm faces down)

  • Y-: Twist arm outward (palm faces up)

  • Z+: Raise arm sideways (abduction)

  • Z-: Lower arm (adduction)

LeftShoulder (in LeftUpperArm)

  • X+: Forward/down

  • X-: Back/up

  • Y+: Twist outward

  • Y-: Twist inward

  • Z+: Lower arm

  • Z-: Raise arm sideways

RightElbow (in RightLowerArm)

  • X+: Bend elbow (forearm toward bicep)

  • X-: Extend elbow (straighten arm)

LeftElbow (in LeftLowerArm)

  • X+: Bend elbow

  • X-: Extend elbow

Waist (in UpperTorso)

  • X+: Lean forward (bow)

  • X-: Lean backward

  • Y+: Twist left

  • Y-: Twist right

  • Z+: Tilt left

  • Z-: Tilt right

RightHip (in RightUpperLeg)

  • X+: Leg backward (behind body)

  • X-: Leg forward (kick forward)

  • Z+: Leg outward (spread)

  • Z-: Leg inward

LeftHip (in LeftUpperLeg)

  • X+: Leg backward

  • X-: Leg forward

  • Z+: Leg inward

  • Z-: Leg outward

RightKnee (in RightLowerLeg)

  • X+: Bend knee (foot toward buttocks)

  • X-: Extend knee (straighten leg)

LeftKnee (in LeftLowerLeg)

  • X+: Bend knee

  • X-: Extend knee

Neck (in Head)

  • X+: Look down

  • X-: Look up

  • Y+: Turn left

  • Y-: Turn right

  • Z+: Tilt head left

  • Z-: Tilt head right

Building KeyframeSequence Programmatically

local function rad(d) return math.rad(d) end

local function buildPoseHierarchy(hierarchy, rotations, parentPose) for jointName, children in pairs(hierarchy) do local pose = Instance.new("Pose") pose.Name = jointName pose.Weight = 1

    local rot = rotations[jointName]
    if rot then
        -- rot = {X, Y, Z} in degrees
        pose.CFrame = CFrame.Angles(rad(rot[1]), rad(rot[2]), rad(rot[3]))
    else
        pose.CFrame = CFrame.new()
    end

    pose.Parent = parentPose

    if children and next(children) then
        buildPoseHierarchy(children, rotations, pose)
    end
end

end

local function createKeyframeSequence(name, priority, keyframes) local sequence = Instance.new("KeyframeSequence") sequence.Name = name sequence.Loop = false sequence.Priority = priority or Enum.AnimationPriority.Action

for _, kfData in ipairs(keyframes) do
    local keyframe = Instance.new("Keyframe")
    keyframe.Time = kfData.time
    buildPoseHierarchy(R15_HIERARCHY, kfData.poses, keyframe)
    sequence:AddKeyframe(keyframe)
end

return sequence

end

Example: Sword Slash Animation

local slashSequence = createKeyframeSequence("SwordSlash", Enum.AnimationPriority.Action, { -- Wind up: rotate torso left, arm back { time = 0.0, poses = { UpperTorso = {0, -30, 0}, -- Twist right (wind up) RightUpperArm = {-90, -45, -90}, -- Arm back and raised RightLowerArm = {-45, 0, 0}, -- Elbow bent }}, -- Mid swing { time = 0.15, poses = { UpperTorso = {0, 45, 0}, -- Twist left (swing through) RightUpperArm = {-45, 90, -90}, -- Arm coming down RightLowerArm = {0, 0, 0}, -- Elbow extending }}, -- Follow through { time = 0.3, poses = { UpperTorso = {0, 60, 0}, -- Full twist left RightUpperArm = {30, 90, -45}, -- Arm across body RightLowerArm = {0, 0, 0}, }}, -- Return to neutral { time = 0.5, poses = { UpperTorso = {0, 0, 0}, RightUpperArm = {0, 0, 0}, RightLowerArm = {0, 0, 0}, }}, })

Playing Animations Without Publishing (Studio Testing)

Use KeyframeSequenceProvider:RegisterKeyframeSequence() to create temporary animation IDs for testing in Studio without needing to publish:

local KeyframeSequenceProvider = game:GetService("KeyframeSequenceProvider")

local function playProceduralAnimation(animator, sequence) -- Register returns a temporary hash ID like "rbxassetid://12345" local hashId = KeyframeSequenceProvider:RegisterKeyframeSequence(sequence)

local animation = Instance.new("Animation")
animation.AnimationId = hashId

local track = animator:LoadAnimation(animation)
track:Play()

return track

end

-- Usage local character = player.Character local humanoid = character:WaitForChild("Humanoid") local animator = humanoid:FindFirstChildOfClass("Animator") if not animator then animator = Instance.new("Animator") animator.Parent = humanoid end

local track = playProceduralAnimation(animator, slashSequence)

Complete Tool Animation Example

A complete example that creates tools and plays animations on activation:

--[[ Complete Animation Tool - Place in StarterPlayerScripts as LocalScript ]]

local Players = game:GetService("Players") local KeyframeSequenceProvider = game:GetService("KeyframeSequenceProvider")

local player = Players.LocalPlayer

local function rad(d) return math.rad(d) end

local R15_HIERARCHY = { HumanoidRootPart = { LowerTorso = { UpperTorso = { Head = {}, LeftUpperArm = { LeftLowerArm = { LeftHand = {} } }, RightUpperArm = { RightLowerArm = { RightHand = {} } }, }, LeftUpperLeg = { LeftLowerLeg = { LeftFoot = {} } }, RightUpperLeg = { RightLowerLeg = { RightFoot = {} } }, } } }

local function buildPoseHierarchy(hierarchy, rotations, parentPose) for jointName, children in pairs(hierarchy) do local pose = Instance.new("Pose") pose.Name = jointName pose.Weight = 1 local rot = rotations[jointName] if rot then pose.CFrame = CFrame.Angles(rad(rot[1]), rad(rot[2]), rad(rot[3])) else pose.CFrame = CFrame.new() end pose.Parent = parentPose if children and next(children) then buildPoseHierarchy(children, rotations, pose) end end end

local function createAnimation(name, priority, keyframes) local sequence = Instance.new("KeyframeSequence") sequence.Name = name sequence.Loop = false sequence.Priority = priority for _, kf in ipairs(keyframes) do local keyframe = Instance.new("Keyframe") keyframe.Time = kf.time buildPoseHierarchy(R15_HIERARCHY, kf.poses, keyframe) sequence:AddKeyframe(keyframe) end return sequence end

-- Define animations with verified rotations local punchSequence = createAnimation("Punch", Enum.AnimationPriority.Action, { { time = 0.0, poses = { UpperTorso = {0, -20, 0}, RightUpperArm = {-45, -30, -45}, RightLowerArm = {-90, 0, 0} }}, { time = 0.1, poses = { UpperTorso = {0, 30, 0}, RightUpperArm = {-90, 60, -90}, RightLowerArm = {-10, 0, 0} }}, { time = 0.2, poses = { UpperTorso = {0, 45, 0}, RightUpperArm = {-90, 90, -90}, RightLowerArm = {0, 0, 0} }}, { time = 0.4, poses = { UpperTorso = {0, 0, 0}, RightUpperArm = {0, 0, 0}, RightLowerArm = {0, 0, 0} }}, })

local function setup() local character = player.Character or player.CharacterAdded:Wait() local humanoid = character:WaitForChild("Humanoid") local animator = humanoid:FindFirstChildOfClass("Animator") if not animator then animator = Instance.new("Animator") animator.Parent = humanoid end

-- Register and load animation
local hashId = KeyframeSequenceProvider:RegisterKeyframeSequence(punchSequence)
local animation = Instance.new("Animation")
animation.AnimationId = hashId
local track = animator:LoadAnimation(animation)

-- Create tool
local tool = Instance.new("Tool")
tool.Name = "Punch"
tool.RequiresHandle = true
tool.CanBeDropped = false

local handle = Instance.new("Part")
handle.Name = "Handle"
handle.Size = Vector3.new(1, 1, 1)
handle.Transparency = 1
handle.Parent = tool

tool.Parent = player:WaitForChild("Backpack")

local debounce = false
tool.Activated:Connect(function()
    if debounce then return end
    debounce = true
    track:Play()
    task.wait(0.5)
    debounce = false
end)

end

player.CharacterAdded:Connect(function() task.wait(1) setup() end) if player.Character then setup() end

Smooth Animation with Many Keyframes

For fluid animations, use 20-30 keyframes with small incremental changes:

-- Generates a smooth punch animation with 26 frames local function generateSmoothPunch() local frames = {} local totalTime = 0.7

-- Phase 1: Wind up (0.0 - 0.15s)
for i = 0, 5 do
    local t = i / 5 * 0.15
    local progress = i / 5
    table.insert(frames, {
        time = t,
        poses = {
            UpperTorso = {0, -20 * progress, 0},
            RightUpperArm = {-45 * progress, -30 * progress, -45 * progress},
            RightLowerArm = {-90 * progress, 0, 0}
        }
    })
end

-- Phase 2: Strike (0.15 - 0.35s)
for i = 1, 10 do
    local t = 0.15 + (i / 10 * 0.2)
    local progress = i / 10
    table.insert(frames, {
        time = t,
        poses = {
            UpperTorso = {0, -20 + 65 * progress, 0},
            RightUpperArm = {-45 - 45 * progress, -30 + 120 * progress, -45 - 45 * progress},
            RightLowerArm = {-90 + 90 * progress, 0, 0}
        }
    })
end

-- Phase 3: Recovery (0.35 - 0.7s)
for i = 1, 10 do
    local t = 0.35 + (i / 10 * 0.35)
    local progress = i / 10
    table.insert(frames, {
        time = t,
        poses = {
            UpperTorso = {0, 45 * (1 - progress), 0},
            RightUpperArm = {-90 * (1 - progress), 90 * (1 - progress), -90 * (1 - progress)},
            RightLowerArm = {0, 0, 0}
        }
    })
end

return createAnimation("SmoothPunch", Enum.AnimationPriority.Action, frames)

end

Animation Easing Functions

Apply easing for natural movement:

local Easing = {}

function Easing.easeInOut(t) return t < 0.5 and 2 * t * t or 1 - (-2 * t + 2)^2 / 2 end

function Easing.easeOut(t) return 1 - (1 - t)^3 end

function Easing.easeIn(t) return t * t * t end

-- Apply to keyframe generation local function interpolateWithEasing(startValue, endValue, t, easingFunc) local easedT = easingFunc(t) return startValue + (endValue - startValue) * easedT end

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

audio-system

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

vfx-effects

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

optimization

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

game-systems

No summary provided by upstream source.

Repository SourceNeeds Review