Procedural Animation: What it is and how to do it

Introduction
This tutorial assumes you are familiar with basic animation terminology, basic animation theory and basic CFrame math operations. You should also be familiar enough with programming to adapt examples into full systems.
If you want to know about procedural animation in general and some use cases for applying it, this GDC talk by David Rosen is both easy to understand and in-depth.
Procedural animation, in Roblox, is a method of animation that doesn’t rely on animation tracks and instead uses CFrames. It can be used to completely replace Roblox’s built in animation features, or to augment it. Two good examples of procedural animation being used are Strife and the view models in Phantom Forces.

History
Procedural animation has traditionally taken advantage of linear interpretation, with the CFrame:Lerp function. This function, combined with a loop and some math, allows for easing of the linear path.
This method of procedural animation is most utilized in games like Black Magic 1 and Strife. Both of these games were killed off when FilteringEnabled was forced, but have recently been revived by @nylium. I’m not exactly sure what he’s changed under the hood, so the animation may not work the exact same way now.
Procedural animation of this type is also common in “script fighters.” If you’re unaware, these are scripts that create a “fighter” from scratch, they use procedural animation instead of animation tracks because it allows animations to play in any game, the owner does not need to own the animation.
As for examples of augmented procedural animation, if you’ve ever seen a character “tilt” to the direction you move, this is a product of augmented procedural animation. A good example of this is Black Magic Two.

The Benefits of Procedural Animation
Procedural animation allows for your game’s animations to have a more “natural” look. This is due to the fact that procedural animations don’t have a “Frame 0 Keyframe.” If you’ve ever seen a game where your character attacks, then goes back to their neutral position, then starts playing the walk cycle animation again, this is due to the fact that animations always need to move to the first frame (frame 0) of an animation before it plays. Hiding this is called “blending,” or smoothing the transition from one animation to another. Roblox’s animation system does not natively have a robust blending system. The easing style is always linear, and all you, the programmer, are able to dictate is how long the transition takes.
Animation:Play(1) would mean the transition from the last animation to the one you’re playing would take one second. You can set this to 0 to have animations instantly start playing from the last, with no blending. Black Magic 2 does this, which is a large part of it’s signature feel.
However, blending is inherently more robust in procedural animation systems. In procedural animation, the code only cares about the end pose, or “goal.” It doesn’t care about what pose the character is in before the animation started, it just cares about getting the character to the end pose.
This behavior means that, assuming we are able to create easing styles and easing directions, we can have much more control over the blending process.

Drawbacks of Procedural Animation
The main three drawbacks of procedural animation are replication, complexity, and tediousness.
This tutorial will not go over replication, I might update it later once I figure out a nice way to do it.
The complexity of procedural animation is the main reason why you don’t see it more. Anybody with a youtube tutorial open can create an animation, load it into the character’s Humanoid, and play it. Procedural animation requires knowledge of Scripting, CFrames, :Lerp Or, even better, tweenservice. We’ll get to that, an input detection system, and other specifics I’m glossing over.
Additionally, even if one is able to animate this way, the tediousness means many don’t. Polishing animations by playing them over and over is annoying, playtesting over and over to make small tweaks is maddening.

The Glorious game:GetService(“TweenService”) and It’s Odd Way of Tweening CFrames.
A quick look at the API site’s entry for TweenService we’ll be greeted with some good news, TweenService is able to tween CFrames! This means we won’t need to tinker around with

for per = 0.00, 1.00, wait() do
startpart.CFrame = startcf:lerp(endcf, per)
wait()
end

“Lerp loops.” However, the method for tweening CFrames with TweenService is a bit odd. Essentially, you have to tween a CFrameValue’s value, have a function that fires whenever the CFrameValue’s value changes, and changes the CFrame you want to tween’s value to the CFrameValue’s value value value value. If that didn’t make much sense, this might:

local ts = game:GetService("TweenService") --Getting the TweenService.
local info = TweenInfo.new(.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out, 0, false, 0) --Creating the characteristics of how the part will move. Go look at the API site if you want to know more.
local CFrameValue = Instance.new("CFrameValue") --This is that "CFrameValue" I was talking about.

local goal = {} --The "end position" for the CFrame you'll be tweening.
goal.Value = CFRAMEYOUWANTTOTWEEN * CFRAMEYOUWANTTOTWEENITTO

CFrameValue.Changed:Connect(function() --This is that function I was talking about, since TweenService can only tween CFrameValue.Value, we'll just detect whenever that changes and set the CFrame we want to tween to that.
	CFRAMEYOUWANTTOTWEEN = CFrameValue.Value
end)

