CFrame Animator, Curves, Easing and more!

Introduction:

Ever wanted to animate an object, but it wasn’t a character? You didn’t want to use tweens, because they didn’t have the styles or directions you were looking for?

You wanted to have easily customizable animations, with multiple keyframes, and all inside of a neatly packed module in script!?

Well, I have just the thing for you!:


CFrame Animator 1.0!

yapping

One day I was working on a game, and I wanted to make some attack moves. I was going to animate, but I realized I didn’t want to have to animate the character, and then implement it into the attack, and then rescript, and then animate the character, and implement, and rinse and repeat. OVER and OVER again.

So, I started writing. A about an hour later I finished my masterpiece, with just a little over 180 lines of code. It would help me script animations! All I would have to do is put the together the character’s body movement (punching, kicking, etc…), And I could leave the rest of the movement (turning around, dashing, flipping, etc) to easily customizable script.

Here’s the resource:


Using this module, you can use your creativity and imagination to put together any effect you want. Whether it’s teleporting behind the player in a move, doing a dash, or maybe even a cutscene animation! You can implement curves, and since you can use curves, you can also do straight lines (straightened out curve).

Tutorial

To use this module, you will first have to come to a basic understanding of bezier curves, and be well-versed in scripting.

NOTE: This module is NOT incremental (subject to change). It does NOT increment the CFrame it manipulates. If you try to assign the CFrame whilst the animation is playing, the animation will not stop, it will be continued and thus overwritten.

This is an OOP (Object Oriented Programming) module, which means to use it, you must create an object of it’s class (animation) using the .new() function.

local module = require(path.to.module)

local myAnimation = module.new(arg1, arg2, arg3...)

The .new() function takes 3 arguments. The BasePart to be manipulated, the table of keyframes, and an optional callback that fires when the animation completes.

Let’s start with the BasePart

local part = workspace.Part

We create a simple part, nothing special.

local keyframes = {}

Now, we must create the keyframes. This will be a table, and it must have at least one keyframe, of course, or the animation will not have a place to go.


Bezier Curves Explained

Before we continue, you must understand something about bezier curves, and how they work with lerping.

Below is a gif represent how bezier curves work:


Bezier curves are curves created by nested interpolation. What do I mean by that? You take a list of a points (P), and for each point, and the next point after it (P(n) & P(n + 1)) excluding the last one, you lerp between those points with a certain alpha (t). After that, you put that point in a new list (K). You repeat for all the points in P, until just before the last point. For the gif, we would stop at P(2), because it’s second to last.

And then, well… That’s it! That’s how you make a bezier curve, no calculus, linear algebra, just some lerping, we repeat the same step on K (the new list) that we did on P, and generate a new list (K1), which would be only 2 points long, until we reach a list that is only one point long. That’s a bezier curve.

Let’s call that whole process, of taking in a list of points, with an alpha (t) and returning a single point value; B(P, t). Where P is the list, and t is the alpha.


Back To Scripting

Now, that we understand bezier curves, we can continue with the script, and create the second argument, a list (P) of keyframes.

-- keyframe one (startCFrame, endCFrame, timeStart, timeEnd, timeFunc, midCFrames?, length)

local keyframes = {
    {
        CFrame.new(), 
        CFrame.new(0, 10, 0),
        0, math.pi / 2, math.sin, {
            
        }, 1
    }
}

This code segment is a lot to take in. Let’s dissect.

You may be wondering why I used CFrame.new() for the starting position instead of part.CFrame. As I’ve mentioned earlier, this animation module is not incremental, it assign values, which means if you change the CFrame while incrementing, the animation will stay in it’s place. We use a blank CFrame because that’s stating that it’s offset 0 studs and degrees from where the part is. Every CFrame value you input will be this way.

The starting position of the BasePart you’re animating is the base position everything will be animated off of, consider the origin.

If you wanted to move the BasePart while animating, you can manually change the offset position using animation:Move(CFrame).

Let’s explain the code. We create a new variable, keyframes. This is a table of tables (each keyframe), and will hold all our keyframes for the aforementioned arg2. In the segment, I gave all the arguments for each keyframe, and they are explained below:

  • startCFrame: This is a CFrame value, and it is the starting CFrame of your keyframe.
  • endCFrame: This is also a CFrame value, and it is the ending CFrame of your keyframe.
  • timeStart, timeEnd and timeFunc: (to be explained)
  • midCFrames: This is an optional list, if provided, these CFrames will be used as midpoints to create a bezier curve as mentioned previously, if not, the keyframe will play as a straight line between startCFrame and endCFrame.
  • length: The length of the keyframe in seconds.


