Create Terrain for Vehicles in Phaser

6/8/17

This tutorial will cover a simple way to make vehicles in Phaser and create terrain for them to drive on. You can take a look at what we will be making here, and you can also download the source from Github here.

This game demonstrates how random terrain can be generated in the Phaser game framework for a vehicle to drive on.

For this tutorial, I have created a Vehicle function in vehicle.js which you can use to quickly make a car or truck and add wheels to it. I have created a TerrainController function in terrain.js which has methods to add a P2 physics body to the bottom of your game world, and also to draw the outline of this body on the canvas.

My TerrainController is not highly flexible, but it can easily be tweaked to fit your needs. At the moment, it assumes that you want the container of the terrain to span the entire world width. We will go over how this controller generates an array of vertices to bound the terrain.

One additional note is that the P2 physics body of our terrain can end up with some buggy behavior. This buggy behavior seems to occur because a singular physics polygon in P2 physics must be convex, so P2 physics composes our concave terrain with a bunch of convex shapes. This is done using the fromPolygon method of P2 physics bodies with documentation here. If you want to solve this problem I would suggest finding new methods to compose the terrain body with convex polygons. I have included a file in the lib folder called earcut.min.js, which is also on Github. This library can compose a concave polygon with triangles, so you could try implementing it to fix buggy terrain problems.

game.js

We will start by looking at the main driver of our game, game.js. Game initialization and the preload function should be no surprise:

var width = 800;
var height = 500;
var game = new Phaser.Game(width, height, Phaser.AUTO, null,
	{preload: preload, create: create, update: update});

var truck;
var wheelMaterial;
var worldMaterial;
var allowTruckBounce = true;

function preload() {
	game.stage.backgroundColor = '#eee';
	game.load.image('truck', 'asset/truck.png');
	game.load.image('wheel', 'asset/wheel.png');
	game.load.physics("physics", "asset/physics.json");
}

At this point we have loaded our assets and initialized some variables.

Next, look at the create function:

function create() {
	//set world boundaries with a large world width
	game.world.setBounds(0,-height,width*10,height*2);
	game.physics.startSystem(Phaser.Physics.P2JS);
	game.physics.p2.gravity.y = 600;

	wheelMaterial = game.physics.p2.createMaterial("wheelMaterial");
	worldMaterial = game.physics.p2.createMaterial("worldMaterial");
	game.physics.p2.setWorldMaterial(worldMaterial, true, true, true, true);

	//create contact material to increase friction between
	//the wheels and the ground
	var contactMaterial = game.physics.p2.createContactMaterial(
		wheelMaterial, worldMaterial
	);
	contactMaterial.friction = 1e3;
	contactMaterial.restitution = .3;

	//call onSpaceKeyDown when space key is first pressed
	var spaceKey = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);
	spaceKey.onDown.add(onSpaceKeyDown, game);

	initTruck();
	initTerrain();
}

In here, we initialize the P2 physics engine and make a large width for our world. We also create a contact material for the wheels and the ground so that we can create a higher friction between them. We initialize the space key, which is used to make our truck bounce, and finally we call two functions that initialize the truck and the terrain. Let's look at initTruck now:

function initTruck() {
	//initialize the truck and add the proper physics body
	var truckFrame = game.add.sprite(width*0.25, height*0.4, "truck");
	truck = new Vehicle(truckFrame);
	truck.frame.body.clearShapes();
	truck.frame.body.loadPolygon("physics", "truck");
	game.camera.follow(truck.frame);

	var distBelowTruck = 24;
	initWheel([55, distBelowTruck]);
	initWheel([-52, distBelowTruck]);
}

It creates the main truck sprite, passes it as an argument for creating a new Vehicle object, and gives the truck a physics body that I created and downloaded as physics.json using Loon Physics. It follows the truck with the camera, then initializes two wheels by calling initWheel:

function initWheel(offsetFromTruck) {
	var wheel = truck.addWheel("wheel", offsetFromTruck);
	wheel.body.setMaterial(wheelMaterial);
	wheel.body.onBeginContact.add(onWheelContact, game);
	return wheel;
}