local tween = tweenservice:Create(CFrameValue, info, goal) --Creating the tween. This is called a "TweenBase."
tween:Play() --Playing the tween.
tween:Cancel() --Cancelling the tween
tween:Pause() --Pausing the tween.

In this example, CFRAMEYOUWANTTOTWEEN is, well, the CFrame you want to tween. Such as game.Players.LocalPlayer.Character.Torso['Root Hip'].C0
By using the C0 of a motor6D, we can allow for regular animations to play at the same time as procedural ones, which allows for augmenting such as in Black Magic 2.
CFRAMEYOUWANTTOTWEENITTO is the CFrame(s) you want to tween CFRAMEYOUWANTTOTWEEN by. If you know about CFrame Math Operations, you’ll know how this works. If you don’t, please read this.
Make sure when using CFrame.Angles(x,y,z) you’re not forgetting that CFrames use radians instead of degrees, so you’ll have to do CFrame.Angles(math.rad(x), math.rad(y), math.rad(z)) to convert degrees into radians. Unless you like using radians I guess.

Creating An Animation With TweenService
Firstly, let’s put what we just learned above into affect. Let’s create a local script that is able to tween the Root Joint of the player. This example is for R6.

local player = game.Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local tweenservice = game:GetService("TweenService")
local info = TweenInfo.new(.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out, 0, false, 0)
local CFrameValue = Instance.new("CFrameValue")
local savedBaseCFrame = character.HumanoidRootPart["RootJoint"].C0 --It's a good idea to have a "default" base to multiply by instead of the current CFrame's value. This will prevent weird behavior.

local goal = {}
goal.Value =savedBaseCFrame * CFrame.Angles(math.rad(0), math.rad(0), math.rad(0))

CFrameValue.Changed:Connect(function()
	character.HumanoidRootPart['RootJoint'].C0 = CFrameValue.Value
end)

local tween = tweenservice:Create(CFrameValue, info, goal)

Okay! I added savedBaseCFrame to prevent some weird behavior. If you want to know what this weird behavior is, experiment!
Now that we have the framework in place to animate with TweenService, let’s go over the method of creating the actual animation and playing it. We can avoid creating a ton of tweens by simply editing one tween over and over.
Here is the method to doing that:

tween:Cancel() -- Stop the tween if it's currently playing.
goal.Value = savedBaseCFrame * CFrame.Angles(math.rad(0), math.rad(45), 0) --Editing the goal, this is equivalent to creating a keyframe.
tween = tweenservice:Create(CFrameValue, info, goal) --Same thing as loading an animation, except we have to do this every keyframe.
tween:Play() --Playing the keyframe.

Now we have?.. The first keyframe. Let’s just create a second keyframe and wrap the animation up.

tween:Cancel() -- Stop the tween if it's currently playing.
goal.Value = savedBaseCFrame * CFrame.Angles(math.rad(0), math.rad(45), 0) --Editing the goal, this is equivalent to creating a keyframe.
tween = tweenservice:Create(CFrameValue, info, goal) --Same thing as loading an animation, except we have to do this every keyframe.
tween:Play() --Playing the keyframe.

tween.Completed:Wait() --Waiting for the first keyframe to be reached.

tween:Cancel() -- Stop the tween if it's currently playing. Just for safety.
goal.Value = savedBaseCFrame * CFrame.Angles(math.rad(0), math.rad(0), 0) --Creating the second keyframe.
info = TweenInfo.new(1, Enum.EasingStyle.Circular, Enum.EasingDirection.InOut, 0, false, 0) --Editing the easing style, direction, and length of the second keyframe.

tween = tweenservice:Create(CFrameValue, info, goal) --Loading the second keyframe.
tween:Play() --Playing the second keyframe.

tween.Completed:Wait() --Waiting for the second keyframe to be reached.

Cool! You can repeat this process for as many keyframes as you need.

The Finished Example
Below is a local script you can use to see what this all does. I suggest just putting it in game.StarterPlayer.StarterCharacterScripts
Wait 10 seconds and the tween will play.

local player = game.Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local tweenservice = game:GetService("TweenService")
local info = TweenInfo.new(.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out, 0, false, 0)
local CFrameValue = Instance.new("CFrameValue")
local savedBaseCFrame = character.HumanoidRootPart["RootJoint"].C0 --It's a good idea to have a "default" base to multiply by instead of the current CFrame's value. This will prevent weird behavior.

local goal = {}
goal.Value =savedBaseCFrame * CFrame.Angles(math.rad(0), math.rad(0), math.rad(0))