Easing Styles Explained

What is an easing style? in TweenService, they’re used to control the rate at which the tween happens, whether it starts fast, and slows down, or starts slow, speeds up, and slows back down. You can do any of those in this module, using math!

Wait! Don’t click off yet… This is going to be fun. This system uses three values for "easing", a starting time, the time function, and the end time. Let me give an example

Example:

image

This is an easing style, specifically catered to function like sine. timeStart is 0, timeEnd is pi / 2, and the timeFunc is just sine. Those numbers generate this graph. But what do those numbers mean?

timeStart is where the function starts easing on the x axis, and timeEnd is where is stops. The function is just the function you’re using the generate the easing style.

You can make sine easing styles, and even bouncy, or back, with the proper configurations. To reverse the easing direction in this case, change *timeEnd* to 0, and timeStart to -pi / 2. It will ease out instead of in!

NOTE: timeEnd HAS to be greater than timeStart, or this may cause undesired behavior, or error.


More Easing Styles

image

  • timeStart: -2
  • timeEnd: 2
  • timeFunc: x2

  • timeStart: 0
  • timeEnd: 1.5 * pi
  • timeFunc: sin(x)

Or… just linear!

image

  • timeStart: 0
  • timeEnd: 1
  • timeFunc: x

Back To Scripting

Great! Now that we understand how the easing system works, let’s continue.

local keyframes = {
    {
		CFrame.new(), 
		CFrame.new(0, 10, 0),
		0, math.pi / 2, math.sin, 
		{ CFrame.new(0, 5, 5) },
		1
    }
}

I’ve inserted in the midCFrames table, another CFrame. Between the start and end, but slightly offset to create a curve.

Now, I will add another keyframe, but just in the reverse order, and with a reverse easing direction, like we showed earlier.

local keyframes = {{
		CFrame.new(), 
		CFrame.new(0, 10, 0),
		0, math.pi / 2, math.sin, 
		{ CFrame.new(0, 5, 5) },
		1
	}, {
		CFrame.new(0, 10, 0), 
		CFrame.new(),
		-math.pi / 2, 0, math.sin,
		{ CFrame.new(0, 5, 5) },
		1
	}
}

Looking nice. We've got the keyframes down, now let's create the third argument, the callback when the animation finishes.

This should be simple, for now, we’ll just print a statement:

local callback = function()
    print("Animation finished!")
end

Now for the fun part. Let’s plug everything into the module, and get our animation like we deserve.

local myAnimation = module.new(part, keyframes, callback)

And let’s play it!

myAnimation:Play()

Here's the whole script (for reference):
local module = require(game.ReplicatedStorage.Modules.MovementModule)

local part = workspace.Part

local keyframes = {{
		CFrame.new(), 
		CFrame.new(0, 10, 0),
		0, math.pi / 2, math.sin, 
		{ CFrame.new(0, 5, 5) },
		1
	}, {
		CFrame.new(0, 10, 0), 
		CFrame.new(),
		-math.pi / 2, 0, math.sin,
		{ CFrame.new(0, 5, 5) },
		1
	}
}

local callback = function()
	print("Animation finished!")
end

local myAnimation = module.new(part, keyframes, callback)

myAnimation:Play()


Conclusion

I will be continuing to take community advice, whether it’s for performance, security, or any other nitpicks, I will take all of the advice, for any other questions, please feel free to fire away in the discussion below.

Have fun with this project.

Remember this module is in 1.0, so it’s VERY beta

12 Likes

Ho? I’m the first person to see this eh, ill use it and let you know how it works out

2 Likes

Seems really useful! I’ll give it a try, I’ll let you know If I have a mental breakdown trying to figure out how to use it! :+1:

Edit: Ok it wasn’t that hard, but now I’m wondering, if its incremental, how would I animate a part to move to another part’s position? since the CFrames work as offsets idk how

1 Like

Ah yes, I talked about this in the original post, it’s actually not incremental, it assigns the CFrame.

Internally while it’s running you could set animation.OriginCFrame, which is the cframe the animation builds off of to change the animation position.

1 Like

I might change this, because it’s very tedious if you want to animate at static CFrame values. Instead I could change it so instead of building off of the thing being animated, you just pass normal cframes at the desired position. If you want to animate off of the part, you can just add the CFrame to the part’s CFrame.