I already explained you can randomly generate an endless terrain with a series of point and a simple Cosine interpolation in my horizontal endless runner tutorial series which eventually led to the creation of RRRisky Hills game, which is inspired by Risky Road game, which I deconstruced in another tutorial series.
And this is what I built:
There is no interactivity: a terrain is generated, then 30 random rectangles fall down, then a new terrain is generated, and so on.
Thanks to Simplify.js we can render the terrian with about 4% of the original amount of points.
And here is the source code:
let game; let gameOptions = { startTerrainHeight: 0.5, amplitude: 300, slopeLength: [100, 350], 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() { let gravity = planck.Vec2(0, 3); this.world = planck.World(gravity); this.debugDraw = this.add.graphics(); this.debugDraw = this.add.graphics(); this.sliceStart = new Phaser.Math.Vector2(0, Math.random()); this.drawTerrain(this.slopeGraphics, this.sliceStart); } drawTerrain(graphics, sliceStart) { let ground = this.world.createBody(); let slopePoints = []; let slopes = 0; let slopeStart = 0; let slopeStartHeight = sliceStart.y; let currentSlopeLength = Phaser.Math.Between(gameOptions.slopeLength[0], gameOptions.slopeLength[1]); let slopeEnd = slopeStart + currentSlopeLength; let slopeEndHeight = Math.random(); let currentPoint = 0; while (currentPoint < game.config.width) { if (currentPoint == slopeEnd) { slopes ++; slopeStartHeight = slopeEndHeight; slopeEndHeight = Math.random(); var y = game.config.height * gameOptions.startTerrainHeight + slopeStartHeight * gameOptions.amplitude; slopeStart = currentPoint; currentSlopeLength = Phaser.Math.Between(gameOptions.slopeLength[0], gameOptions.slopeLength[1]); slopeEnd += currentSlopeLength; } else { var y = (game.config.height * gameOptions.startTerrainHeight) + this.interpolate(slopeStartHeight, slopeEndHeight, (currentPoint - slopeStart) / (slopeEnd - slopeStart)) * gameOptions.amplitude; } slopePoints.push(new Phaser.Math.Vector2(currentPoint, y)) currentPoint ++ ; } let simpleSlope = simplify(slopePoints, 1, true); for (let i = 1; i < simpleSlope.length; i ++) { ground.createFixture(planck.Edge(planck.Vec2(simpleSlope[i - 1].x / gameOptions.worldScale, simpleSlope[i - 1].y / gameOptions.worldScale), planck.Vec2(simpleSlope[i].x / gameOptions.worldScale, simpleSlope[i].y / gameOptions.worldScale)), { density: 0, friction : 1 }); } this.add.text(0, game.config.height - 60, "Edges to generate terrain: " + simpleSlope.length, { fontFamily: "Arial", fontSize: 48, color: "#00ff00" }); this.polygons = 0; this.time.addEvent({ delay: 500, callbackScope: this, callback: function() { this.createBox(Phaser.Math.Between(0, game.config.width), -50, Phaser.Math.Between(20, 60), Phaser.Math.Between(20, 60)) this.polygons ++; if(this.polygons > 30){ this.scene.start("PlayGame"); } }, loop: true }); } createBox(posX, posY, width, height) { let box = this.world.createBody(); box.setDynamic(); box.createFixture(planck.Box(width / 2 / gameOptions.worldScale, height / 2 / gameOptions.worldScale)); box.setPosition(planck.Vec2(posX / gameOptions.worldScale, posY / gameOptions.worldScale)); box.setMassData({ mass: 1, center: planck.Vec2(), I: 1 }); return box; } update(t, dt) { this.world.step(dt / 1000 * 2); this.world.clearForces(); this.debugDraw.clear(); for (let body = this.world.getBodyList(); body; body = body.getNext()) { this.debugDraw.beginPath(); for (let fixture = body.getFixtureList(); fixture; fixture = fixture.getNext()) { let shape = fixture.getShape(); switch (fixture.getType()) { case "edge": { this.debugDraw.lineStyle(4, 0xff0000); let v1 = shape.m_vertex1; let v2 = shape.m_vertex2; this.debugDraw.moveTo(v1.x * gameOptions.worldScale, v1.y * gameOptions.worldScale); this.debugDraw.lineTo(v2.x * gameOptions.worldScale, v2.y * gameOptions.worldScale); this.debugDraw.strokePath(); break; } default: { this.debugDraw.lineStyle(2, 0x00ff00); this.debugDraw.fillStyle(0x00ff00, 0.2); let vertices = shape.m_vertices.length; for (let i = 0; i < vertices; i ++) { let vertex = shape.getVertex(i); let worldPosition = body.getWorldPoint(vertex); if (i == 0) { this.debugDraw.moveTo(worldPosition.x * gameOptions.worldScale, worldPosition.y * gameOptions.worldScale); } else { this.debugDraw.lineTo(worldPosition.x * gameOptions.worldScale, worldPosition.y * gameOptions.worldScale); } } this.debugDraw.closePath(); this.debugDraw.strokePath(); this.debugDraw.fillPath(); break; } } } } } interpolate(vFrom, vTo, delta) { let interpolation = (1 - Math.cos(delta * Math.PI)) * 0.5; return vFrom * (1 - interpolation) + vTo * interpolation; } }
Next time, I’ll add a car powered by Box2D, joints and motors, meanwhile download the source code.