Chapter III

Painting a scene

Thanks to our own toolbox, we can already paint a scene, but let's not rush it. First, let's create a new tool. This time, it won't be a developer tool, but a scene designer tool.

Plant tree

The right scale

In the previous chapter, we drew a character by giving it a width and height of 100 pixels. However, pixels can be a bit difficult to understand. They are not like the units of measurement we use in everyday life, such as meters (or feet).

We could convert pixels into meters, but I have a better idea. Let's create our own unit of measurement. For instance, we could use the size of our character as a reference, and this will make it easier to position the other elements of our scene.

The height of our canvas is 400 pixels, and I would like to stack 4 characters on top of each other. We can therefore assume that 1 character = 100 pixels, so 1 unit can be set to 100 pixels on the canvas scale.

Character scale

First, let's create our scale tool using the Canvas API.

→ utils.js
Copy
export function setScale (ctx, scale) {
    ctx.scale(scale, scale)
}

Now, we can draw the character by giving it a width and height of 1. Let's also draw a square of the same size behind it to see the boundaries of the image.

→ toy.js
Copy
setScale(ctx, 100)

const heroImage = await loadImage('images/hero_run_01.png')

drawRectangle(ctx, {
    x: 0,
    y: 0,
    width: 1,
    height: 1,
    color: 'orange'
})

drawImage(ctx, {
    x: 0,
    y: 0,
    width: 1,
    height: 1,
    image: heroImage
})
Character scale

Draw a grid

Now that we have defined a scale, I think it can be very helpful visually to display a grid.

To achieve this, we will use the lineTo method of the Canvas 2D API. This method allows us to draw a line between two points. To draw a grid, we will therefore draw vertical and horizontal lines.

→ utils.js
Copy
export function drawGrid (ctx) {
    const {width, height} = ctx.canvas
    const cellSize = 1

    ctx.strokeStyle = 'rgba(0, 0, 0, 0.25)'
    ctx.lineWidth = 0.01

    for (let x = 0; x < width; x += cellSize) {
        ctx.beginPath()
        ctx.moveTo(x, 0)
        ctx.lineTo(x, height)
        ctx.stroke()
    }
    for (let y = 0; y < height; y += cellSize) {
        ctx.beginPath()
        ctx.moveTo(0, y)
        ctx.lineTo(width, y)
        ctx.stroke()
    }
}

Once our new tool is in place, we simply need to use it.

→ toy.js
Copy
setScale(ctx, 100)

It is much easier to position the elements in our scene.

Display of a grid

Load all of the images

At the moment, we have only loaded one image, but we will need many more for our project. Therefore, we will create a function that loads all of the necessary images at once. This will greatly simplify the process.

We can simply reuse our loadImage tool and loop through a collection of images. This will enable us to load all of the necessary images with minimal effort.

→ utils.js
Copy
export async function loadImages (collection) {
    const images = {}

    for (let name in collection) {
        images[name] = await loadImage(collection[name])
    }

    return images
}

We can use this new function to load two images: the character image and the tree image. And display them side by side to ensure that everything is functioning properly.

→ toy.js
Copy
const images = await loadImages({
    hero1: 'images/hero_run_01.png',
    tree1: 'images/tree_01.png'
})

drawImage(ctx, {
    x: 0,
    y: 0,
    width: 1,
    height: 1,
    image: images.hero1
})

drawImage(ctx, {
    x: 1,
    y: 0,
    width: 2,
    height: 2,
    image: images.tree1
})
A character and a tree, side by side

Since there are many files to load, I suggest creating a separate file that lists all of the necessary images. We can then import this file into our main file and load all of the images at once.

→ image_paths.js
Copy
const imagePaths = {
    hero1:     'images/hero_run_01.png',
    hero2:     'images/hero_run_02.png',
    hero3:     'images/hero_run_03.png',

    // ... and so on

    tech5:     'images/tech_05.png',
    tech6:     'images/tech_06.png',
    tech7:     'images/tech_07.png'
}

export default imagePaths

What exactly is a scene?

When we code, everything is somewhat arbitrary. There isn't really a "scene" - everything is an abstraction. In the case of our game, we can consider a scene as an object that contains all of the game elements.

Therefore, I suggest creating a scene, in which we can store all of the elements to be displayed.

→ scene.js
Copy
export default class Scene {

    constructor () {
        this.elements = []
    }

    add (object) {
        this.elements.push(object)
    }

}

We will also need a function that allows us to display the elements of a scene.

→ utils.js
Copy
export function drawScene (ctx, scene) {
    scene.elements.forEach(element => {
        drawImage(ctx, element)
    })
}

Now, let's connect all of this to our main file (toy.js). This step may not have changed the display itself, but it is an important transition from displaying images on a canvas to displaying elements in a scene. This will unlock new possibilities that we will explore very soon.

→ toy.js
Copy
    const scene = new Scene()

    scene.add({
        x: 0,
        y: 0,
        width: 1,
        height: 1,
        image: images.hero1
    })

    scene.add({
        x: 1,
        y: 0,
        width: 2,
        height: 2,
        image: images.tree1
    })

    drawScene(ctx, scene)

Let's compose the scene!

We have finally reached the much-awaited step. We can now position our elements in the scene using the grid

Please be aware that on the Canvas API, the Y-axis is "inverted". 0 corresponds to the top of the screen, and as the Y-value increases, the element moves downwards on the screen. Personally, I find this quite confusing, but for this game, we will deal with it.

Y axis

Let's start by placing the floor. The height of the ground images corresponds to half a cell. We should position it at the bottom of the screen, so a Y position of 3 seems like a good idea. We can always refine this later on. There are several floor images available to choose from.

→ toy.js
Copy
scene.add({
    x: 0,
    y: 3,
    width: 1,
    height: 0.5,
    image: images.floor1
})

scene.add({
    x: 1,
    y: 3,
    width: 1,
    height: 0.5,
    image: images.floor2
})

// ... and so on
Placing the floor

Now, let's place the trees. The trees are a bit more complicated. We will need to make sure that they are not too close to each other.

A tree seems to be about 2 cells high, and since we want to place them slightly above the road, a Y position of 2 seems appropriate. Let's space them out by about one and a half cells each.

→ toy.js
Copy
scene.add({
    x: 0,
    y: 1,
    width: 2,
    height: 2,
    image: images.tree1
})

scene.add({
    x: 1.5,
    y: 1,
    width: 2,
    height: 2,
    image: images.tree2
})

// ... and so on
Placing trees

You get it. The next step is to do the same for the rest of the elements.

Full scene

Don't spend too much time on this part - it's just to give you a general idea of how the elements are positioned. We'll revisit this section in a future chapter.

What's new in our toolkit?

  • We can now load multiple images at once
  • Change the canvas scale to use our own unit of measurement
  • Display a grid
  • Add elements to a scene
  • Draw a scene

We can move on to the next chapter, which will focus on animating the scene.

Source code
Next: Chapter 4
Movement