The basics of infinite terrain generation for a horizontal endless runner – putting all together and adding a car

Read all posts about "" game

It’s time to put all together and turn the concepts of drawing an randomly generated endless terrain and discretizing a terrain and turning it into a physics body into a game.

Why don’t we add a car, running through a randomly generated endless terrain made of physics bodies and using object pooling?

Does it sound complicated? It’s not, but keep in mind these two points:

1 – I always said in endless runner the main character does not move, and it’s the whole environment which moves towards the player. This is true in most cases, but to ensure an accurate physics simulation, this time we need to make the car actually run. The camera will follow the player.

2 – When it’s time to reuse a body, before resizing it and giving it a new angle, you should “reset” it by setting its scale to 1 and its angle to zero, otherwise you may get unexpected results.

Have a look at the game:

Press and hold to accelerate, release to brake.

You’ll rarely see more than 120 bodies to create the terrain, due to object pooling.

And I also have the completely commeted source code for you:

var game;

var gameOptions = {

    // start vertical point of the terrain, 0 = very top; 1 = very bottom
    startTerrainHeight: 0.5,

    // max slope amplitude, in pixels
    amplitude: 100,

    // slope length range, in pixels
    slopeLength: [150, 350],

    // a mountain is a a group of slopes.
    mountainsAmount: 2,

    // amount of slopes for each mountain
    slopesPerMountain: 10,

    // positive and negative car acceleration
    carAcceleration: [0.01, -0.005],

    // maximum car velocity
    maxCarVelocity: 1.2
}
window.onload = function() {
    let gameConfig = {
        type: Phaser.AUTO,
        backgroundColor: 0x75d5e3,
        scale: {
            mode: Phaser.Scale.FIT,
            autoCenter: Phaser.Scale.CENTER_BOTH,
            parent: "thegame",
            width: 1334,
            height: 750
        },
        physics: {
            default: "matter",
            matter: {
                debug: true,
                debugBodyColor: 0x000000
            }
        },
        scene: playGame
    }
    game = new Phaser.Game(gameConfig);
    window.focus();
}
class playGame extends Phaser.Scene{
    constructor(){
        super("PlayGame");
    }
    create(){

        // creation of pool arrays
        this.bodyPool = [];
        this.bodyPoolId = [];

        // array to store mountains
        this.mountainGraphics = [];

        // mountain start coordinates
        this.mountainStart = new Phaser.Math.Vector2(0, 0);

        // loop through all mountains
        for(let i = 0; i < gameOptions.mountainsAmount; i++){

            // each mountain is a graphics object
            this.mountainGraphics[i] = this.add.graphics();

            // generateTerrain is the method to generate the terrain. The arguments are the graphics object and the start position
            this.mountainStart = this.generateTerrain(this.mountainGraphics[i], this.mountainStart);
        }

        // method to add the car
        this.addCar();

        // input management
        this.input.on("pointerdown", this.accelerate, this);
        this.input.on("pointerup", this.decelerate, this);

        // car initial velocity
        this.velocity = 0;

        // car initial acceleration
        this.acceleration = 0;

        // text object with terrain information
        this.terrainInfo = this.add.text(0, game.config.height - 110, "", {
            fontFamily: "Arial",
            fontSize: 64,
            color: "#00ff00"
        });
    }

    // method to generate the terrain. Arguments: the graphics object and the start position
    generateTerrain(graphics, mountainStart){

        // array to store slope points
        let slopePoints = [];

        // variable to count the amount of slopes
        let slopes = 0;

        // slope start point
        let slopeStart = new Phaser.Math.Vector2(0, mountainStart.y);

        // set a random slope length
        let slopeLength = Phaser.Math.Between(gameOptions.slopeLength[0], gameOptions.slopeLength[1]);

        // determine slope end point, with an exception if this is the first slope of the fist mountain: we want it to be flat
        let slopeEnd = (mountainStart.x == 0) ? new Phaser.Math.Vector2(slopeStart.x + gameOptions.slopeLength[1] * 1.5, 0) : new Phaser.Math.Vector2(slopeStart.x + slopeLength, Math.random());

        // current horizontal point
        let pointX = 0;

        // while we have less slopes than regular slopes amount per mountain...
        while(slopes < gameOptions.slopesPerMountain){

            // slope interpolation value
            let interpolationVal = this.interpolate(slopeStart.y, slopeEnd.y, (pointX - slopeStart.x) / (slopeEnd.x - slopeStart.x));

            // if current point is at the end of the slope...
            if(pointX == slopeEnd.x){

                // increase slopes amount
                slopes ++;

                // next slope start position
                slopeStart = new Phaser.Math.Vector2(pointX, slopeEnd.y);

                // next slope end position
                slopeEnd = new Phaser.Math.Vector2(slopeEnd.x + Phaser.Math.Between(gameOptions.slopeLength[0], gameOptions.slopeLength[1]), Math.random());

                // no need to interpolate, we use slope start y value
                interpolationVal = slopeStart.y;
            }

            // current vertical point
            let pointY = game.config.height * gameOptions.startTerrainHeight + interpolationVal * gameOptions.amplitude;

            // add new point to slopePoints array
            slopePoints.push(new Phaser.Math.Vector2(pointX, pointY));

            // move on to next point
            pointX ++ ;
        }

        // simplify the slope
        let simpleSlope = simplify(slopePoints, 1, true);

        // place graphics object
        graphics.x = mountainStart.x;

        // draw the ground
        graphics.clear();
        graphics.moveTo(0, game.config.height);
        graphics.fillStyle(0x654b35);
        graphics.beginPath();
        simpleSlope.forEach(function(point){
            graphics.lineTo(point.x, point.y);
        }.bind(this))
        graphics.lineTo(pointX, game.config.height);
        graphics.lineTo(0, game.config.height);
        graphics.closePath();
        graphics.fillPath();

        // draw the grass
        graphics.lineStyle(16, 0x6b9b1e);
        graphics.beginPath();
        simpleSlope.forEach(function(point){
            graphics.lineTo(point.x, point.y);
        })
        graphics.strokePath();

        // loop through all simpleSlope points starting from the second
        for(let i = 1; i < simpleSlope.length; i++){

            // define a line between previous and current simpleSlope points
            let line = new Phaser.Geom.Line(simpleSlope[i - 1].x, simpleSlope[i - 1].y, simpleSlope[i].x, simpleSlope[i].y);

            // calculate line length, which is the distance between the two points
            let distance = Phaser.Geom.Line.Length(line);

            // calculate the center of the line
            let center = Phaser.Geom.Line.GetPoint(line, 0.5);

            // calculate line angle
            let angle = Phaser.Geom.Line.Angle(line);

            // if the pool is empty...
            if(this.bodyPool.length == 0){

                // create a new rectangle body
                this.matter.add.rectangle(center.x + mountainStart.x, center.y, distance, 10, {
                    isStatic: true,
                    angle: angle,
                    friction: 1,
                    restitution: 0
                });
            }

            // if the pool is not empty...
            else{

                // get the body from the pool
                let body = this.bodyPool.shift();
                this.bodyPoolId.shift();

                // reset, reshape and move the body to its new position
                this.matter.body.setPosition(body, {
                    x: center.x + mountainStart.x,
                    y: center.y
                });
                let length = body.area / 10;
                this.matter.body.setAngle(body, 0)
                this.matter.body.scale(body, 1 / length, 1);
                this.matter.body.scale(body, distance, 1);
                this.matter.body.setAngle(body, angle);
            }
        }

        // assign a custom "width" property to the graphics object
        graphics.width = pointX - 1

        // return the coordinates of last mountain point
        return new Phaser.Math.Vector2(graphics.x + pointX - 1, slopeStart.y);
    }