This function uses a method of Vehicle called addWheel to place a new wheel at a position relative to the truck.

Vehicle

Now let's take a look at vehicle.js to better understand how this works:

Vehicle = function(frameSprite) {
    this.frame = frameSprite;
    this.game = this.frame.game;
    this.wheels = this.game.add.group();
    this.debug = false;

    this.game.physics.p2.enable(this.frame, this.debug);

    this.cursors = this.game.input.keyboard.createCursorKeys();

    this.maxWheelForce = 1000;
    this.rotationSpeed = 400;
}

Vehicle.prototype = {
    /**
     * Constrains a wheel to the frame at a given offset position
     * @param  {String} wheelSpriteKey    Preloaded sprite key of the wheel
     * @param  {Array} offsetFromVehicle Relative position on vehicle frame to
     *                                   constrain the wheel in the form [x,y]
     * @return {Phaser.Sprite}           The created wheel
     */
    addWheel: function(wheelSpriteKey, offsetFromVehicle) {
        var vehicleX = this.frame.position.x;
    	var vehicleY = this.frame.position.y;
    	var wheel = this.game.add.sprite(vehicleX + offsetFromVehicle[0],
    						vehicleY + offsetFromVehicle[1], wheelSpriteKey);

    	this.game.physics.p2.enable(wheel, this.debug);

        wheel.body.clearShapes();
    	wheel.body.addCircle(wheel.width * 0.5);

        //constrain the wheels to the vehicle frame so that they can rotate
        //about a fixed point
    	var rev = this.game.physics.p2.createRevoluteConstraint(
            this.frame.body, offsetFromVehicle,
            wheel.body, [0,0], this.maxWheelForce
        );

    	this.wheels.add(wheel);

        return wheel;
    },
    /**
     * Update method for the vehicle's wheels to be controlled
     */
    update: function() {
        var rotationSpeed = this.rotationSpeed;
    	if (this.cursors.left.isDown) {
    		this.wheels.children.forEach(function(wheel,index) {
    			wheel.body.rotateLeft(rotationSpeed);
    		});
    	}
    	else if (this.cursors.right.isDown) {
    		this.wheels.children.forEach(function(wheel,index) {
    			wheel.body.rotateRight(rotationSpeed);
    		});
    	}
    	else {
    		this.wheels.children.forEach(function(wheel,index) {
    			wheel.body.setZeroRotation();
    		});
    	}
    }
};

Creating a new Vehicle object simply initializes the main truck frame with P2 and creates a wheel group. When the addWheel method is called, it gives it a circular physics body and constrains it to the truck at a given position where it can rotate freely. The update method rotates the wheels if arrow keys are pressed and is called in game.js like this:

function update() {
	truck.update();
}

So, at this point, we have initialized a truck and placed it at the far left corner of our game world. But we haven't gone over creating the terrain yet!

Terrain

Let's look at the initTerrain function in game.js.

function initTerrain() {
	//initialize the terrain with bounds
	var terrain = new TerrainController(game, 50, game.world.width - 50,
		100, height - 50);
	//draw the terrain
	terrain.drawOutline();
	//add the physics body
	var groundBody = terrain.addToWorld();
	groundBody.setMaterial(worldMaterial);
	groundBody.name = "terrain";
}

here we instantiate a new TerrainController object, passing it the bounds that will contain our terrain. We call its method to draw an outline around the terrain with a curved black line. We then add the terrain body to the world. There are no sprites involved, because all we are really looking for is the physics body.

Before looking at the TerrainController, let's think about what it should do. It needs to create an array of vertices in the form [[x1, y1], [x2, y2], ...] which are ordered to create a polygon at the bottom of the world. It is useful to first surround this with a container so that we can safely place vertices within that boundary. Here is how I did this visually:

Let's look at how we initialize everything in terrain.js:

