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.