CFrameValue.Changed:Connect(function()
	character.HumanoidRootPart['RootJoint'].C0 = CFrameValue.Value
end)

local tween = tweenservice:Create(CFrameValue, info, goal)

wait(5)
print("Tweening")


tween:Cancel() -- Stop the tween if it's currently playing.
goal.Value = savedBaseCFrame * CFrame.Angles(math.rad(0), math.rad(45), 0) --Editing the goal, this is equivalent to creating a keyframe.
tween = tweenservice:Create(CFrameValue, info, goal) --Same thing as loading an animation, except we have to do this every keyframe.
tween:Play() --Playing the keyframe.

tween.Completed:Wait() --Waiting for the first keyframe to be reached.

tween:Cancel() -- Stop the tween if it's currently playing. Just for safety.
goal.Value = savedBaseCFrame * CFrame.Angles(math.rad(0), math.rad(0), 0) --Creating the second keyframe.
info = TweenInfo.new(1, Enum.EasingStyle.Circular, Enum.EasingDirection.InOut, 0, false, 0) --Editing the easing style, direction, and length of the second keyframe.

tween = tweenservice:Create(CFrameValue, info, goal) --Loading the second keyframe.
tween:Play() --Playing the second keyframe.
print("Finished tweening")

Last updated: 5/8/2020
If you have anything to add, feel free to post it in the comments! I’m specifically interested in different ideas people have for replication, as mine would require using :Lerp() and TweenService.

183 Likes

Very helpful tutorial, even though I have no idea what you said or mean, but hey, I’ll learn. Thanks for the guide!

20 Likes

Thanks for the tutorial! I’ve never heard about procedural animation (I’m not a pro animator, so I don’t know very much about it, just the basics), but you explained it very well and it seems to be a nice way to animate things! Really good tutorial!

P.S. I would add some videos/photos to show how it looks like and to make the tutorial more attractive

11 Likes

Nice article, now I feel motivated to continue fleshing out my old custom animator I made months ago, will probably make a Class for it so it’s a lot easier to handle and if it works well, will probably open source it.

The way I handled replication is having a predefined table of animations with corresponding animation IDs. If a player is animating, tell the server what animation it is and fire that information to all the clients. The clients take the animation ID and run the animation locally for every player. I imagine Roblox does something similar to this since it avoids exploiters from tampering with the animations - in this way, any inappropriate players will be walled since the animations are predefined instead of sending CFrame data across the server.

To be clear, I’m still learning what replicates and what doesn’t - all I do is see if it replicates in studio and if it doesn’t, I know I have to do it manually myself. I have only worked with Motor6D and a simple loop - I didn’t know about TweenService at the time I made my animator and I didn’t like :lerp for being too linear. I’m still fairly new to the API but slowly chugging along :wink:

3 Likes

Procedural Animations was something that I needed to learn. I wanted this kind of resources or tutorials for months, thanks.

4 Likes

RootJoint is not a valid member of Part

1 Like

Are you using R6 or R15? The finished example is for R6, I just tested it.

2 Likes

Sounds weird but would honestly be great to see an example of it working/what it is supposed to look like :man_shrugging: .

4 Likes

Default. How can I check or switch?

1 Like
2 Likes

Home<Game Settings<Avatar<Choose R6

2 Likes

This definitely seems pretty cool to use in games, but wouldn’t it be inefficient if you’re using it for a lot of stuff? (Thanks for the article by the way, the way you explained it was simple and straight to the point).

1 Like

All depends on how you’re replicating. Tweening all players might be a bit inefficient (assuming you haven’t removed the default animation scripts), but there are ways of getting around that, such as only tweening when players are in close proximity and using CFrame:lerp on far away players.
Speaking from my own experience, as long as you aren’t tweening on the server, everything should be fine. This is a major reason for why Strife is so laggy, the game sends your positional data to the server every time it changes, instead of telling the server when your goal is and having the server relay that to other clients for them to tween themselves.

3 Likes

Glad to see black magic being in the spotlight for something like this, thank you for the tutorial!

1 Like

Great call out to the black magic series, specifically phantom’s caustaum, that game keeps me going especially for coding

1 Like

Hello there!
Im having problems with the tweening. I dont know how to describe it, but the animations is acting pretty weird. I have a video here:

The code is here

