Talking about Platformer game, Game development, HTML5, Javascript, Phaser and TypeScript.
Although Phaser is shipped with Arcade, a great physics engine suitable for most platformer games, sometimes is good to reinvent the wheel basically for two reasons:
1 – Understanding how things work under the hood. I am not talking about complex tasks which would require a lot of time, but everybody should be able to code a basic platformer using no physics engines.
Do you really think there were physics engines in the days of 8bit platformers like Jet Set Willy? Absolutely not, it was all scripted.
2 – Arcade physics does not support slopes, and I want my platformer engine to feature slopes. So rather than tweak the engine like I did some years ago, I’ll write my simple script which is absolutely simpler than Arcade but it fits my need.
I already built custom physics engine for my games, like the one used in Space to Jump, so here’s my simple platformer engine with slopes:
Move with A and D, jump with W. Try to walk up and down along the slopes and jump on them.
Level has been built with Tiled editor and at the moment has three tiles: slope from left to right, normal tile and slope from right to left.
Slopes have 45 degrees of inclination so they can lead from a row to another.
Player behavior changes according to tile being walked on.
Look at the commented source code which consists in one HTML file, one CSS file and five TypeScript files.
index.html
The web page which hosts the game, to be run inside thegame element.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="style.css">
<script src="main.js"></script>
</head>
<body>
<div id = "thegame"></div>
</body>
</html>
style.css
The cascading style sheets of the main web page.
/* remove margin and padding from all elements */
* {
padding : 0;
margin : 0;
}
/* set body background color */
body {
background-color : #000000;
}
/* Disable browser handling of all panning and zooming gestures. */
canvas {
touch-action : none;
}
gameOptions.ts
Configurable game options.
It’s a good practice to place all configurable game options, if possible, in a single and separate file, for a quick tuning of the game.
// CONFIGURABLE GAME OPTIONS
// changing these values will affect gameplay
export const GameOptions : any = {
gameSize : {
width : 480, // width of the game, in pixels
height : 320 // height of the game, in pixels
},
gameBackgroundColor : 0x000000, // game background color
playerSpeed : 50, // player speed, in pixels per second
gravity : 400, // game gravity
jumpForce : 200 // player jump force
}
main.ts
This is where the game is created, with all Phaser related options.
// MAIN GAME FILE
// modules to import
import Phaser from 'phaser'; // Phaser
import { PreloadAssets } from './scenes/preloadAssets'; // preloadAssets scene
import { PlayGame } from './scenes/playGame'; // playGame scene
import { GameOptions } from './gameOptions'; // game options
// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
mode : Phaser.Scale.FIT, // adjust size to automatically fit in the window
autoCenter : Phaser.Scale.CENTER_BOTH, // center the game horizontally and vertically
parent : 'thegame', // DOM id where to render the game
width : GameOptions.gameSize.width, // game width, in pixels
height : GameOptions.gameSize.height // game height, in pixels
}
// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
type : Phaser.WEBGL, // game renderer
backgroundColor : GameOptions.gameBackgroundColor, // game background color
scale : scaleObject, // scale settings
scene : [ // array with game scenes
PreloadAssets, // PreloadAssets scene
PlayGame // PlayGame scene
],
pixelArt : true // set antialiasing as pixel art (no antialiasing)
}
// the game itself
new Phaser.Game(configObject);
scenes > preloadAssets.ts
Here we preload all assets to be used in the game.
// CLASS TO PRELOAD ASSETS
// PreloadAssets class extends Phaser.Scene class
export class PreloadAssets extends Phaser.Scene {
// constructor
constructor() {
super({
key : 'PreloadAssets'
});
}
// method to be called during class preloading
preload() : void {
// load a spritesheet
this.load.spritesheet('hero', 'assets/sprites/hero.png', { // the hero
frameWidth : 20,
frameHeight : 32
});
// load a Tiled tilemap
this.load.tilemapTiledJSON('map', 'assets/maps/level.tmj');
this.load.image('tiles', 'assets/maps/tiles.png')
}
// method to be executed when the scene is created
create() : void {
// start PlayGame scene
this.scene.start('PlayGame');
}
}
scenes > playGame.ts
Main game file, all game logic is stored here.
// THE GAME ITSELF
// modules to import
import { GameOptions } from '../gameOptions'; // game options
import { PhysicsPlayer } from '../physicsPlayer'; // custom class which extends Phaser.GameObjects.Sprite adding some physics features
// PlayGame class extends Phaser.Scene class
export class PlayGame extends Phaser.Scene {
constructor() {
super({
key : 'PlayGame'
});
}
controlKeys : any; // keys used to move the player
hero : PhysicsPlayer; // the player
// method to be called once the instance has been created
create() : void {
// set keyboard controls
const keyboard : Phaser.Input.Keyboard.KeyboardPlugin = this.input.keyboard as Phaser.Input.Keyboard.KeyboardPlugin;
this.controlKeys = keyboard.addKeys({
'left' : Phaser.Input.Keyboard.KeyCodes.A,
'right' : Phaser.Input.Keyboard.KeyCodes.D,
'jump' : Phaser.Input.Keyboard.KeyCodes.W
});
// define player animations
this.anims.create({
key : 'right',
frames : this.anims.generateFrameNumbers('hero', {
start : 0,
end : 3
}),
frameRate : 10,
repeat : -1
});
this.anims.create({
key : 'left',
frames : this.anims.generateFrameNumbers('hero', {
start : 4,
end : 7
}),
frameRate : 10,
repeat : -1
});
// create the map and display layer
const map : Phaser.Tilemaps.Tilemap = this.make.tilemap({
key : 'map'
});
const tiles : Phaser.Tilemaps.Tileset = map.addTilesetImage('tiles', 'tiles') as Phaser.Tilemaps.Tileset;
const layer : Phaser.Tilemaps.TilemapLayer = map.createLayer(0, tiles, 0, 0) as Phaser.Tilemaps.TilemapLayer;
// create the hero and assign properties
this.hero = new PhysicsPlayer(this, 32, this.game.config.height as number - 48, 'hero');
this.hero.gravity = GameOptions.gravity;
this.hero.jumpForce = GameOptions.jumpForce;
this.hero.speed = GameOptions.playerSpeed;
this.hero.layer = layer;
this.hero.leftAnimKey = 'left';
this.hero.rightAnimKey = 'right';
}
// metod to be called at each frame
// time: milliseconds passed since the scene has been created
// delta: milliseconds passed since previous frame
update(time : number, delta : number) {
// check if the player must jump
if (this.controlKeys.jump.isDown) {
this.hero.jump();
}
// check hero movement direction according to keys pressed
this.hero.movement = PhysicsPlayer.movingDirection.NONE;
if (this.controlKeys.right.isDown && !this.controlKeys.left.isDown) {
this.hero.movement = PhysicsPlayer.movingDirection.RIGHT;
}
if (!this.controlKeys.right.isDown && this.controlKeys.left.isDown) {
this.hero.movement = PhysicsPlayer.movingDirection.LEFT;
}
// advance physics simulation
this.hero.step(delta);
}
}
physicsPlayer.ts
Custom class to add some basic physics to a Phaser sprite.
Content of terrainType enum should be edited if you use your own Tiled exported level.
// THE PHYSICS SPRITE
// PhysicsPlayer class extends Phaser.GameObjects.Sprite
export class PhysicsPlayer extends Phaser.Physics.Arcade.Sprite {
jumping : boolean; // is the player jumping?
jumpForce : number; // jump force
gravity : number; // gravity affecting the player
speed : number; // player movement speed
velocity : Phaser.Math.Vector2; // actual player velocity
movement : PhysicsPlayer.movingDirection; // movement direction
layer : Phaser.Tilemaps.TilemapLayer; // Tiled layer where to play
leftAnimKey : string; // key used for left walk animation
rightAnimKey : string; // key used for right walk animation
constructor(scene : Phaser.Scene, posX : number, posY : number, key : string) {
super(scene, posX, posY, key);
this.jumping = false;
this.velocity = new Phaser.Math.Vector2(0, 0);
scene.add.existing(this);
}
// method to make player jump, if not already jumping
jump() : void {
if (!this.jumping) {
this.jumping = true;
this.velocity.y = this.jumpForce;
}
}
// method to find the closest tile below player's feet
raycastBottomTile() : Phaser.Tilemaps.Tile {
const sensorPoint : Phaser.Math.Vector2 = this.getCenter();
let tile : Phaser.Tilemaps.Tile = this.layer.getTileAtWorldXY(sensorPoint.x, sensorPoint.y, true);
while (tile.index == PhysicsPlayer.terrainType.NONE) {
tile = this.layer.getTileAtWorldXY(sensorPoint.x, tile.getCenterY() + tile.height, true);
}
return tile;
}
// method to advance simulation
// milliseconds: amount of milliseconds passed
step(milliseconds : number) : void {
const s : number = milliseconds / 1000;
// handle jumps
if (this.jumping) {
this.setY(this.y - this.velocity.y * s);
this.velocity.y -= this.gravity * s;
}
// handlle movements
switch (this.movement) {
case PhysicsPlayer.movingDirection.RIGHT :
this.velocity.x = this.speed;
this.anims.play(this.rightAnimKey, true);
break;
case PhysicsPlayer.movingDirection.LEFT :
this.velocity.x = -this.speed;
this.anims.play(this.leftAnimKey, true);
break;
case PhysicsPlayer.movingDirection.NONE :
this.velocity.x = 0;
this.anims.stop();
}
// update x position
this.setX(this.x + this.velocity.x * s);
// get tile below the player
const tileBelow : Phaser.Tilemaps.Tile = this.raycastBottomTile();
switch (tileBelow.index) {
// slope like this one: /
case PhysicsPlayer.terrainType.SLOPE_FROM_LEFT :
var groundY : number = tileBelow.getBottom() - this.displayHeight / 2 - (this.x - tileBelow.getLeft());
if (!this.jumping) {
this.setY(groundY);
}
else {
if (this.y > groundY) {
this.jumping = false;
this.setY(groundY);
}
}
break;
// plain terrain
case PhysicsPlayer.terrainType.PLAIN :
var groundY : number = tileBelow.getTop() - this.displayHeight / 2;
if (!this.jumping) {
this.setY(groundY)
}
else {
if (this.y > tileBelow.getTop() - this.displayHeight / 2) {
this.jumping = false;
this.setY(groundY);
}
}
break;
// slope like this one: \
case PhysicsPlayer.terrainType.SLOPE_FROM_RIGHT :
var groundY : number = tileBelow.getBottom() - this.displayHeight / 2 - (tileBelow.getRight() - this.x);
if (!this.jumping) {
this.setY(groundY);
}
else {
if (this.y >= groundY) {
this.jumping = false;
this.setY(groundY);
}
}
break;
}
}
}
export namespace PhysicsPlayer {
// moving directions
export enum movingDirection {
NONE,
LEFT,
RIGHT
}
// terrain types
// YOU MAY NEED TO CHANGE THESE VALUES ACCORDIG TO YOUR TILED EXPORTED LEVEL
export enum terrainType {
NONE = -1,
SLOPE_FROM_LEFT = 1,
PLAIN,
SLOPE_FROM_RIGHT
}
}
There is still a lot to do, such as walls to block player movements or holes to make player fall down, but I’ll show you how to build them in next tutorial, meanwhile download the entire project and build something useful out of it.
Don’t know where to start? I have a free guide for you.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.