Joachim Ford About
25.12.2023

Io's Mission: The Hero

Table Of Contents

Introduction
The Transitioning Extravaganza
Getting to the Code
 ∙ Step 1
 ∙ Step 2
 ∙ Step 3
Conclusion

Introduction

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

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

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 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.

// 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'
]¬

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 article.

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>
© Copyright joachimford.uk