Yo. I don’t post much, but for anyone who wishes to come up with a system like this, I’ll run through it really quick.
I thought the replies under this post were really vague and it took days of research to find any solutions.
The ModuleScript in this post was created with the Animation Converter plugin by samfundev. Click here for the link to the plugin (free),
Why use CFrame animations? Because animations cannot be saved from one place to another if they are owned by separate people. This means they’re needed for any kind of kit with animations, require() scripts, or if the dev wants better control over animations - like updating them in real-time.
Let’s start. It MIGHT NOT be the greatest approach, but it’s a really good place to start from.
Please note that some things may need to be adjusted to work properly, this won’t work on it’s own unless you have a place to both put it and use it. Will need to disable character animations additionally
Create tables for playing keyframes, playing animations, and fading animations. These will be your main sources for the animations
local playingKeyframes = {}
local playing = {}
local fading = {}
-- this is where you put the CFrame animations from the modules. You could add the require(path) or insert manually
local totalAnimations = {}
-- these are useful for comparing priorities. if there is a better way LET ME KNOW!
local priorities = {
Enum.AnimationPriority.Core,
Enum.AnimationPriority.Idle,
Enum.AnimationPriority.Movement,
Enum.AnimationPriority.Action,
Enum.AnimationPriority.Action2,
Enum.AnimationPriority.Action3,
Enum.AnimationPriority.Action4,
}
local core = {
-- these are your core animations. return their names as strings here as an array. these will override current animations. dont include animations which just move an arm or any separate part of the body
}
- I created a table for animation priorities, ordering the highest priority last - useful for comparing weights later on.
- I created three functions - One to play, stopAll, and stop the animation keyframes from running.
local function play(name, fade)
task.spawn(function()
playAnimation(name)
end)
fadeTime(name, fade)
end
local function stop(name, fade, keyframes)
if playing[name] then
playing[name] = false
if keyframes then
local keyframe = getPlayingKeyframe(name)
if keyframe and not keyframe.fade then
removeKeyFrame(keyframe)
end
end
end
end
-- BTW, fade used to be used, but I found a better way recently.
local function stopAll(ignore, fade, keyframes)
for name, value in playing do
if value and (name ~= ignore) then
local default = 0.25
-- stop animation
stop(name, fade or default, keyframes)
end
end
end
Right, now we have the three functions. Next, we would need to call these functions and continue with the next set of functions - managing playing keyframes.
vvv how one would use this
play(“animation name”, 0.25) – fade no longer works, it’s better this way THOUGH
You will still need to create a system to call the character’s animations - it’s not incredibly tough, but if neccessary, I could give an explanation on how that’s done.
- For managing keyframes - which would be updated every frame with a RenderStepped function, we’ll need to create a few more functions.
- The main things to takeaway is that there’ll be ONE keyframe running from a playing animation, and that only the highest priority CFrames from a keyframe will be able to play.
BTW read the comments for any extra information, should help with understanding.
-- this function compares the time duration between two playing animation keyframes
local function compareAdded(time0, time1)
if not time0 then
return false
end
if not time1 then
return true
end
return time0 > time1
end
-- this function returns the index of the animation priority
local function getPriority(priority, time0)
local priority = table.find(priorities, priority)
if (time0 == false) then
return 0
end
return priority
end
-- this compares two priorities to each-other, used for fading
local function comparePriorities(priority, otherPriority, time0, time1, debugName)
local priority0 = getPriority(priority, time0)
local priority1 = getPriority(otherPriority, time1)
if priority0 == priority1 then
return compareAdded(time0, time1)
end
if debugName then
print(priority0 > priority1 and `{debugName} is *greater* than the other anim` or `{debugName} is *less* than the other anim`)
end
return priority0 > priority1
end
-- i reassigned next here. I don't know if there's a better way, but this basically
-- just finds the next index in a dictionary, compatible with decimal numbers.
-- if there is no index specified, it finds the first index.
local function next(keyframes, index)
local closestAfter = 100_000
local first = 100_000
for newIndex, value in keyframes do
if index and (newIndex > index) and (newIndex < closestAfter) then
closestAfter = newIndex
end
if (newIndex < first) then
first = newIndex
end
end
if not index then
return first
end
return closestAfter
end
-- this function gives both currently playing and fading animations
local function getPlayingAndFade()
local total = {}
-- get the fading animations too
for name, value in fading do
if not value then
continue
end
total[name] = value
end
-- get playing animations
for name, value in playing do
if not value then
continue
end
total[name] = value
end
return total
end
-- this function obtains all moving parts in a keyframe, kind-of confusing name, but it'll make sense later
-- some of the assignments were changed like the Torso's hierarchy, because it was confusing originally.
local function getKeyframes(keyframe)
local keyframes = {}
-- this doesn't really matter because there's only one keyframe in it
for rootPart, torso in keyframe.keyframe or keyframe do
-- get torso's keyframes
for _, frames in torso do
-- get the keyframes in torso!
for name, value in frames do
if not name then
keyframes.Torso = value
continue
end
keyframes[name] = value
end
end
end
return keyframes
end
-- returns any given playing keyframe from the animation name. At all times, there should only be one max.
local function getPlayingKeyframe(name)
for _, keyframe in playingKeyframes do
local keyframeName = keyframe.name
local keyframeData = keyframe.keyframe
if keyframeName == name then
return keyframeData, keyframe
end
end
return nil
end
-- this obtains the properties, e.g. looping, priorities, and keyframes, of an animation
local function getProperties(name)
local animation = totalAnimations[name]
if (not name) or (not animation) then
warn(name, "doesn't exist")
return {}, false, Enum.AnimationPriority.Core
end
-- get animation properties
local properties = animation.Properties
local keyframes = animation.Keyframes
-- get
local looping = properties.Looping
local priority = properties.Priority
-- return
return keyframes, looping, priority
end
-- this function gives the parts that are ALLOWED to change from a keyframe.
local function comparePlayingToKeyframe(keyframe, priority)
local playingAndFade = getPlayingAndFade()
local name = keyframe.name
local start = playing[name]
local playable = {}
for name, value in getKeyframes(keyframe) do
local torso = typeof(value) == "CFrame"
local cframe = torso and value or value.CFrame
if torso then
name = "Torso"
end
playable[name] = cframe
end
-- cycle through playing animations
for otherName, otherStart in playingAndFade do
local animation = totalAnimations[otherName]
if not animation then
warn(otherName, "doesn't exist")
continue
end
if otherName == name then
continue
end
local properties = animation.Properties
local otherPriority = properties.Priority
-- confirm that this animation is > our current one
local comparison = comparePriorities(priority, otherPriority, start, otherStart)
if not comparison then
local newKeyframe, keyFrameData = getPlayingKeyframe(otherName)
if not newKeyframe then
continue
end
for otherName, value in getKeyframes(newKeyframe) do
playable[otherName] = nil
end
end
end
return playable
end
-- ^^^ extent of the other function, it's basically the same thing
local function checkPlayingPriority(name, keyframe)
local keyframes, looping, priority = getProperties(name)
local compare = comparePlayingToKeyframe(keyframe, priority)
return compare
end
-- remove the current playing keyframe
local function removeKeyFrame(keyframe)
local find = table.find(playingKeyframes, keyframe)
table.remove(playingKeyframes, find)
end
-- play a keyframe
local function playKeyFrame(name, index, fade)
local keyframes, looping, priority = getProperties(name)
local keyframe = keyframes[index]
local newIndex = next(keyframes, index)
local duration = newIndex - index
if duration > 1000 then
return true
end
-- create the keyframe data
local constructor = {
index = index,
duration = fade or duration,
keyframe = keyframe,
name = name,
fade = fade,
}
-- add a keyframe / wait
table.insert(playingKeyframes, constructor)
task.wait(fade or duration)
-- remove the keyframe
removeKeyFrame(constructor)
end
-- this is where you would end up if you called play().
-- basically, this sets the playing data, gets the animation, and gets properties
-- then, it creates a loop that plays all keyframes in order, and loops IF looped is on
local function playAnimation(name)
if playing[name] and playing[name]~=999999999 then
return
end
-- set playing value
local identifier = os.clock()
playing[name] = identifier
local animation = totalAnimations[name]
local keyframes, looping = getProperties(name)
local index = next(keyframes)
-- create loop / runs once if not looping
while (playing[name] == identifier) do
local cycled = playKeyFrame(name, index)
if cycled then
if (not looping) then
break
end
index = next(keyframes)
else
index = next(keyframes, index)
end
end
end
-- this function simply plays the first keyframe of an animation.. used for fading. it returns '999999999' is used to find out if the animation is already fading. so that the same animation doesn't overlap with each-other
local function playFirstKeyframe(name, fade, identifier)
local keyframes, looping, priority = getProperties(name)
local index = next(keyframes)
if identifier then
playing[name] = 999999999
end
playKeyFrame(name, index, fade)
return playing[name] == 999999999
end
-- seems to fix some bugs with animations - it just does what I described with the core array from up-top
local function disableOtherAnimations(name)
for n, v in core do
if name ~= n then
playing[n] = false
end
end
end
Now that we have our functions which handle keyframes. it is time to figure out HOW we would get the motors from our character. Then, we can figure out how we would move them.
-- this gets all motors inside of the character's body and returns them.
local function getMotor6Ds()
local motor6Ds = {}
-- vv YOU WILL NEED TO DEFINE CHARACTER earlier on, it is from Player.Character.
for _, motor6D in character:GetDescendants() do
if not motor6D:IsA("Motor6D") then
continue
end
table.insert(motor6Ds, motor6D)
end
return motor6Ds
end
-- this cycles through all of the character's motors and finds which ones align with the animations which we have defined from up-top
local function getMotor6DFromName(name)
for _, motor6D in getMotor6Ds() do
local part1 = motor6D.Part1
if part1 and part1.Name == name then
return motor6D
end
end
return nil
end
Now that we have obtained the character’s motors, we need to next change their transformations. Transformation is the offset CFrame which regular ROBLOX animations use to move certain limbs.
We will bind a function to RunService’s RenderStepped event. There is implementation already for this, and we can use the animation priority of 300 to mimic character movement.
Should note that this system isn’t 100% and there’s still a few issues needing to be worked out. I’ve seen some non-core animations stop after a keyframe is played, but it could be due to my handling elsewhere.
-- Here, we check every frame to update the highest priority motors. We disable other animations apart of the core array so they don't override
RunService:BindToRenderStep("Animation", 300, function(delta)
local torsoMotor6D = nil
-- play animations
for _, keyframe in playingKeyframes do
local name = keyframe.name
local fade = keyframe.fade or fading[name]
local duration = fade or keyframe.duration
-- the animation is no longer being played
if (not duration) or (not playing[name] and not fading[name]) then
continue
end
if core[name] then
disableOtherAnimations(name)
end
local playable = checkPlayingPriority(name, keyframe)
for name, value in playable do
local motor6D: Motor6D? = getMotor6DFromName(name)
if not motor6D then
continue
end
if name == "Torso" then
torsoMotor6D = motor6D
end
local alpha = 0.2
-- Only part i'm not 100% on is this. The idea is to lerp from the delta, so 1/60th of a frame, the amount of distance between the two points to make a smooth transition over the given time. This would be 1/delta*duration, if you were curious. For now, I'm leaving it at 0.2, but it causes animations to appear slow when you have a ton of keyframes. Thanks
-- We lerp the motor's transformation to create a smooth transition!
motor6D.Transform = motor6D.Transform:Lerp(value, alpha)
end
end
end)
Thanks, this should have gone over the most important parts, but if there’s any questions y’all still have, please let me know. It helps a lot!
Also if any of the whitespace looks out of place it’s because editing in script editor and in the devforum is different.
TL:DR: Play animations via a custom keyframes system - whereas, you give information about the current keyframe, you wait the duration of the keyframe, and repeat. Update the motors’ transformations using a RenderStepped bind.