    // method to build the car
    addCar(){

        // add car body
        this.body = this.matter.add.rectangle(game.config.width / 8, 0, 100, 10, {
            friction: 1,
            restitution: 0
        });

        // add front wheel. I used an octagon rather than a circle just to let you see wheel movement
        this.frontWheel = this.matter.add.polygon(game.config.width / 8 + 25, 25, 8, 15, {
            friction: 1,
            restitution: 0
        });

        // add rear wheel
        this.rearWheel = this.matter.add.polygon(game.config.width / 8 - 25, 25, 8, 15, {
            friction: 1,
            restitution: 0
        });

        // these two constraints will bind front wheel to the body
        this.matter.add.constraint(this.body, this.frontWheel, 20, 0, {
            pointA: {
                x: 25,
                y: 10
            }
        });
        this.matter.add.constraint(this.body, this.frontWheel, 20, 0, {
            pointA: {
                x: 40,
                y: 10
            }
        });

        // same thing for rear wheel
        this.matter.add.constraint(this.body, this.rearWheel, 20, 0, {
            pointA: {
                x: -25,
                y: 10
            }
        });
        this.matter.add.constraint(this.body, this.rearWheel, 20, 0, {
            pointA: {
                x: -40,
                y: 10
            }
        });
    }

    // method to accelerate
    accelerate(){
        this.acceleration = gameOptions.carAcceleration[0]
    }

    // method to decelerate
    decelerate(){
        this.acceleration = gameOptions.carAcceleration[1]
    }
    update(){

        // make the game follow the car
        this.cameras.main.scrollX = this.body.position.x - game.config.width / 8

        // adjust velocity according to acceleration
        this.velocity += this.acceleration;
        this.velocity = Phaser.Math.Clamp(this.velocity, 0, gameOptions.maxCarVelocity);

        // set angular velocity to wheels
        this.matter.body.setAngularVelocity(this.frontWheel, this.velocity);
        this.matter.body.setAngularVelocity(this.rearWheel, this.velocity);

        // loop through all mountains
        this.mountainGraphics.forEach(function(item){

            // if the mountain leaves the screen to the left...
            if(this.cameras.main.scrollX > item.x + item.width + 100){

                // reuse the mountain
                this.mountainStart = this.generateTerrain(item, this.mountainStart)
            }
        }.bind(this));

        // get all bodies
        let bodies = this.matter.world.localWorld.bodies;

        // loop through all bodies
        bodies.forEach(function(body){

            // if the body is out of camera view to the left side and is not yet in the pool..
            if(this.cameras.main.scrollX > body.position.x + 200 &amp;&amp; this.bodyPoolId.indexOf(body.id) == -1){

                // ...add the body to the pool
                this.bodyPool.push(body);
                this.bodyPoolId.push(body.id);
            }
        }.bind(this))

        // update terrain info text
        this.terrainInfo.x = this.cameras.main.scrollX + 50;
        this.terrainInfo.setText("bodies: " + bodies.length + " - pool: " + this.bodyPool.length)
    }

    // method to apply a cosine interpolation between two points
    interpolate(vFrom, vTo, delta){
        let interpolation = (1 - Math.cos(delta * Math.PI)) * 0.5;
        return vFrom * (1 - interpolation) + vTo * interpolation;
    }
}

I would like to create the same stuff with Box2D, and compare performances. Meanwhile download the source code.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

214 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers