DEV Community

Preacher
Preacher

Posted on • Updated on

DEVBLOG #0: Creating a Custom Cframe Animation System

I've been trying to create a custom animation system using cframe for some time. This is for a shooter, so the requirements aren't too complicated. Animations need to do the following:

-leg walk cycles, leg prone cycles
-swing arms when nothing in hands
-swapping between weapons
-hold a gun when its equipped
-reload the gun
-lean left/right

additional requirements I wanted to include:

  • rolling when prone
  • emotes even when you have a gun equipped (hand signals)
  • stances of varying amount (half crouching, crawling on all fours, etc)
  • procedural walk cycles of varying stride length in all directions, also reusing same walk cycle for varying stances (crouched, half crouched, standing)
  • possibility of footplanting
  • procedural hand positions for interacting with self and world (reaching up to your NVGs to raise/lower them, holding a door handle when opening it)

Of course, why not just use roblox animations? They could accomplish the first set of requirements. You can do some really cool spline animations in blender. However, they can't be used for all of the additional requirements such as procedural interactions with self and the environment. You also have to use blending and weights to get varying stride lengths. Roblox animations require you to upload them all to the platform and have all of them accessed through the cloud instead of locally, so iterating them is tedious. Also, I believe keyframe animation is a clunky way to do animations. In fact, I've noticed a lot of games and animators moving away from this system and using motion matching.

So, I decided to use a custom cframe animation system. This gives me immense freedom of the animations, and allows me to use procedural animations. It doesn't require me to use Roblox's or an external animator and upload all the animations to the platform, instead I can save them in a script. Now that I decided what tools to use, I have to actually make the animation system. First I'll make the rig, and I decided to call animation points nodes. nodes of a simple character

At first I thought it would be best to closely match regular animation systems. I decided that for procedural animations to be a possibility, each animation will be its own function that manipulates the data of the nodes, which also gives them the freedom to change a plethora of things such as their weights. They would be called every frame. I planned that animations would create a layer (layer = {cframe, weight}) in each node's table of layers which is ordered nodeLayers = {layer1, layer2...}. Then I realized what would happen if I wanted to override the arm walk cycle to hold a gun? I would have to set the walk cycle weight to 0 in each hand and also any other animation moving the hands around. However, this was tedious and I had to change the weight of each of these layers in the node, remember their original weights when we stopped holding a gun, and come across the possibility of these weights wanting to change themselves while we are holding a gun and having to stop them and track their changes. This was obviously a no go, so I took a step back.

plan 1plan 2
(I later solved that issue in red from simply switching from table.remove to table[i] = nil)

I began by drawing some sketches of what I thought it should do.
The biggest issue I had was whether to iterate over the animations and let them iterate over the nodes they animated themselves, or iterate over the nodes, find their animations, and call them. The first is an animation-oriented approach, and the second node-oriented. Animation-oriented has the benefit of having easy access to the animation weight, and also sharing calculations and variables between nodes, such as left and right leg, since they will obviously be accessing similar data. However, the override animations act per node, So I would have to check if the node is overridden, otherwise its not worth computing the animation, since it'll never show. This involves me accessing the node table to see if there is an override animation and an override weight greater than 0. In a node-oriented approach, you easily know if the animations are overridden and can just call the animation that is overriding the others. The downside is that to share computations between nodes with the same animation, it requires more design to share a table. One of the nodes has to be delegated to calculate everything and then let the nodes read from it, or have an extra function that runs before that'll pre-calculate. It is also costs O(n) where n = nodes involved in the animation the amount of table indexing for animations, such as x2 for a left and right foot walk cycle. Table indexing is one of the things lua is quite slow at.

I figured that animation-oriented was the way to go for the performance gains, since all the required cframe lerping that was needed to apply all the animation weights is quite expensive as it is. However, I still had to create workarounds for the problems it created, such as knowing if a node is overridden to save compute time. So I decided for an animation script to have a separate table solely to keep track of which nodes were being overridden, and the standard animations could check each node in the table before it updates them. I also decided to not have a table of layers in each node for each of its animations. Instead there is one variable for the standard animation cframe and one for the override animation cframe, as well as a weight for the override animation. At the beginning of an animation update, the cframes get blanked out, and each animation would add their calculated cframe to the nodes with their desired weights. Then an animation script will lerp between the standard cframe and the override cframe if the weight wasn't 0 or 1, else we would use the respective cframe. This massively speeds up the animations, since worst case each node will only has to lerp once, ex. O(n) where n = #nodes, since each animation can apply its weight to the components that make up the cframe rather than the cframe itself. Originally it was O(l*n) where l is #layers in a node, because weights would be applied to the cframe afterwards. It also speeds it up since the animation script wont have to loop through a table at the end for final cframe calculations.

Top comments (0)