local rad = math.rad
local ti = TweenInfo.new(0.5, Enum.EasingStyle.Sine, Enum.EasingDirection.Out, 0, false, 0)
local leftArm = char.Torso["Left Shoulder"]
local rightArm = char.Torso["Right Shoulder"]
local rightLeg = char.Torso["Right Hip"]
local leftLeg = char.Torso["Left Hip"]
local hrp = char.HumanoidRootPart.RootJoint
local head = char.Torso.Neck
local savedbaseCframe = rightArm.C0
local savedbaseCframe2 = leftArm.C0
local savedbaseCframe3 = rightLeg.C0
local savedbaseCframe4 = leftLeg.C0
local savedbaseCframe5 = hrp.C0
local savedbaseCframe6 = head.C0

local CFrameValue = Instance.new("CFrameValue")
local CFrameValue2 = Instance.new("CFrameValue")
local CFrameValue3 = Instance.new("CFrameValue")
local CFrameValue4 = Instance.new("CFrameValue")
local CFrameValue5 = Instance.new("CFrameValue")
local CFrameValue6 = Instance.new("CFrameValue")

local goal = {}
local goal2 = {}
local goal3 = {}
local goal4 = {}
local goal5 = {}
local goal6 = {}
goal.Value = savedbaseCframe * CFrame.Angles(rad(-33), rad(0), rad(61))
goal2.Value = savedbaseCframe2 * CFrame.Angles(rad(-33), rad(0), rad(-61))
goal3.Value = savedbaseCframe3 * CFrame.Angles(rad(0), rad(0), rad(-29))
goal4.Value = savedbaseCframe4 * CFrame.Angles(rad(0), rad(0), rad(-29))
goal5.Value = savedbaseCframe5 * CFrame.Angles(rad(25), rad(0), rad(0))
goal6.Value = savedbaseCframe6 * CFrame.Angles(rad(-45), rad(0), rad(0))

CFrameValue.Changed:Connect(function()
	rightArm.C0 = CFrameValue.Value
end)

CFrameValue2.Changed:Connect(function()
	leftArm.C0 = CFrameValue2.Value
end)

CFrameValue3.Changed:Connect(function()
	rightLeg.C0 = CFrameValue3.Value
end)

CFrameValue4.Changed:Connect(function()
	leftLeg.C0 = CFrameValue4.Value
end)

CFrameValue5.Changed:Connect(function()
	hrp.C0 = CFrameValue5.Value
end)

CFrameValue6.Changed:Connect(function()
	head.C0 = CFrameValue6.Value
end)

local tween = ts:Create(CFrameValue, ti, goal)
local tween2 = ts:Create(CFrameValue2, ti, goal2)
local tween3 = ts:Create(CFrameValue3, ti, goal3)
local tween4 = ts:Create(CFrameValue4, ti, goal4)
local tween5 = ts:Create(CFrameValue5, ti, goal5)
local tween6 = ts:Create(CFrameValue6, ti, goal6)
wait(1)
tween:Play()
tween2:Play()
tween3:Play()
tween4:Play()
tween5:Play()
tween6:Play()

Is this something with TweenService? Please help me if you can. :slight_smile:

1 Like

Hi there, not sure what the problem here is… Seems like everything is work as intended.
image
If this is the problem (a weird initial keyframe), see if you’re setting savedbaseCframes before your idle animation plays. In general, I usually wait about two seconds after the player’s character has finished loading in before I apply everything.

1 Like

What happens when regular animations try to play over a procedural animation? Will they interpolate or will one take priority over the other?

(With animations playing) https://gyazo.com/3128b4cc384f7b820f4bd2931c49abb8
(Normal) https://gyazo.com/68d8a219c69555b33ba3d0e76d3697d0
I believe that animation track edits to the C0 take priority over tween service, though animation tracks make changes to the current C0 value (EX, rotate thirty degrees from the current point) versus setting from 0 (EX, set rotation to thirty degress). The stock roblox animations look off because this is re-rigged R6.
The reason my procedural idle isn’t completely overwritten is because I simply set the C0 instead of tweening it.

1 Like

Yo, could I do this but without giving specific angles? I mean, can I just use keyframe sequences from RobloxStudio, sort of like what the animations contain? I wanna smoothly transition from one keyframe sequence to another.

If not, would I be able to get the keyframe sequence of each Motor6D using a function that loops through everything in the character and gets all the Motor6Ds in the character, and then sets a base CFrame(reset keyframe sequence), and then just subtracts from the final keyframe sequence to get the change? Like:

final keyframe sequence - reset keyframe sequence(base CFrame)

This would give me the CFrame relative to the base CFrame, and I can just use that CFrame and do:

goal.Value = baseCFrame * relative_cframe_to_base;

If not, how can I achieve this?

1 Like