Chapter V

Infinite world

In this chapter, we'll explore procedural generation, a technique where game content is created algorithmically. This approach can simplify game development and enable seemingly endless possibilities.

A character with a movie film

Get everything ready

By reorganizing the game elements, we can group similar objects together (mountains, trees, etc). This will enable us to manage these objects as the game world becomes more complex.

To accomplish this, we will create a world object that groups these elements by category.

World boxes

We also need to make changes to the add function to make sure it works with this new organization.

→ scene.js
Copy
export default class Scene {
    constructor () {
        this.world = {
            mountains:  [],
            props:      [],
            trees:      [],
            floorTiles: [],
            obstacles:  []
        }

        // ...
    }

    add (type, object) {
        this.world[type].push(object)
    }

    // omitted for brevity
}

We need to apply this change to each of our elements in the toy.js file.

→ toy.js
Copy
scene.add('mountains', {
    x: 0,
    y: 0.85,
    width: 2,
    height: 2,
    image: images.mountain1
})

// ... and so on

Currently, when we add an element to the scene, we attach an image to it. However, because the scene is not responsible for display, it's best practice to separate the object from its image.

To do this, we can rename the image property to sprite and only pass the image's name when creating the object.

This way, the scene will only hold the object information, while the drawScene function can handle the display by using the appropriate image based on the sprite name.

By separating these responsibilities, we can create a cleaner and more organized development process.

→ toy.js
Copy
scene.add('mountains', {
    x:      0,
    y:      0.85,
    width:  2,
    height: 2,
    sprite: 'mountain1'
})

Now let's modify the drawScene function to take into account this new way of organizing the elements.

→ utils.js
Copy
export function drawScene (ctx, scene, images) {
    for (let category in scene.world) {
        scene.world[category].forEach(element => {
            const drawParams = Object.assign({}, element)

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

            drawImage(ctx, drawParams)
        })
    }
}

Don't forget to pass the images as parameters to the drawScene function in the toy.js file.

→ toy.js
Copy
drawScene(ctx, scene, images)

You may think that this change hasn't made any difference. And you are right, but now with this new way of organizing the elements that make up our scene, we will be able to generate them procedurally.

Random Number Generator

Procedural generation often involves randomness. It's an opportunity to create new tools.

In JavaScript, one such tool is the Math.random function. This function returns a random number between 0 and 1. But, in many cases we may need to generate random numbers within a specific range.

So, let's create a function that will generate random numbers between 2 values. This function will be used to calculate the position of our elements.

Note that I added a condition to check if the value passed as a parameter is an array. If it's not, we simply return the value.

→ utils.js
Copy
export function floatBetween (range) {
    if (!Array.isArray(range)) {
        return range
    }
    const [min, max] = range

    return Math.random() * (max - min) + min
}

Now let's create another function that will allow us to pick a random element from an array. This function will be used to choose a random sprite from a list.

→ utils.js
Copy
export function randomPick (choices) {
    if (!Array.isArray(choices)) {
        return choices
    }

    return choices[Math.floor(Math.random() * choices.length)]
}

Procedural generation

Finally, we come to the main point of this chapter. We want to display objects on the screen by generating them and spacing them randomly.

Every time the screen updates, we add new objects on the right side of the screen if there is enough space, and we get rid of objects that have moved past the left side of the screen.

Procedural process

To figure out where to place the next object, we have to know the position of the last object in that category.

→ scene.js
Copy
lastElementFor (category) {
    const collection = this.world[category]

    return collection[collection.length - 1]
}

From here, we can get the position of the last element. If there is none, we take the position of the camera.

→ scene.js
Copy
lastPositionFor (category) {
    const {camera} = this
    const lastElement = this.lastElementFor(category)

    return lastElement ? lastElement.x : camera.x
}

Now we can make a function that generate random objects. This function needs some details, which could be either set values or ranges (minimum and maximum) for spacing, height, width, and y position.

For the sprite part, we give a set of images names, and the randomPick function will pick one randomly from the set.

Lastly, we use the count parameter to decide the most number of objects we want to see on the screen.

→ scene.js
Copy
generate (category, {spacing, y, width, height, sprite, count}) {
    const lastPosition = this.lastPositionFor(category)

    let x = lastPosition

    if (this.world[category].length > 0) {
        x += floatBetween(spacing)
    }

    while (!this.isOffCamera(x)) {
        this.world[category].push({
            x,
            y:      floatBetween(y),
            width:  floatBetween(width),
            height: floatBetween(height),
            sprite: randomPick(sprite)
        })

        x += floatBetween(spacing)
    }

    this.cleanCategory(category, count)
}

The generate function will loop until a generated element is off the screen.

At the end of the loop, we call the cleanCategory function, which removes elements that are off the screen.

→ scene.js
Copy
cleanCategory (category, count) {
    while (this.world[category].length > count) {
        this.world[category].shift()
    }
}

Generating the scene

We're going to generate each element in the scene. To do this, we'll make a new function called generateWorld. This function will be called in the update loop.

→ scene.js
Copy
generateWorld () {
    this.generate('floorTiles', {
        spacing: 1,
        y:       3,
        width:   1,
        height:  0.5,
        sprite:  ['floor1', 'floor2', 'floor3', 'floor4', 'floor5', 'floor6'],
        count:   8
    })

    this.generate('trees', {
        spacing: [1, 2],
        y:       1,
        width:   [1.8, 2.2],
        height:  [1.8, 2.2],
        sprite:  ['tree1', 'tree2', 'tree3', 'tree4', 'tree5'],
        count:   12
    })

    // and so on...
}
→ scene.js
Copy
update (deltaTime) {
    this.camera.x += this.camera.speed * deltaTime
    this.generateWorld()
}

You can delete the objects you made manually after starting the scene because the new objects we generate in the update loop will replace them.

Here is the result.

We gained new tools

  • Generate a random number between two values
  • Choose a random element from a list
  • Generate random elements

The next chapter will be dedicated to the hero.

Source code
Next: Chapter 6
Hero