-
Notifications
You must be signed in to change notification settings - Fork 5
Home
Some people liked the animations of this game. So it may be nice to write a bit about it. The animation used is the so-called "skeletal animation", which is the way 3D models are animated. The web is full of documentation about this subject.
The aim of this guide is to explain how my implementation works and how to use it. It's not a fully working skeletal animation library, but it's on MIT license, so you are free to modify it and to use it in your game if you like.
This kind of animation is called "skeletal" because it works on a skeleton. The skeleton is just a tree structure made on bones. The bones are segments that can be rotated around their starting point. Each bone starts where its parent bone ends. Each bone has a texture that is rendered on top of it. Basically it's an implementation of how this thing works:
Maybe. Comparing it to a spritesheet animation:
PROS:
- Smoother animation: you can get 60 FPS animations.
- Animations can be combined. For example: if you have a running animation and an attack animation you can easily obtain an attack-while-running animation.
- Bones rotations can be easily modified, giving you the chance to make very responsive animations. For example: you can make your character look around or attack in any direction by rotating bones according to mouse position.
CONS:
- It's kinda heavy. For each frame of animation you have to apply animations on the skeleton and draw every bone instead of just drawing a single frame of a spritesheet.
- May be too limited. With a spritesheet you can draw everything you want, with skeletal animation you are just rotating stuff around.
utils.js has some general utility functions that we will be using.
skeleton.js has some utility functions for handling skeletons.
base-classes.js has the _13Sprite class that handles animations.
The bones are segments that can be rotated around their starting point. Each bone has a texture that is rendered on top of it. You can actually use an image file or you can draw something on a canvas. For this game I have used canvas drawing because of the 13kB limitation.
I have made a simple but powerful function handling all the path drawing stuff:
funciton _13Path(ctx, pathObj)
The ctx parameter is the canvas context you want to draw to. The pathObj is an object describing the path and it must be something like this:
{ c: 'red', // fill color
p: [ // points in the path
[ 0, 0 ],
[ 0, 100 ],
[ 100, 100 ],
[ 100, 0 ]
]}
This draws a simple square: the p array is just a sequence of coordinates that are used as parameters for lineTo calls on the canvas context.
But the p array supports also arcs, rectangles, bezier curves. So you can draw a red square also like this:
{ c: 'red',
p: [
[ 'rect', 0, 0, 100, 100 ]
]}
You can put as a first element 'rect' to draw a rectangle instead of a simple line. Arcs and bezier curves work in a similar fashion. So: how many _13Path calls you need to draw a light bulb? Just 2 of them:
_13Path(_ctx, { c: 'yellow', p: [
[100, 300],
['arc', 150, 150, 100, PI - 0.3, 0.3],
[200, 300]
]});
_13Path(_ctx, { c: '#dddddd', p: [
[100, 300],
[100, 350],
['bez', 100, 400, 200, 400, 200, 350],
[200, 300],
]});
The lightbulb example on JSFiddle
We have an image, we have to make sure that it can rotate. The base bone object looks like this:
{
size: 100,
rot: -0.25 * PI,
path: [
{ c: 'red', p: [
[ 'rect', -0.2, 0, 0.4, 1 ]
]}
]
}
Let's have a look at these properties:
- size: the length of the bone. The path objects in the path array will be scaled to this value.
- rot: the rotation of the bone.
- path: an array of path objects. The paths will be scaled to the size property, so [0, 0] is the starting point of the bone and [0, 1] is the ending point of the bone. It's quite useful because you can change the size of a bone and the path will scale accordingly. This also mean that the texture is centered on x = 0. So if you want to draw a rectangle from the starting point to the ending point that is centered on the bone you must start at x = -0.2 and make it 0.4 width.
Some of these things may look weird but they will become clear when we see how the skeleton works. Or at least, I hope so.
The skeleton is a tree structure made on bones. Each bone starts where its parent bone ends.
Let's add some properties to the bone object to handle children bones:
{
name: 'bone_0',
x: 100,
y: 100,
size: 100,
rot: -0.25 * PI,
path: [
{ c: 'red', p: [
[ 'rect', -0.2, 0, 0.4, 1 ]
]}
],
under: false,
link: [ {
name: 'bone_1',
x: 0,
y: 0,
size: 70,
rot: PI * 0.5,
path: [
{ c: 'blue', p: [
[ 'rect', -0.2, 0, 0.4, 1 ]
]}
]
} ]
};
- name: the name of the bone. It will be useful to get a bone by its name and not by following the path in the tree.
- x and y: offset between the starting point of the bone and the ending point of the parent.
- under: if it's true the bone will be drawn behind the parent.
- link: the list of the children of this bone.
That's it. Let's see it actually drawn on a canvas.
It's a recursive function that must translate and rotate according to the bone object, draw the image and recurse on child nodes. Nothing exceptional and it's also easier with the functions we have in the skeleton.js file.
First of all anyway we need to do some initialization on the skeleton using this function:
_13SkelInit(_skeleton);
This function will do 2 things:
- Paths get drawn on a canvas, which is stored in the texture property.
- A bones property is created in the root bone of the skeleton. It's an object where you can find the bones by their name, for example: _skeleton.bones.bone_0 will be an array with all the bones named "bone_0"
After init we can draw the skeleton on the canvas with this function:
_13SkelDraw(_ctx, _skeleton);
The two rectangles example on JSFiddle
Now let's put things in motion.
Animations are basically functions that change the bones' rotation over time. The skeleton and a set of animation objects are passed as parameters to the _13Sprite class that handles animation-related stuff.
When we need to draw an animation frame on the canvas we will need to change the bones rotation to let the skeleton animate. We need a transformation function that gets the base skeleton and the moment in the animation's duration as parameters and sets the bones accordingly. It's something like this:
function (skeleton, animval) {
skeleton.bones.bone_0[0].rot += animval * 0.5;
skeleton.bones.bone_1[0].rot -= animval;
return skeleton;
}
About the parameters:
- skeleton: it's the skeleton that the function must modify.
- animval: it's a 0 to 1 value that tells in what moment of the animation we are (0: start, 1: end).
What we are doing here is just modifing the two rectangles' rotation based on the animval parameter. In this way their rotation will change as the animation progresses.
An animation object has a transformation function but also other things: duration, if it must loop or not and a name. Let's put it all together and let's create a _13Sprite object that will handle all this stuff:
var _animSprite = _13Sprite({
skel: _skeleton, // the two rectangles from the previous example
anim: {
'test': {
dur: 2000,
loop: true,
trans: function (skeleton, animval) {
skeleton.bones.bone_0[0].rot += animval * 0.5;
skeleton.bones.bone_1[0].rot -= animval;
return skeleton;
}
}
}
});
Now we have a _13Sprite object. But how it works?
First of all we have to set the animation playing:
_animSprite.play('test');
Then we have to set up a simple interval. For each cycle we have to tell the sprite object that some time has passed and to refresh the skeleton's bones rotations accordingly. This will call the animation function of the active animation with the proper animval parameter.
_animSprite.refresh(30);
Finally we draw the skeleton to our canvas:
_animSprite.render(_ctx);
Take a look at the result and the full code in this JSFiddle.
As you can see the rectangles will rotate and jump back to the starting position. But there's something more. You may notice that when the animation goes back doesn't go instantly to the starting position but there are a couple of frames in the middle. That's because the sprite object also takes care of easing a bit the bones' rotations. It may seem not very noticeable but actually makes the animation feel more natural and smooth.
But this is all really ugly. Let's try to do something that remotely looks like a person running.
For the sake of simplicity, I'll use a skeleton made just by a body and the legs. Also, I will use a very simple and basic texture for all the bones. So let's start from the texture:
var _basePath = [
{ c: 'red', p: [
[ 'arc', 0, 0, 0.3, 0, PI, true ],
[ 'arc', 0, 1, 0.2, PI, 0, true ],
]}
]
It's just something with round ends.
Now, let's build the skeleton. We need a body, two tighs attached to it and two calves, one for each tigh:
var _skeleton = {
name: 'body',
x: 250,
y: 100,
size: 130,
rot: 0,
path: _basePath,
under: false,
z: 1,
link: [ {
name: 'leg',
x: 0,
y: 0,
size: 90,
rot: -0.2,
path: _basePath,
link: [ {
name: 'leg_1',
x: 0,
y: 0,
size: 60,
rot: 0.1,
path: _basePath
} ]
}, {
name: 'leg',
x: 0,
y: 0,
size: 90,
rot: 0,
path: _basePath,
z: 2,
under: true,
link: [ {
name: 'leg_1',
x: 0,
y: 0,
size: 60,
rot: 0.1,
z: 2,
path: _basePath
} ]
} ]
};
Notice how one of the legs has the under property set to true because it has to be rendered behind the body.
You may also have noticed a z property. This is not about a fully working layer handling, this stuff doesn't do that. You can just set if a child goes under or on top of the parent, which is quite ugly and leads to some hacks to make a complex animation work. This z property is just a darkening effect on the texture to make it look like it's on another layer.
Our base skeleton will look like this:
Still ugly and unimpressive. Let's try to work some magic.
How a person runs? Let's think about it.
Your upper leg swings back and forth. Your lower leg goes straight and bends.
So our first deduction is:
- Upper leg rotation goes from -0.5 to 0.5
- Lower leg rotation goes from 0 to 0.8
Fine, let's try it. But how? Let's consider the upper leg. It has to go from -0.5 to 0.5, so we can just add the animation time parameter to -0.5 but it won't work because it will go all the way back and go back to the starting position. Also the movement would be very unnatural because it will just swing at the same speed for all the animation duration.
But we do have a math function that swings back and forth with a smooth and lovely curve. It goes full speed then slows, then turns back and so on.
Math.sin(animval * PI * 2)
It's just perfect for this. It will go 0 to 1, 1 to -1 and back to 0 in a continous and fluid motion.
The problem looks solved, but not yet. In fact it's fine for the upper leg but what about the lower leg? We can do the same but, let's look again at how a person runs.
When your upper leg is at the front, your lower leg is bent. When the upper leg goes back at the middle, your lower leg is straight. Then the upper leg goes back and the lower bends again. When your upper leg goes forward and is at the middle, your lower leg is fully bent.
It may sound complicated, but it's not. Your lower leg is just making what the upper leg does but with a delay. To be precise, it's 1/4 backwards in the loop. So it's enough to do this:
Math.sin(animval * PI * 2 - PI / 2)
The same things but 1/4 of a loop later. That's it for one of the legs. What about the other? It will just move in the opposite way. So our animation function looks like this:
var _legRot = Math.sin(animval * PI * 2);
var _kneeRot = Math.sin(animval * PI * 2 - PI / 2);
skeleton.bones.leg[0].rot = _legRot * 0.5;
skeleton.bones.leg[1].rot = -_legRot * 0.5;
skeleton.bones.leg_1[0].rot = 0.4 + _kneeRot * 0.4;
skeleton.bones.leg_1[1].rot = 0.4 - _kneeRot * 0.4;
Let's add some details. While running your body will be a bit bent forward. Your body will also move a bit up and down. Transformation functions usually are used to change rotation of the bones, but can edit any property, so we can edit the y property to let the body move. But how?
When one of your legs pushes on the ground your body will go up, then gravity will pull it down. It will go up fast, slow, go back down accelerating. Then it will be pushed up again and so on. It's just like something bouncing, when it hits the ground his speed just reverses instantly to go up again.
-Math.abs(Math.sin(animval * PI * 2))
That's it! Let's add this to our animation:
var _bodyBounce = -Math.abs(_legRot);
skeleton.bones.body[0].rot = 0.1 + 0.1 * _bodyBounce;
skeleton.bones.body[0].y += _bodyBounce * 20;
So here it is, our animation. You can see it in this JSFiddle.
It's still not so cool, but the animation looks smooth and natural. There's hope!
I hope I've been clear. Really. I also hope that this may help someone developing something cool :)
This is not a full guide, there are some features implemented that I didn't write about. For example there's support for playing multiple animations combined together, playing animations at different speeds, animations with an external parameter passed in to the transformation function, chained animations...
But it is supposed to be just an introduction. You can have a look at source code (it's kinda messy sometimes but I've made my best to make it readable and properly commented). This stuff is not a skeletal animation library, it's missing a lot of features like proper layer handling or decent tools to edit skeleton and animations. But it may be good for other js13k games needing something very barebones or it can be a good start for a more complete library.
Anyway: if you find a typo or you have questions, just tweet me.