Using Inverse Kinematics to Animate Your Character
Table Of Contents
∙ The Transitioning Extravaganza • The Rough Concept
∙ Getting to the Code • Animating the Character
∙ Step 1
∙ Step 2
∙ Step 3
∙ Conclusion
Introduction • The Character Himself
The development of Io's Mission ran me into a lot of challenges. I was unsure - even when I'd been making it for almost a year - whether I'd really be able to pull it off. It may sound surprising to you, but the most complicated part of the whole game turned out to be Io himself.
He went through a lot of changes as he grew older. For a long time, his head didn't lean forward as he walked, and the even the swing of his arms was absent. His appearence changed as well. Here is a picture of his growth over time.
The Transitioning Extravaganza • The Rough Concept
Aside from everything I've already mentioned, I view the most interesting - and challenging, if you haven't done it before - part of Io's functionings to be his "transitioning between states."
I don't know if you noticed, but whenever Io changed position (like rolling or jumping into the sea), he would take a few frames to "transition" between poses. In other words, his body joints would smoothly move to a new position, instead of just switching immidiately.
Look closely at the example below to understand it better.
In the first part of the example, Io "jolts" to the next position. In the second part, Io slowly transitions between states to give the character a more natural feel.
It may be a bit tricky to implement at first, but the results will definitely pay off!
Getting to the Code • Animating the Character
So far we've only discussed this "smooth transitioning" in concept. Now we're going to go slightly further by looking at what we need to do in code.
Let's start by constructing a template character. He has a few main joints and some abilities - like walking, standing and climbing. The first stage is getting him (or her) on the screen. That's quite easy, so let's skip to the next step - defining each joint.
STEP 1: Defining Each Joint
To do this, create a dictionary that stores all of your character's moving parts.
const hero = { arm1: 0, // angle arm2: 0, leg1: 0, leg2: 0, ang: 0, eyex: 0, // position eyey: 0, body: 0 }
As well as defining these variables, make sure they actually change what the hero looks like. For example, if hero.arm1 is set to 1, the character on the screen should have his arm at an angle of one radian.
STEP 2: Setting the Hero's State
The next step is to define what state our character is currently in. Create a new variable to determine what he's doing.
activeState = 'walking'
Now we need to move the hero based on his state. Have a look at the code below to understand how to do this.
// this changes the hero's joints based on the inputted state function changeHeroJoints(inputState) { if (inputState == 'walking') { // the code below is merely an example of // how you may want a walking character to move const sway = .3 + Math.cos(time / 5) * .05 hero.arm1 = -.5 hero.arm2 = .5 hero.leg1 = Math.sin(time / 10) * .5 - sway hero.leg2 = Math.sin(time / 10 + Math.PI) * .5 - sway hero.eyex = .2 hero.ang = sway hero.body = 1.5 } } activeState = 'walking' // this runs every frame function update() { requestAnimationFrame(update) // a number that tells us how many frames have gone by time += 1 // update the hero joints changeHeroJoints(activeState) # draw hero ect...
I'll explain a little of what the code above actually does. Remember we created a string called activeState? This "keyword" is put into the changeHeroJoints() function, which in turn updates the character's body parts. If you need help, read the comments within the code.
The character should now be moving based on what is entered into the changeHeroJoints() function. In the code below, I've given the character another state: climbing.
# hero.leg2 = Math.cos(time) * .5 # hero.eyex = 1 # } else if (inputState == 'climbing') { hero.arm1 = 1.3 + Math.sin(time / 10) * .5 hero.arm2 = 1.3 + Math.sin(time / 10 + Math.PI) * .5 hero.leg1 = 1 + Math.sin(time / 10) * .5 hero.leg2 = 1 + Math.sin(time / 10 + Math.PI) * .5 hero.ang = Math.cos(time / 10) * .05 hero.eyex = -.2 hero.body = 1.5 } } activeState = 'walking' function update() { requestAnimationFrame(update) time += 1 // change the state of the hero every second if (time % 200 < 1) { if (activeState == 'walking') activeState = 'climbing' else activeState = 'walking' } // update the hero joints changeHeroJoints(activeState) # draw hero ect...
Well that was a lot of coding! Thankfully that's the end of the set-up code, and the rest should hopefully be a little easier to understand. Right now, our character behaves much like the "switching" Io - shown earlier - and that's just unacceptable.
Without further ado, let us now commence with the implementation of our famous Transitioning Extravaganza, and press on to the final part of this section.
STEP 3: Coding The Transitioning Extravaganza
We're finally at step three. Let's have a quick recap by taking a look at the comparison again.
So, how do we make it smooth? The first stage is turn our hero dictionary into a function. Sounds pointless, but it will be very useful later on.
Write your code a bit like this:
// creates a new template body and returns it function createBody() { return { arm1: 0, arm2: 0, leg1: 0, leg2: 0, eyex: 0, eyey: 0, body: 0, ang: 0 } } const hero = createBody()
Our changeHeroJoints() function also needs a slight change-around. Instead of directly manipulating the character's body, change the function so that it returns a new body instead, which can be used later on.
Pay particular attention to the comments in the following updated code.
const hero = createBody() function changeHeroJoints(inputState) { // create a new template body const bod = createBody() // notice that we now use "bod" instead of "hero" when moving the joints if (inputState == 'walking') { bod.leg1 = Math.sin(time) * .5 # (...) } else if (inputState == 'climbing') { # (...) } // return the new body pose from the function return bod } // oldState represents the state we're moving from oldState = '' // activeState represents the state we're aiming for activeState = 'walking' // a decimal value representing our progress // through the transition. We will use it later transition = 0 function update() { requestAnimationFrame(update) # (...) // get old and new expected body positions const oldBody = changeHeroJoints(oldState) const newBody = changeHeroJoints(activeState) # draw hero ect...
At the moment, the code shown above won't do anything. If you look carefully, you'll notice that our hero body isn't being changed.
The code is very incomplete, so let's finish it off. Have a read of the comments if you need help.
#function createBody() { # return { # (...) # } #} // Create a new array representing all the moving parts // in our hero's body. This is important const arr = [ 'arm1', 'arm2', 'leg1', 'leg2', 'eyex', 'eyey', 'body', 'ang' ] const hero = createBody() oldState = '' activeState = 'walking' function update() { requestAnimationFrame(update) # (...) const oldBody = changeHeroJoints(oldState) const newBody = changeHeroJoints(activeState) // transition if has not met goal if (oldState != activeState) { transition += .02 // reset transition if finished if (transition >= 1) { transition = 0 oldState = activeState } // iterate through all the body parts for (let i = 0; i < arr.length; i ++) { const oldPart = oldBody[arr[i]] const newPart = newBody[arr[i]] const dist = newPart - oldPart // change the hero's joints hero[arr[i]] = oldPart + (dist * transition) } } // set straight away if not transitioning if (!transition) hero = newBody # draw hero ect...
Well, what is the code doing? First, we created a new array - one that directly mirrored the contents of our createBody() function.
Later, in our update() loop, we went through every body part one by one, and changed the hero's body based on the transition's progress.
We can actually give one small improvement to this code. Currently, the transition works in a "linear" way, but it looks much better (in my opinion anyway) to have it move in a "bezier" fashion. Take a look at the difference to see what I mean.
It's a very, very small difference. It's all about the "feel" you get from it. When it was transitioning, did you think first one looked slightly robotic, whereas the other was a bit smoother?
If you didn't notice a difference you don't need to worry about it, but for those who like minor details (like me), make this one small edit. No need to worry about figuring out the maths.
hero[arr[i]] = oldPart + (dist * (.5 + Math.cos(transition * Math.PI + Math.PI) * .5))
To discover more pointless details from Io's Mission, check out my next blog.
Conclusion
Phew! That was a lot of coding! I only really touched on one aspect of Io's code on this page, but it's something I've always struggled with, and I wrote it to help fellow coders who are puzzling with it too.
I admit it is a little complicated, but it's a very good thing to get working. And remember it's always easier the second time round!
Below is the code for a working version of smooth transitioning. It might be useful to refer to.
<!DOCTYPE html> <html> <head> <style> body { background: #fff; margin: 0; overflow: hidden } canvas { width: 100%; } </style> </head> <body> <canvas id = cvs></canvas> <script> function resize() { cvs.width = innerWidth * devicePixelRatio cvs.height = innerHeight * devicePixelRatio s = cvs.height / 9 } function createBody() { return { arm1: 0, arm2: 0, leg1: 0, leg2: 0, eyex: 0, eyey: 0, body: 0, ang: 0 } } const arr = [ 'arm1', 'arm2', 'leg1', 'leg2', 'eyex', 'eyey', 'body', 'ang' ] let hero = createBody() let activeState = 'walking' let oldState = '' let transition = 0 function live(inputState) { const bod = createBody() if (inputState == 'walking') { const sway = .3 + Math.cos(time / 5) * .05 bod.arm1 = -.5 bod.arm2 = .5 bod.leg1 = Math.sin(time / 10) * .5 - sway bod.leg2 = Math.sin(time / 10 + Math.PI) * .5 - sway bod.eyex = .2 bod.ang = sway bod.body = 1.5 } else if (inputState == 'climbing') { bod.arm1 = 1.3 + Math.sin(time / 10) * .5 bod.arm2 = 1.3 + Math.sin(time / 10 + Math.PI) * .5 bod.leg1 = 1 + Math.sin(time / 10) * .5 bod.leg2 = 1 + Math.sin(time / 10 + Math.PI) * .5 bod.ang = Math.cos(time / 10) * .05 bod.eyex = -.2 bod.body = 1.5 } return bod } function update() { requestAnimationFrame(update) time += 1 // get old and new hero states const oldBody = live(oldState) const newBody = live(activeState) // transition if has not met goal if (oldState != activeState) { transition += .02 // reset transition if finished if (transition >= 1) { transition = 0 oldState = activeState } // iterate through all the body parts for (let i = 0; i < arr.length; i ++) { const oldPart = oldBody[arr[i]] const newPart = newBody[arr[i]] const dist = newPart - oldPart // change the hero's joints hero[arr[i]] = oldPart + (dist * (.5 + Math.cos(transition * Math.PI + Math.PI) * .5)) } } // set straight away if not transitioning if (!transition) hero = newBody // change hero state when ready if (time % 200 < 1) { if (activeState == 'walking') activeState = 'climbing' else activeState = 'walking' } // draw hero ect... ctx.fillStyle = '#bfd' ctx.fillRect(0, 0, cvs.width, cvs.height) const rect = (x, y, w, h) => ctx.fillRect(x - w / 2, y, w, h) const heady = 2 const head = 2 const thick = .2 const main = head - thick * 2 const eyes = .6 const pupw = .2 const pups = .5 const len = 1.3 const start = cvs.height / 2 - heady*s + head*s const y = -head*s + s / 1.5 ctx.save() ctx.translate(cvs.width / 2, cvs.height / 2) ctx.rotate(hero.ang) ctx.fillStyle = '#222' rect(0, 0, thick*s, hero.body*s) ctx.save() ctx.translate(0, 0) ctx.rotate(hero.arm1) rect(0, -heady*s + head*s, thick*s, s * len) ctx.restore() ctx.save() ctx.translate(0, 0) ctx.rotate(hero.arm2) rect(0, -heady*s + head*s, thick*s, s * len) ctx.restore() ctx.save() ctx.translate(0, hero.body*s) ctx.rotate(hero.leg1) rect(0, 0, thick*s, s * len) ctx.restore() ctx.save() ctx.translate(0, hero.body*s) ctx.rotate(hero.leg2) rect(0, 0, thick*s, s * len) ctx.restore() ctx.fillStyle = '#222' rect(0, -heady*s, head*s, head*s) ctx.fillStyle = '#fff' rect(0, -heady*s + (head-main)*s/2, main*s, main*s) ctx.fillStyle = '#222' rect(-s / 3.5 + hero.eyex*s, y + eyes*s/2 - pups*s/2 + hero.eyey*s, s*pupw, pups*s) rect(s / 3.5 + hero.eyex*s, y + eyes*s/2 - pups*s/2 + hero.eyey*s, s*pupw, pups*s) ctx.restore() } const ctx = cvs.getContext('2d') let time = 0 let s = 0 addEventListener('resize', resize) resize() update() </script> </body> </html>
All comments are reviewed before being posted. No contact details or other personal information will be shared.
If there's anything else you don't want published, please say so in the comment.
Thanks for your feedback!