In this unit we will define a simple Phaser game that can be used as a template to define an unlimited number of levels, enemies and objects. The map will be drawn from a simple array of tiles where several characters will be used to choose the positions of each item.
We will also use npm and webpack to provide a friendly development environment, so that the application will be automatically compiled and restarted each time a file is changed. A web server is also provided by webpack to test the application, and a production version of the code can be also obtained to upload it to any public server.
Some features
These are some of the features to be implemented:
Show several types of animated objects that can be collected individually.
Keep several counters with the number of the items collected.
Show several types of animated enemies that can be killed just by jumping over them.
Move automatically the enemies from left to right and viceversa using spritesheets that contain only a single direction.
Draw the map using plain characters that are matched againt tiles, objects, enemies, player, and goal.
Easily define an unlimited number of levels and let the player jump from one level to the other just by reaching a goal in the map.
Display a minimap automatically generated from the real sized map.
Play a different background music for each level, and a different sound when either objects are collected or enemies are killed.
Allow the user to play the game on mobile phones and tablets with touch controls to jump and also to activate both left and right movements.
Implement PWA features to let the users install the application in mobile devices and also to lock the screen in landscape mode.
Adjust the game automatically to the size of the window where the game is displayed, and also after the window is resized on desktop computers.
Loading the assets (src/scripts/scenes/preloadScene.ts)
Inside this file we will load all the assets (both audio and images) so that they are ready to be played or displayed after the game is started:
We will use plain characters to represent the player, the goal, the objects, the enemies and the ground:
P: Player
G: Goal (the door to jump to the next level)
Objects:
C: Coin
K: Key (at least 1 key is required to open the door to the next level)
Enemies:
B: Bee (a flying enemy)
S: Slime (an enemy crawling on the ground)
[/// … ///]: The ground
Defining the levels (src/scripts/components/levels/levels.ts)
As it can be seen in the “levels.ts” file, the map of each level can be easily defined using simple characters. We can also place all the enemies, objects, player and goal at any position in the map:
type Level = string[]
// prettier-ignore
const level0: Level = [
' K S ',
' [/////] ',
' ',
' P G ',
'[/////] [////]',
]
// prettier-ignore
const level1: Level = [
' B K ',
' [/////] ',
' C C ',
' S B S S G ',
'[//] [////] [////] [//////] ',
' C C ',
' P B ',
'[///] [//] ',
]
// prettier-ignore
const level2: Level = [
' P [//] G ',
'[/] C B C [/] ',
' [//] [//] ',
' S B B ',
' C [//] B [///] ',
' [///] [] C C ',
'[] K ',
' [/] [/] [/] '
]
// prettier-ignore
const level3: Level = [
' C ',
' C C ',
' [///] C [////] ',
' [///] S [//] K ',
' [///] [///] ',
' S ',
' [///] C G ',
' C C [////] ',
' P S B ',
'[//////] [///] [////] '
]
// prettier-ignore
const level4: Level = [
' C C S ',
' [//////] K B G ',
' C [/////] [///////] [////] ',
' ',
' P S B B C S S S ',
'[////////] [///////] [/] [////////////////////] '
]
const Levels: Level[] = [level0, level1, level2, level3, level4]
export default Levels
Matching characters to textures (src/scripts/components/map/map.ts)
In case we want to add new enemies or objects, we will have to match a new character against the new texture:
import Levels from '../levels/levels'
export class Map {
info: TilesConfig[]
size: MapSize
public static calcCurrentLevel(currentLevel: number) {
const MAX_LEVELS = Levels.length
return currentLevel % MAX_LEVELS
}
constructor(currentLevel: number) {
const TILE_SIZE = 96
const config: any = {
'[': {
type: 'tile',
texture: 'tile-left'
},
'/': {
type: 'tile',
texture: 'tile-middle'
},
']': {
type: 'tile',
texture: 'tile-right'
},
'G': {
type: 'goal',
texture: 'goal'
},
'C': {
type: 'object',
texture: 'coin'
},
'K': {
type: 'object',
texture: 'key'
},
'S': {
type: 'enemy',
texture: 'slime'
},
'B': {
type: 'enemy',
texture: 'bee'
},
'P': {
type: 'player',
texture: 'player'
}
}
const map = Levels[Map.calcCurrentLevel(currentLevel)]
// the player can jump a bit higher than the map's height
const paddingTop = 4 * TILE_SIZE
this.size = {
x: 0,
y: 0,
width: map[0].length * TILE_SIZE,
height: map.length * TILE_SIZE + paddingTop
}
this.info = []
map.forEach((row, y) => {
for (let i = 0; i < row.length; i++) {
const tile = row.charAt(i)
const x = i
if (tile !== ' ') {
let info = { ...config[tile.toString()], x: x * TILE_SIZE, y: y * TILE_SIZE + paddingTop }
this.info.push(info)
}
}
})
}
}
The counters of coins and keys are incremented when any of these objects is collected:
...
this.physics.add.overlap(this.player, objectsGroup, (player: Player, object: ObjectClass) => {
if (object.collecting) return
if (object instanceof CoinSprite) this.score++
else if (object instanceof KeySprite) this.keys++
scoreText.update(this.score, this.keys)
object.collect()
})
...
The player (src/scripts/components/player/player.ts)
The get a more realistic movement, the player is animated using the “spine” plugin. The full documentation about this plugin can be found here, and some examples can be found here.
Player animation using spine plugin (assets/spine/boy.png)Player killed (assets/audio/hurt.mp3)
import Controls from '../controls/controls'
import PlayerSpine from './playerSpine'
export default class Player extends Phaser.Physics.Arcade.Sprite {
private _dead: boolean = false
private _halt: boolean = false
private mapSize: MapSize
playerSpine: PlayerSpine
audioHurt: any
constructor(scene: Phaser.Scene, player: TilesConfig, mapSize: MapSize, level: number) {
super(scene, player.x, player.y, player.texture)
scene.add.existing(this)
scene.physics.add.existing(this)
this.scene = scene
this.mapSize = mapSize
this.setVisible(false)
this.setOrigin(0, 1)
this.setDragX(1500)
this.body.setSize(70, 132)
this.body.setOffset(25, 24)
let theSkin = level % 2 == 0 ? 'blue' : 'green'
this.playerSpine = new PlayerSpine(scene, this.body.center.x, this.body.bottom)
this.playerSpine.setSkin(theSkin)
this.audioHurt = scene.sound.add('hurt', { volume: 1.0 })
}
kill() {
this._dead = true
this.audioHurt.play()
// animate the camera if the player dies
this.scene.cameras.main.shake(500, 0.025)
this.scene.time.addEvent({
delay: 500,
callback: () => this.scene.scene.restart()
})
}
killEnemy() {
this.playerSpine.spine.customParams.isKilling = true
this.setVelocityY(-600)
}
halt() {
this.body.enable = false
this._halt = true
}
update(cursors: any, controls: Controls) {
if (this._halt || this._dead) return
// check if out of camera and kill
if (this.body.right < this.mapSize.x || this.body.left > this.mapSize.width || this.body.top > this.mapSize.height)
this.kill()
// controls left & right
if (cursors.left.isDown || controls.leftIsDown) {
this.setVelocityX(-500)
this.playerSpine.spine.setScale(-1, 1)
} else if (cursors.right.isDown || controls.rightIsDown) {
this.setVelocityX(550)
this.playerSpine.spine.setScale(1, 1)
}
// controls up
if ((cursors.up.isDown || cursors.space.isDown || controls.upIsDown) && this.body.blocked.down) {
this.setVelocityY(-1250)
}
// update spine animation
this.playerSpine.update(this)
}
}
The goal (src/scripts/components/goal/goalSprite.ts)
When the user collides with the door, an achievement sound will be played, and it will jump to the next level.
The goal (assets/img/goal.png)Next level (assets/audio/door.mp3)
This game is 100% PWA ready, so that it can be installed and executed in your mobile device as it was an app from the Play Store. You can easily personalize its settings by following these steps:
Replace both icons in /pwa/icons with your own.
One is 512×512 the other 192×192.
Add your own favicon.ico to /src.
Adjust these parameters in the manifest.json file in /pwa:
short_name: Max. 12 characters.
name: The full game name.
orientation: “landscape” or “portrait”.
background_color: color of the splash screen.
theme_color: color of the navbar – has to match the theme-color in the index.html file.
You can leave the sw.js (serviceWorker) file in /pwa as it is.
You can get the game up and running just following these steps:
Install nodejs (in case you do not have installed it yet). Select the version recommended for most users and just click next on each option to use the default settings.
Download the code. You can download from this link a single zip file containing the full game template and all the assets.
Uncompress the zip file, and open a terminal from inside the project folder.
Execute “npm install” to install all the dependencies requested by the project.
Execute “npm run start” to compile and execute the code. After this command is completed, a new tab will be opened in your default browser. The game will also be refreshed automatically when the source code changes.
Execute “npm run build” to get a production version. After executing this command, all the html, css and js files, and also all the assets required to run the game will be put inside the “dist” folder, ready to be uploaded to any public web server. For example, you could make your game available to anyone just by creating a new project in https://replit.com and uploading all the files inside “dist” to the root folder of the replit project.
Play the game on your browser
You have also a version ready to be played online here.
Install the game in your phone
Since all the PWA functionality is included in this project, you can install your application in any mobile device (Chrome browser and Android operating system are the preferred options for this purpose). You will get the same behaviour as the apps in the play store: an icon will be created after carrying out the installation from your browser, a loading screen will be shown when the game is started, and the game will run in fullscreen and landscape mode.
When you open the address of the game in your browser, a message to install the application is displayed after a few seconds:
Installing the app
Or you can also install the game manually just by selecting the option “Instalar aplicación” from the browser menu:
Installing the app
Bibliography
More information about the template used to build this game can be found on github.