Do you like my tutorials?

Then consider supporting me on Ko-fi

Talking about Horizontal Endless Runner game, Box2D, Game development, HTML5, Javascript and Phaser.

The tutorial about the generation of a physics driven random terrain for your HTML5 games using Phaser, Box2D by Planck.js and Simplify.js has been quite successful, so I am going to show you how to generate an endless physics random terrain, using only a bunch of bodies.

Actually, only a bunch of edges, since I am using Box2d edges to build the terrain, have a look:

There is no interactivity: a terrain is generated slope by slope, and a ball is running on it.

The generation of the terrain slope by slope, together with Simplify.js, allowed me to use only from 30 to 40 edges to handle an endless terrain.

And you also have a completely commented source code to study:

let game;
let gameOptions = {

    // starting terrain height, in % of game height
    startTerrainHeight: 0.5,

    // slope amplitude. The higher the value, the higher the hills
    slopeAmplitude: 120,

    // slope lenght range, in pixels
    slopeLengthRange: [100, 350],

    // amount of pixels in a meter, in Box2D world
    worldScale: 30
}
window.onload = function() {
    let gameConfig = {
        type: Phaser.AUTO,
        scale: {
            mode: Phaser.Scale.FIT,
            autoCenter: Phaser.Scale.CENTER_BOTH,
            parent: "thegame",
            width: 1334,
            height: 750
        },
        scene: playGame
    }
    game = new Phaser.Game(gameConfig);
    window.focus();
}
class playGame extends Phaser.Scene {
    constructor() {
        super("PlayGame");
    }
    create() {

        // gravity vector
        let gravity = planck.Vec2(0, 4);

        // box2D world creation
        this.world = planck.World(gravity);

        // body to represent the ground
        this.ground = this.world.createBody();

        // slope start coordinates
        this.slopeStart = new Phaser.Math.Vector2(0, 0);

        // terrain generation
        this.generateTerrain();

        // graphics game object where to draw the debug draw
        this.debugDrawGraphics = this.add.graphics();

        // a text game object to display the number of edges used
        this.edgeText = this.add.text(10, game.config.height - 60, "", {
            fontFamily: "Arial",
            fontSize: 48,
            color: "#00ff00"
        });

        // method to add the ball
        this.addBall();
    }

    generateTerrain() {

        // while next slope starting X coordinate is less than game width and camera scroll...
        while (this.slopeStart.x < this.cameras.main.scrollX + game.config.width) {

            // ... generate a new slope
            this.generateSlope();
        }
    }

    addBall() {

        // ball body
        this.ball = this.world.createBody();

        // set the ball dynamic
        this.ball.setDynamic();

        // creation of a circle fixture to be assigned to the ball
        this.ball.createFixture(planck.Circle(1));

        // set ball position
        this.ball.setPosition(planck.Vec2(150 / gameOptions.worldScale, -100 / gameOptions.worldScale));

        // set ball mass data
        this.ball.setMassData({
            mass: 1,
            center: planck.Vec2(),
            I: 1
        });
    }

    generateSlope() {

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

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

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

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

        // current horizontal point
        let pointX = 0;

        // while the slope hans't been completely generated...
        while (pointX <= slopeEnd.x) {

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

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

            // 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);

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

            // create a Box2D edge
            this.ground.createFixture(planck.Edge(planck.Vec2((simpleSlope[i - 1].x + this.slopeStart.x) / gameOptions.worldScale, simpleSlope[i - 1].y / gameOptions.worldScale), planck.Vec2((simpleSlope[i].x + this.slopeStart.x) / gameOptions.worldScale, simpleSlope[i].y / gameOptions.worldScale)), {
                density: 0,
                friction : 1
            });
        }

        // upldate next slope start point
        this.slopeStart.x += pointX - 1;
        this.slopeStart.y = slopeEnd.y;
    }

    update(t, dt) {

        // advance Box2D world simulation
        this.world.step(dt / 1000 * 2);

        // reset Box2D world forces
        this.world.clearForces();

        // get ball position
        let ballPosition = this.ball.getPosition();

        // scroll the camera accordingly
        this.cameras.main.scrollX = ballPosition.x * gameOptions.worldScale - 150;

        // set ball angular velocity
        this.ball.setAngularVelocity(10);

        // debug draw
        this.debugDraw();

        // keep generating terrain
        this.generateTerrain()

    }

    // below this line, only functions to represent objects on the screen or common math functions

    debugDraw() {
        this.debugDrawGraphics.clear();
        let edges = 0;
        for (let body = this.world.getBodyList(); body; body = body.getNext()) {
            for (let fixture = body.getFixtureList(); fixture; fixture = fixture.getNext()) {
                let shape = fixture.getShape();
                switch (fixture.getType()) {
                    case "edge": {
                        edges ++;
                        this.debugDrawGraphics.lineStyle(4, 0xff0000);
                        let v1 = shape.m_vertex1;
                        let v2 = shape.m_vertex2;
                        if(v2.x * gameOptions.worldScale < this.cameras.main.scrollX){
                            body.destroyFixture(fixture)
                        }
                        else{
                            this.debugDrawGraphics.beginPath();
                            this.debugDrawGraphics.moveTo(v1.x * gameOptions.worldScale, v1.y * gameOptions.worldScale);
                            this.debugDrawGraphics.lineTo(v2.x * gameOptions.worldScale, v2.y * gameOptions.worldScale);
                            this.debugDrawGraphics.strokePath();
                    }
                        break;
                    }
                    case "circle": {
                        let position = body.getPosition();
                        let angle = body.getAngle();
                        this.debugDrawGraphics.fillStyle(0x00ff00, 0.5);
                        this.debugDrawGraphics.fillCircle(position.x * gameOptions.worldScale, position.y * gameOptions.worldScale, shape.m_radius * gameOptions.worldScale);
                        this.debugDrawGraphics.lineStyle(2, 0x00ff00);
                        this.debugDrawGraphics.strokeCircle(position.x * gameOptions.worldScale, position.y * gameOptions.worldScale, shape.m_radius * gameOptions.worldScale);
                        this.debugDrawGraphics.beginPath();
                        this.debugDrawGraphics.moveTo(position.x * gameOptions.worldScale, position.y * gameOptions.worldScale);
                        this.debugDrawGraphics.lineTo(position.x * gameOptions.worldScale + 30 * Math.cos(angle), position.y * gameOptions.worldScale + 30 * Math.sin(angle));
                        this.debugDrawGraphics.strokePath();
                        break;
                    }
                }
            }
        }
        this.edgeText.x = this.cameras.main.scrollX + 10;
        this.edgeText.text = "Edges to generate terrain: " + edges;
    }
    interpolate(vFrom, vTo, delta) {
        let interpolation = (1 - Math.cos(delta * Math.PI)) * 0.5;
        return vFrom * (1 - interpolation) + vTo * interpolation;
    }
}

The core of the script is less than 50 lines of pure awesomeness, given the result. Maybe I am turning it into a full game, meanwhile download the source code and enjoy.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.