Chapter VII

Obstacles

Our hero can run and jump, but the game needs more excitement and danger. Let's make it more interesting by adding obstacles!

Hero jumping over obstacle

Since obstacles are different from basic objects, we'll give them their own obstacle.js file. We could make them stationary, but it's more thrilling to jump over moving obstacles.

We'll use the camera speed to decide how fast the obstacles should move.

→ obstacle.js
Copy
export default class Obstacle {
    constructor ({x, y, width, height, sprite}) {
        this.x      = x
        this.y      = y
        this.width  = width
        this.height = height
        this.sprite = sprite
        this.speed  = 0.5
    }

    update (deltaTime, cameraSpeed) {
        this.x -= cameraSpeed * this.speed * deltaTime
    }
}

Don't forget to update the obstacles in the scene's update loop.

→ scene.js
Copy
update (deltaTime) {
    // ... omitted for brevity

    this.world.obstacles.forEach(obstacle => {
        obstacle.update(deltaTime, this.camera)
    })
}

We'll generate obstacles at random intervals based on the camera's speed.

But first, we need to add information about the time that has passed in the scene.

→ scene.js
Copy
constructor (canvas) {
    // ... omitted for brevity

    this.elapsedTime = 0
}

update (deltaTime) {
    // ... omitted for brevity

    this.elapsedTime += deltaTime
}

First, we'll check how much time has passed since the last obstacle was made. If it's too soon to create a new obstacle, we'll wait until the next loop.

Next, we'll make a new obstacle with a random position on the y-axis, size, and image.

Finally, we'll set the time for the next obstacle based on the camera's speed.

→ scene.js
Copy
generateObstacles () {
    const {camera} = this

    if (this.elapsedTime < this.nextObstacleAt) {
        return
    }

    const scale = floatBetween([0.4, 0.8])

    const obstacle = new Obstacle({
        x:      camera.x + camera.width + 1,
        y:      floatBetween([2.5, 2.9]),
        width:  scale,
        height: scale,
        sprite: randomPick([
            'tech1',
            'tech2',
            'tech3',
            'tech4',
            'tech5',
            'tech6',
            'tech7'
        ])
    })

    this.add('obstacles', obstacle)

    const nextObstacleDelay = (1 / this.camera.speed) * floatBetween(2.5, 4.5)
    this.nextObstacleAt = this.elapsedTime + nextObstacleDelay
}

Don't forget to start the obstacle generation at the end of the generateWorld function.

→ scene.js
Copy
generateWorld () {
    // ... omitted for brevity

    this.generateObstacles()
}

Collisions

The obstacles don't do anything when they hit the hero. To fix this, we need to do some math.

Fortunately, we can simplify the hero and obstacles shapes by using circles. Calculating collisions between two circles is easy.

But before we calculate collisions, we'll start by showing circles. This will make it easier to visualize the collisions.

Drawing a circle is a bit complicated because we need to draw an arc. I'll simplify this for you, but you can learn more by reading the arc documentation.

→ utils.js
Copy
export function drawCircle (ctx, {x, y, radius, color}) {
    ctx.fillStyle = color
    ctx.beginPath()
    ctx.arc(x, y, radius, 0, 2 * Math.PI)
    ctx.fill()
}

We have a problem: the hero and obstacles are squares for now. To calculate collisions, we need to find the center of the circle and its radius relative to the square.

The radius of the circle is half the size of the square.

We have to find the center of the circle by taking the position of the top-left corner of the square and adding the radius.

Obstacle radius

We'll make a new dynamic property to calculate the circle of our hitBox.

→ obstacle.js
Copy
get hitBox () {
    const semiWidth  = this.width  / 2
    const semiHeight = this.height / 2

    return {
        x:      this.x + semiWidth,
        y:      this.y + semiHeight,
        radius: semiWidth
    }
}

Now let's create a new method called drawHitBox that will draw the collision circle of an element if a hitbox exists on that element.

→ utils.js
Copy
export function drawHitBox (ctx, scene, element) {
    const {hitBox} = element

    if (hitBox) {
        drawCircle(ctx, {
            x:      hitBox.x - scene.camera.x,
            y:      hitBox.y - scene.camera.y,
            radius: hitBox.radius,
            color:  'black'
        })
    }
}

We can draw the circle hitboxes in the scene for debugging purpose.

Unfortunately, our drawScene function is starting to get a bit cluttered. This is an opportunity to do some refactoring to incorporate the display of the hitboxes more cleanly.

But the drawScene function is getting too cluttered. This is a chance to refactor.

We want to do two things in the drawScene function: show the world then show the hero.

→ scene.js
Copy
export function drawScene (ctx, scene, images) {
    drawWorld(ctx, scene, images)
    drawHero(ctx, scene, images)
}

Now it's much cleaner. In the drawWorld function, we want to go through each element of the world and show it.

→ scene.js
Copy
function drawWorld (ctx, scene, images) {
    for (let category in scene.world) {
        scene.world[category].forEach(element => {
            drawSceneElement(ctx, scene, images, element)
        })
    }
}

Then, we'll make a function called drawSceneElement that will show an element of the scene. We'll add the hitBox here.

→ scene.js
Copy
function drawSceneElement (ctx, scene, images, element) {
    const drawParams = Object.assign({}, element)

    drawParams.image = images[drawParams.sprite]
    drawParams.x -= scene.camera.x
    drawParams.y -= scene.camera.y

    drawImage(ctx, drawParams)
    drawHitBox(ctx, scene, element)
}

Now you can see the obstacles hitboxes. This step was a bit long, but it's important to refactor. Otherwise, the code will be hard to read.

Next, let's look at the hero's hitbox, which is a bit more complicated to calculate.

The hero's body doesn't take up the entire square that surrounds it. Also, it's slightly off-center because of the legs. We need to consider these factors to find the center of the circle and its radius.

→ hero.js
Copy
get hitBox () {
    const semiWidth  = this.width  / 2
    const semiHeight = this.height / 2

    return {
        x:      this.x + semiWidth,
        y:      this.y + semiHeight * 0.75,
        radius: semiWidth * 0.5
    }
}

Let's draw this hitBox when we display the hero.

→ utils.js
Copy
function drawHero (ctx, scene, images) {
    // ... omitted for brevity

    drawImage(ctx, drawParams)
    drawHitBox(ctx, scene, hero)
}

You should see a black hitBox around the hero. It's not perfect, but it's enough for our mini-game.

Display hitboxes

Collision detection

To find out if 2 circles touch each other, we will use the Pythagorean theorem.

I recommend you to take a look at this Pythagorean theorem progression by Silent Teacher. It's a fun way to understand how it works.

→ utils.js
Copy
export function circleVsCircle (circleA, circleB) {
    const distanceX = circleA.x - circleB.x
    const distanceY = circleA.y - circleB.y
    const distance  = Math.sqrt(distanceX * distanceX + distanceY * distanceY)

    return distance < circleA.radius + circleB.radius
}

Now in the scene, we'll check if the hero collides with an obstacle on every frame.

→ scene.js
Copy
checkCollisions () {
    const {world, hero} = this

    const collided = world.obstacles.some(obstacle => {
        obstacle.collided = circleVsCircle(hero.hitBox, obstacle.hitBox)

        return obstacle.collided
    })

    hero.collided = collided
}

Remember to call this function first in the update function.

→ scene.js
Copy
update (deltaTime) {
    this.checkCollisions()

    // ... omitted for brevity
}

Collisions should be working now, but we can't see them yet. We'll update the drawHitBox function to show red when an element is colliding.

→ utils.js
Copy
export function drawHitBox (ctx, scene, element) {
    const {hitBox} = element

    if (hitBox) {
        const color = element.collided ? 'red' : 'black'

        drawCircle(ctx, {
            x:      hitBox.x - scene.camera.x,
            y:      hitBox.y - scene.camera.y,
            radius: hitBox.radius,
            color
        })
    }
}

And we're done! Collisions are working. You'll see the obstacles turn red when they collide with the hero.

What's new ?

  • Obstacle generation based on time
  • Calculating collisions between 2 circles
  • Showing a hitbox based on its state

We have everything we need to make a game now.

Source code
Next: Chapter 8
From toy to game