TerrainController = function(game, x1, x2, y1, y2) {
    this.game = game;
    if (x1 < x2) {
        this.leftBound = x1;
        this.rightBound = x2;
    }
    else {
        this.leftBound = x2;
        this.rightBound = x1;
    }

    if (y1 < y2) {
        this.topBound = y1;
        this.bottomBound = y2;
    }
    else {
        this.topBound = y2;
        this.bottomBound = y1;
    }
    //set this to true to see the polygons of the terrain physics body
    this.debug = false;
    this.cliffs = true;
    this.fissures = true;

    //create the container, add it to the vertices, then create the terrain
    //and also add it to the end of the vertices
    this.vertices = [];
    var container = this.createContainer();
    this.vertices = this.vertices.concat(container);

    var terrain = this.generateTerrain(this.leftBound, this.rightBound,
        this.topBound, this.bottomBound);
    //flip the terrain to match Phaser's coordinate system
    terrain = this.flipTerrain(terrain, this.topBound, this.bottomBound);
    this.vertices = this.vertices.concat(terrain);
}

The top part is just some basic boundary logic to handle all ways that function arguments might be passed. Towards the bottom, we call createContainer to generate our container array, then we call generateTerrain with our boundaries to make an array of the terrain that we then add to the end of the overall vertex array.

Essentially, the createContainer method does some simple arithmetic to calculate where its vertices should go. The generateTerrain has an x-value step size that it follows and places a point with a reasonable slope from the previous point. It does this until it reaches the maximum x-value, and it switches to a negative slope if it has reached the maximum y-value. It also adds fissures or cliffs when the y-value is relatively high. Take a look at these methods in the source code and try to create your own terrain feature, like a peak or a ramp.

Finally, let's look at the methods that we actually called in game.js. We first called drawOutline:

drawOutline: function() {
    var points = this.vertices.flatten();
	var bmd = this.game.add.bitmapData(this.game.world.width,
        this.game.world.height);
	var ctx = bmd.context;
	ctx.fillStyle = 'black';
	ctx.lineWidth = 5;
	ctx.beginPath();
	ctx.moveTo(points[0],points[1]);
    //Create a spline curve through the vertices
	ctx.curve(points, null, null, true);
	ctx.stroke();

	bmd.addToWorld();
}

This uses canvas drawing methods to draw a nice curved line through the terrain vertices. The curve method is from curve.js which can be found here.

Finally, we must add the P2 physics body to the world. Take a look at addToWorld which we also call from game.js:

addToWorld: function() {
    var groundBody = new Phaser.Physics.P2.Body(this.game,null,0,0);
    var points = this.vertices.slice(0);
    //Convert vertices from pixels to meters for P2
    //in the proper orientation
    for (var i = 0 ; i < points.length ; i++) {
		points[i][0] = this.game.physics.p2.pxmi(points[i][0]);
		points[i][1] = this.game.physics.p2.pxmi(points[i][1]);
	};
    //Add a group of convex polygons to the ground body from
    //our array which forms a concave polygon
	groundBody.data.fromPolygon(points);
	groundBody.debug = this.debug;
	groundBody.kinematic = true;
	this.game.physics.p2.addBody(groundBody);

    return groundBody;
}

When we call original P2 methods, rather than methods adapted from P2 for Phaser, we must convert our vertices to the format that P2 physics expects. We convert from pixels to meters and adjust our points to what P2 needs using the pxmi method. We do this so that we can call the P2 body method fromPolygon. Notice that we must call it from groundBody.data, rather than just groundBody. The difference is that

groundBody is a Phaser P2 body, as can be seen when we initialized it, and it has its original P2 body stored in its data property. The fromPolygon method composes our concave terrain with a bunch of convex shapes. Finally, we add this body to our game. We placed its top left corner at (0,0), and the rest of the body is placed relative to that. Hopefully you have now picked up the skills to make a car and to make terrain. You could even transfer this to other side scrolling games. Be sure to take a look at the terrain code, as I did not place everything here. Feel free to use my Vehicle and TerrainController functions for your own great games!

Up Next