It supports even little-known features, themes, additional file types, and accessibility features like Eye Gaze Mode and Speech Recognition (more information on github).
Ah yes, good old Paint. Not the one with the ribbons or the new skeuomorphic one with the interface that can take up nearly half the screen. (And not the even newer Paint 3D.)
Windows 95, 98, and XP were the golden years of Paint. You had a tool box and a color box, a foreground color and a background color, and that was all you needed.
Things were simple. But we want to undo more than three actions. We want to edit transparent images. We can’t just keep using the old Paint. So that’s why the author is making JS Paint. He wants to bring good old Paint into the modern era.
Rudimentary multi-user collaboration support. Start up a session at https:/paint.fernandoruizrico.com/#session:multi-user-test and send the link to your friends! It isn’t seamless; actions by other users interrupt what you’re doing, and visa versa. Sessions are not private, and you may lose your work at any time. If you want better collaboration support, follow the development of Mopaint.
Extras > Themes to change the look of the app. Dark mode included.
Eye Gaze Mode, for use with an eye tracker, head tracker, or other coarse input device, accessible from Extras > Eye Gaze Mode. With just a webcam, you can try it out with Enable Viacam (head tracker) or GazePointer (eye tracker).
Speech Recognition Mode. Using your voice you can select tools and colors, pan the view (“scroll down and to the left”, or “go southwest”, etc.), explore the menus (but you can activate any menu item without opening the menus first), interact with windows (including scrolling the history view with “scroll up”/”scroll down” etc.), dictate text with the Text tool, and even tell the application to sketch things (for instance, “draw a house”)
Create an animated GIF from the current document history. Accessible from the Extras menu or with Ctrl+Shift+G. It’s pretty nifty, you should try it out! You might want to limit the size of the image though.
You can also drag and drop palette files into the app to load.
Editing Features
Use Alt+Mousewheel to zoom in and out
Edit transparent images! To create a transparent image, go to Image > Attributes… and select Transparent, then OK, and then Image > Clear Image or use the Eraser tool. Images with any translucent pixels will open in Transparent mode.
You can crop the image by making a selection while holding Ctrl
Keyboard shortcuts for rotation: Ctrl+. and Ctrl+, (< and >)
Rotate by any arbitrary angle in Image > Flip/Rotate
In Image > Stretch/Skew, you can stretch more than 500% at once
Zoom to an arbitrary scale in View > Zoom > Custom…
Zoom to fit the canvas within the window with View > Zoom > Zoom To Window
Non-contiguous fill: Replace a color in the entire image by holding Shift when using the fill tool
You can use the Text tool at any zoom level (and it previews the exact pixels that will end up on the canvas).
Spellcheck is available in the textbox if your browser supports it.
Resize handles are easier to grab than in Windows 10’s Paint.
Omits some Thumbnail view bugs, like the selection showing in the wrong place.
Unlimited undos/redos (as opposed to a measly 3 in Windows XP, or a measly 50 in Windows 7)
Undo history is nonlinear, which means if you undo and do something other than redo, the redos aren’t discarded. Instead, a new branch is created in the history tree. Jump to any point in history with Edit > History or Ctrl+Shift+Y
Automatically keeps a backup of your image. Only one backup per image tho, which doesn’t give you a lot of safety. Remember to save with File > Save or Ctrl+S! Manage backups with File > Manage Storage.
Limitations
A few things with the tools aren’t done yet. See TODO.md
Full clipboard support in the web app requires a browser supporting the Async Clipboard API w/ Images, namely Chrome 76+ at the time of writing.
In other browsers you can still can copy with Ctrl+C, cut with Ctrl+X, and paste with Ctrl+V, but data copied from JS Paint can only be pasted into other instances of JS Paint. External images can be pasted in.
Supported File Formats
Image Formats
⚠️ Saving as JPEG will introduce artifacts that cause problems when using the Fill tool or transparent selections.
⚠️ Saving in some formats will reduce the number of colors in the image.
💡 Unlike in MS Paint, you can use Edit > Undo to revert color or quality reduction from saving. This doesn’t undo saving the file, but allows you to then save in a different format with higher quality, using File > Save As.
💡 Saving as PNG is recommended as it gives small file sizes while retaining full quality.
Capabilities marked with 🌐 are currently left up to the browser to support or not. If “Write” is marked with 🌐, the format will appear in the file type dropdown but may not work when you try to save. For opening files, see Wikipedia’s browser image format support table for more information.
Capabilities marked with 🔜 are coming soon, and N/A of course means not applicable.
“Read Palette” refers to loading the colors into the Colors box automatically (from an indexed color image), and “Write Palette” refers to writing an indexed color image.
Color Palette Formats
With Colors > Save Colors and Colors > Get Colors you can save and load colors in many different formats, for compatibility with a wide range of programs.
If you want to add extensive palette support to another application, I’ve made this functionality available as a library: AnyPalette.js
There’s a black and white mode with patterns instead of colors in the palette, which you can get to from Image > Attributes…
You can drag the color box and tool box around if you grab them by the right place. You can even drag them out into little windows. You can dock the windows back to the side by double-clicking on their title bars.
In addition to the left-click foreground color and the right-click background color, there’s a third color you can access by holding Ctrl while you draw. It starts out with no color so you’ll need to hold Ctrl and select a color first. The fancy thing about this color slot is you can press and release Ctrl to switch colors while drawing.
You can apply image transformations like Flip/Rotate, Stretch/Skew or Invert (in the Image menu) either to the whole image or to a selection. Try scribbling with the Free-Form Select tool and then doing Image > Invert
In this unit we will build a clone of Google Chrome “secret” Dino game (the original game can be revealed by entering “chrome://dino” in the address bar). We will use simple web technologies such as HTML, CSS, JavaScript and Phaser 3, so that any user can play on any device just using any browser (not only Chrome).
The result is a game which looks almost the same as the original one using less than 10% lines of code. As it can be seen from the Chromium source code, the original game is made of more than 4000 lines of code, whereas the one in this unit has been developed using around 300 lines of code. This means that Phaser could be a much better option than simple JavaScript when thinking about game development.
Some features
This version of the game clones almost every feature of the original one:
The speed of the game is increased gradually.
Both cactuses and birds will appear randomly to hurt you.
Two scores are shown: current and highest.
Each time the user reaches 100 points, the score blinks and a sound is played.
It can be played on desktop computers using both the mouse and the keyboard:
By clicking on the screen to jump or to restart the game.
Using the up arrow key or the space bar to jump, the down arrow key to duck under the enemy, and the enter key to restart the game.
It can be played on mobile devices by touching the screen, the same way as any single button game.
The game is automatically adjusted to fit the screen size, even if it changes after the page is loaded (for example, in case the user rotates the device).
Vibration is activated on mobile devices when the game over screen appears.
CSS code (styles.css)
We are going to insert some CSS code to remove any margin and padding, and also to remove the scrollbars, so that the whole window size is used when the game is started:
We are going to define some constants (i.e. screen width and height and text styles) so that we can use them inside every JavaScript file. We will also define a function to set the initial values of some variables before the game is started, together with the usual Phaser configuration, which must be performed as usual.
Moreover we are adding a piece of code to check whether the screen is being resized (i.e. when the device is rotated or the window size is adjusted) so that the whole game is reloaded to fit the new values of the screen width and height.
We will need some images, sprites and sounds. You may use any assets you like, but we are providing the original ones so that you can easily develop the same game as the one available in Google Chrome. Some examples are shown below, and also a zip file containing all the assets can be downloaded here.
Images
CloudCactuses
Game overRestart
Sprites
Dino waiting and runningDino downBird enemy
Sounds
HitJumpReach
Loading all the assets (preload.js)
So that we can use all those images, sprites and sounds, they have to be loaded previously. That is the purpose of the “preload()” function.
Inside the “initSounds()” and “createAnims()” we will set the volume of the sounds and the frames of the animations respectively.
Creating the scores, the clouds, the dino, and the game over message (create.js)
Scores: Two boxes are used to print both the current and highest scores on the right hand side of the screen.
Clouds: A loop will be used to create 3 clouds at once printing the same image on random positions.
Dino: The dino will collide with the ground below, and it will jump using the keys defined here (in our case we will use both the up cursor key and the space bar). The mouse functionality is already available by default.
Game over: A message will be shown when the user collides with either the birds or the cactuses. After the user clicks on it, this message will be hidden using the “setAlpha(0)” function call.
Controlling the player and displaying the birds and the cactuses (update.js)
Starting the game and building the ground: The “startGame()” method will build the ground progresively, just once, when the game is started the very first time.
Restarting the game: After the game over message appears, the user may click on the screen to start a new game. This is done inside the “restartGame()” method, which hides the message using the “setAlpha(0)” method call, restarts the score and resumes all the physics.
Updating the scores: The function “updateScore()” will not only update the current score, but it will also increment the game speed, and it will play a sound each time the user reaches 100 points, making the numbers blink at the same time. The “updateHighScore()” function will keep a record of the highest score each time a game is over.
Placing and moving the birds and the cactuses: The “placeObsticle()” function will create either birds or cactuses randomly, and the “update()” function will move everything to the left except the dino, which will always remain in the same position.
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.
You will find in this post several free and open-source 2D sprite editors and pixel art tools that can be used on your browser to create fancy pictures and animations.
Pixelorama
Pixelorama is an awesome free and open source pixel art editor, proudly created with the Godot engine, by Orama Interactive. Whether you want to make animated pixel art, game graphics, tiles and any kind of pixel art you want, Pixelorama has you covered with its variety of tools and features (you can find online documentation here). Free to use for everyone:
A variety of different tools to help you draw, with the ability to map a different tool in each left and right mouse buttons.
Are you an animator? Pixelorama has its own animation timeline just for you! You can work at an individual cel level, where each cel refers to a unique layer and frame. Supports onion skinning, cel linking, motion drawing and frame grouping with tags.
Custom brushes, including random brushes.
Create or import custom palettes.
Import images and edit them inside Pixelorama. If you import multiple files, they will be added as individual animation frames. Importing spritesheets is also supported.
Export your gorgeous art as PNG, as a single file, a spritesheet or multiple files, or GIF file.
Pixel perfect mode for perfect lines, for the pencil, eraser & lighten/darken tools.
Autosave support, with data recovery in case of a software crash.
Horizontal & vertical mirrored drawing.
Tile mode for pattern creation.
Rectangular & isometric grid types.
Scale, rotate and apply multiple image effects to your drawings.
Multi-language localization support! See the Crowdin page for more details.
Did you know that you can create pixel art using CSS? Pixel Art to CSS is an online editor that helps you with that task. Combining the power of both box-shadow and keyframes CSS properties, you will get CSS code ready to use in your site. Furthermore, you can download your work in different formats such as a static image, animated GIF or sprite like image.
Pixel Art to CSS aims to be an intuitive tool by its simplicity, however it is equipped with a wide range of features: customize your color palette, go back and forth in time, modify animation settings, save or load your projects, among others.
In this unit we will build a clone of Google Chrome “secret” Dino game (the original game can be revealed by entering “chrome://dino” in the address bar). We will use simple web technologies such as HTML, CSS, JavaScript and Phaser 3, so that any user can play on any device just using any browser (not only Chrome).
The result is a game which looks almost the same as the original one using less than 10% lines of code. As it can be seen from the Chromium source code, the original game is made of more than 4000 lines of code, whereas the one in this unit has been developed using around 300 lines of code. This means that Phaser could be a much better option than simple JavaScript when thinking about game development.
Some features
This version of the game clones almost every feature of the original one:
The speed of the game is increased gradually.
Both cactuses and birds will appear randomly to hurt you.
Two scores are shown: current and highest.
Each time the user reaches 100 points, the score blinks and a sound is played.
It can be played on desktop computers using both the mouse and the keyboard:
By clicking on the screen to jump or to restart the game.
Using the up arrow key or the space bar to jump, the down arrow key to duck under the enemy, and the enter key to restart the game.
It can be played on mobile devices by touching the screen, the same way as any single button game.
The game is automatically adjusted to fit the screen size, even if it changes after the page is loaded (for example, in case the user rotates the device).
Vibration is activated on mobile devices when the game over screen appears.
CSS code (styles.css)
We are going to insert some CSS code to remove any margin and padding, and also to remove the scrollbars, so that the whole window size is used when the game is started:
html, body {
margin: 0px;
padding: 0px;
overflow: hidden;
}
HTML code (index.html)
We are going to use a single HTML file to link all the files in the project. This is going to be the “index.html” file:
We are going to define some constants (i.e. screen width and height and text styles) so that we can use them inside every JavaScript file. We will also define a function to set the initial values of some variables before the game is started, together with the usual Phaser configuration, which must be performed as usual.
Moreover we are adding a piece of code to check whether the screen is being resized (i.e. when the device is rotated or the window size is adjusted) so that the whole game is reloaded to fit the new values of the screen width and height.
// Fill the whole width and height of the screen
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;
// Time in milliseconds for each obsticle (lower = faster)
const OBSTICLE_TIME = 750;
// Text style to be used when printing texts (scores, etc.)
const TEXT_STYLE = { fill: "#535353", font: '900 35px Courier', resolution: 10 };
// Variables to be initialized each time the game is restarted
function initVariables() {
this.isGameRunning = false;
this.gameSpeed = 10;
this.respawnTime = 0;
this.score = 0;
}
// Phaser initialization
let game = new Phaser.Game({
type: Phaser.AUTO,
width: WIDTH,
height: HEIGHT,
pixelArt: true,
transparent: true,
physics: {
default: 'arcade',
arcade: {
debug: false
}
},
scene: { preload: preload, create: create, update: update }
});
// Reload the game when the device orientation changes or the user resizes the window
let resizing = false;
window.addEventListener('resize', () => {
if (resizing) clearTimeout(resizing);
resizing = setTimeout(() => window.location.reload(), 500);
});
The assets
We will need some images, sprites and sounds. You may use any assets you like, but we are providing the original ones so that you can easily develop the same game as the one available in Google Chrome. Some examples are shown below, and also a zip file containing all the assets can be downloaded here.
Images
CloudCactuses
Game overRestart
Sprites
Dino waiting and runningBird enemy
Sounds
HitJumpReach
Loading all the assets (preload.js)
So that we can use all those images, sprites and sounds, they have to be loaded previously. That is the purpose of the “preload()” function.
Inside the “initSounds()” and “createAnims()” we will set the volume of the sounds and the frames of the animations respectively.
Creating the scores, the clouds, the dino, and the game over message (create.js)
Scores: Two boxes are used to print both the current and highest scores on the right hand side of the screen.
Clouds: A loop will be used to create 3 clouds at once printing the same image on random positions.
Dino: The dino will collide with the ground below, and it will jump using the keys defined here (in our case we will use both the up cursor key and the space bar). The mouse functionality is already available by default.
Game over: A message will be shown when the user collides with either the birds or the cactuses. After the user clicks on it, this message will be hidden using the “setAlpha(0)” function call.
// Score text on the right side of the screen
function createScore() {
this.scoreText = this.add.text(WIDTH - 25, HEIGHT / 3, "", TEXT_STYLE).setOrigin(1, 1);
this.highScoreText = this.add.text(0, HEIGHT / 3, "", TEXT_STYLE).setOrigin(1, 1).setAlpha(0.75);
}
// Three clouds hidden at the beginning
function createClouds() {
this.clouds = this.add.group();
for (let i = 0; i < 3; i++) {
const x = Phaser.Math.Between(0, WIDTH);
const y = Phaser.Math.Between(HEIGHT / 3, HEIGHT / 2);
this.clouds.add(this.add.image(x, y, 'cloud'));
}
this.clouds.setAlpha(0);
}
// Dino starts running and the the ground is created
function createGround() {
this.startText.destroy();
this.dino.setVelocityX(100);
this.dino.play('dino-run', 1);
const interval = setInterval(() => {
this.ground.width += (WIDTH / 100);
if (this.ground.width >= WIDTH) {
clearInterval(interval);
this.dino.setVelocityX(0);
this.scoreText.setAlpha(1);
this.clouds.setAlpha(1);
this.isGameRunning = true;
}
}, 50);
}
// Dino waiting animation, keyboard initilization and starting text
function createDino() {
this.dino = this.physics.add.sprite(0, HEIGHT / 1.5, 'dino-idle').setSize(50).setGravityY(5000).setOrigin(0, 1);
this.dino.play('dino-waiting', true);
this.physics.add.collider(this.dino, this.belowGround);
this.physics.add.collider(this.dino, this.obsticles, () => stopGame.call(this));
this.cursors = this.input.keyboard.createCursorKeys();
this.spaceBar = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
this.startText = this.add.text(WIDTH - 15, HEIGHT / 1.5, "Press to play", TEXT_STYLE).setOrigin(1);
}
// Game over screen and pause until player starts the game again
function createGameOverScreen() {
this.gameOverText = this.add.image(0, 0, 'game-over');
this.restart = this.add.image(0, 50, 'restart').setInteractive();
this.gameOverScreen = this.add.container(WIDTH / 2, HEIGHT / 2 - 50);
this.gameOverScreen.add([this.gameOverText, this.restart]);
this.input.keyboard.on('keydown_ENTER', () => startGame.call(this));
this.restart.on('pointerdown', () => startGame.call(this));
}
// Define the physical ground and obsticles group
function createWorld() {
this.ground = this.add.tileSprite(0, HEIGHT / 1.5, 88, 26, 'ground').setOrigin(0, 1);
this.belowGround = this.add.rectangle(0, HEIGHT / 1.5, WIDTH, HEIGHT / 3).setOrigin(0, 0);
this.physics.add.existing(this.belowGround);
this.belowGround.body.setImmovable();
this.obsticles = this.physics.add.group();
}
// Create all the elements in the game and initilize the score
function create() {
initVariables.call(this);
initSounds.call(this);
createAnims.call(this);
createWorld.call(this);
createClouds.call(this);
createScore.call(this);
createDino.call(this);
setInterval(() => updateScore.call(this), 100);
}
Controlling the player and displaying the birds and the cactuses (update.js)
Starting the game and building the ground: The “createGround()” method will build the ground progresively, just once, when the game is started the very first time (after the user taps the screen or presses either the up arrow key or the space bar).
Restarting the game: After the game over message appears, the user may click on the screen to start a new game. This is done inside the “startGame()” method, which clears the message using the “destroy()” method call, restarts the score and resumes all the physics.
Updating the scores: The function “updateScore()” will not only update the current score, but it will also increment the game speed, and it will play a sound each time the user reaches 100 points, making the numbers blink at the same time. The “updateHighScore()” function will keep a record of the highest score each time a game is over.
Placing and moving the birds and the cactuses: The “placeObsticle()” function will create either birds or cactuses randomly, and the “update()” function will move everything to the left except the dino, which will always remain in the same position.
// Restart the game after dino is hurt
function startGame() {
this.dino.setVelocityY(0);
this.physics.resume();
this.anims.resumeAll();
this.obsticles.clear(true, true);
this.gameOverScreen.destroy();
this.isGameRunning = true;
}
// On game over a sound is played and all physics are paused
// The game over screen will be shown and mobile devices will vibrate
function stopGame() {
initVariables.call(this);
updateHighScore.call(this);
this.hitSound.play();
this.physics.pause();
this.anims.pauseAll();
this.dino.setTexture('dino-hurt');
createGameOverScreen.call(this);
if (window.navigator.vibrate) window.navigator.vibrate(50);
}
// Update the score and increase the game speed
function updateScore() {
if (!this.isGameRunning) return;
this.score++;
this.gameSpeed += 0.01;
if (this.score % 100 == 0) {
this.reachSound.play();
this.tweens.add({
targets: this.scoreText,
duration: 100,
repeat: 3,
alpha: 0,
yoyo: true
})
}
this.scoreText.setText(("0000" + this.score).slice(-5));
}
// Update the high score in case is higher than the previous one
function updateHighScore() {
this.highScoreText.x = this.scoreText.x - this.scoreText.width - 30;
const highScore = this.highScoreText.text.substr(this.highScoreText.text.length - 5);
const newScore = Number(this.scoreText.text) > Number(highScore) ? this.scoreText.text : highScore;
this.highScoreText.setText(`HI ${newScore}`);
}
// Place a new obsticle on the screen (either cactuses or birds)
function placeObsticle() {
let obsticle;
const obsticleNum = Phaser.Math.Between(1, 7);
if (obsticleNum > 6) {
obsticle = this.obsticles.create(WIDTH, HEIGHT / 1.5 - Phaser.Math.Between(20, 50), 'enemy-bird');
obsticle.body.height /= 1.5
obsticle.play('enemy-bird-anim', 1);
} else {
obsticle = this.obsticles.create(WIDTH, HEIGHT / 1.5, `obsticle-${obsticleNum}`)
}
obsticle.setOrigin(0, 1).setImmovable();
}
// Update the screen (obsticles, ground, clouds and dino) and check the keyboard and mouse (or touch screen) continuously
// Dino will start running and jump if the user taps the screen or presses either the arrow up key or the spacebar
function update() {
let jump = this.cursors.up.isDown || this.spaceBar.isDown || this.input.activePointer.isDown;
if (this.isGameRunning) {
this.ground.tilePositionX += this.gameSpeed;
this.respawnTime += this.gameSpeed;
if (this.respawnTime >= OBSTICLE_TIME) {
this.respawnTime = 0;
placeObsticle.call(this);
}
this.obsticles.getChildren().forEach(obsticle => {
obsticle.x -= this.gameSpeed;
if (obsticle.getBounds().right < 0) {
obsticle.destroy();
}
})
this.clouds.getChildren().forEach(cloud => {
cloud.x -= 0.5;
if (cloud.getBounds().right < 0) {
cloud.x = WIDTH;
}
})
if (this.dino.body.onFloor()) {
if (jump) {
this.jumpSound.play();
this.dino.anims.stop();
this.dino.setVelocityY(-1600);
this.dino.setTexture('dino', 0);
}
else {
this.dino.play('dino-run', true);
}
}
}
else if (jump && (this.ground.width < WIDTH)) {
createGround.call(this);
}
}
Proposed exercise: Using your own assets
Create a new version of the Dino game using your own assets (images, sprites and sounds). You can also change any other things you like.
Do not forget to create all the files required to run the code in this unit:
styles.css
index.html
preload.js
create.js
update.js
config.js
assets folder
You may find many free assets in OpenGameArt, and some new versions of the Dino game in github:
You can download this zip file to get the source code and all the assets.
Updating the map
As done previously, you can use the tile map editor to update the map. In case you have not done it yet, you can download the editor from this link and install it on your computer.
To update the map you have to go the assets folder and open the file “town.tmx” which contains the map of the example in this unit. Select a layer (on the right corner, at the top) and change anything you like (by adding or removing some objects to or from the map). When you finish, export the map to JSON format using the option “File” -> “Export as…”, and overwrite the file “town.json”, located inside the “assets” folder.
Finally check the results using your browser and move the player to go to each part of the map. You will notice that the camera will follow you on each movement. Do not forget to fully refresh the contents of the browser by pressing Ctrl+F5, since the map and some other contents are usually cached by the browser. Also move the player and check that the map has been updated with the changes you have made.
Frames per second (FPS)
You can update the number of frames per second to choose the best renderization performance for your computer:
const MAX_FPS = 25;
Using the keyboard and the mouse
In this game we are using the keys A (left), S (down), D (right), W (up) to move the player on desktop computers and the joystick for mobile devices:
let vX = 0, vY = 0;
// Horizontal movement
if (wsadKeys.A.isDown || joyStick.left) vX = -speed;
else if (wsadKeys.D.isDown || joyStick.right) vX = speed;
// Vertical movement
if (wsadKeys.W.isDown || joyStick.up) vY = -speed;
else if (wsadKeys.S.isDown || joyStick.down) vY = speed;
The mouse is used for shooting on desktop computers:
let pointer = this.input.activePointer;
if (pointer.isDown && this.sys.game.device.os.desktop) createBullet.call(this, pointer.worldX - player.x, pointer.worldY - player.y);
In case you do not have any mouse, you may use the cursor keys on desktop computers, or the second joystick on mobile devices, to shoot the bullets and to choose the shooting direction:
let vX = 0, vY = 0;
// Horizontal bullets
if (cursors.left.isDown || joyStick2.left) vX = -speed;
else if (cursors.right.isDown || joyStick2.right) vX = speed;
// Vertical bullets
if (cursors.up.isDown || joyStick2.up) vY = -speed;
else if (cursors.down.isDown || joyStick2.down) vY = speed;
if (vX || vY) createBullet.call(this, vX, vY);
Enjoy the game!
You may enjoy playing this wonderful game online here.
You can have a look at this preview of the whole map.
The source code and the assets
You can download this zip file to get the source code and all the assets.
Updating the map
As done previously, you can use the tile map editor to update the map. In case you have not done it yet, you can download the editor from this link and install it on your computer.
To update the map you have to go the assets folder and open the file “town.tmx” which contains the map of the example in this unit. Select a layer (on the right corner, at the top) and change anything you like (by adding or removing some objects to or from the map). When you finish, export the map to JSON format using the option “File” -> “Export as…”, and overwrite the file “town.json”, located inside the “assets” folder.
Since we are working with a lot of tilesets, in this case we are setting the collisions with all the objects in some of the layers: “Terrain_3”, “Buildings_1” and “Buildings_2”. The player and the enemies will collide with all the objects that you drawn in these three layers.
Finally check the results using your browser and move the player to go to each part of the map. You will notice that the camera will follow you on each movement. Do not forget to fully refresh the contents of the browser by pressing Ctrl+F5, since the map and some other contents are usually cached by the browser. Also move the player and check that the map has been updated with the changes you have made.
Renderization problems
Due to the large amount of resources this game is going to use from your computer, you might need to adjust a couple of parameters. It seems it works fine on Chrome browser, but if you are using another browser, or in case you find any problems, you can try updating the following parameters.
Phaser renderer
I have selected “Phaser.CANVAS” inside the phaser configuration settings, which seems to work much better on Chrome browser, but you may try “Phaser.AUTO” instead:
const config = {
type: Phaser.AUTO
...
}
Frames per second (FPS)
You can also change the number of frames per second to choose the right renderization performance:
const MAX_FPS = 25;
Using the keyboard and the mouse
In this game we are using the keys A (left), S (down), D (right), W (up) to move the player on desktop computers and the joystick for mobile devices:
let vX = 0, vY = 0;
// Horizontal movement
if (wsadKeys.A.isDown || joyStick.left) vX = -speed;
else if (wsadKeys.D.isDown || joyStick.right) vX = speed;
// Vertical movement
if (wsadKeys.W.isDown || joyStick.up) vY = -speed;
else if (wsadKeys.S.isDown || joyStick.down) vY = speed;
The mouse is used for shooting on desktop computers:
let pointer = this.input.activePointer;
if (pointer.isDown && this.sys.game.device.os.desktop) createBullet.call(this, pointer.worldX - player.x, pointer.worldY - player.y);
In case you do not have any mouse, you may use the cursor keys on desktop computers, or the second joystick on mobile devices, to shoot the bullets and to choose the shooting direction:
let vX = 0, vY = 0;
// Horizontal bullets
if (cursors.left.isDown || joyStick2.left) vX = -speed;
else if (cursors.right.isDown || joyStick2.right) vX = speed;
// Vertical bullets
if (cursors.up.isDown || joyStick2.up) vY = -speed;
else if (cursors.down.isDown || joyStick2.down) vY = speed;
if (vX || vY) createBullet.call(this, vX, vY);
Enjoy the game!
You may enjoy playing this wonderful game online here.
In this unit we are going to use Phaser together with a tilemap editor to build a maze. You may find more information at the official page of the Tiled editor and you may also have a look at some interesting demonstrations at the Phaser’s examples page, and also performing some search on the latest examples.
HTML and CSS code
The first thing we should do is linking the Phaser library. We will use the last version at the time this unit has been written:
After that we are going to insert some CSS code to remove any margin and padding, and also to remove the scrollbars, so that all the window size is used when the game is started:
We are going to define some constants at the beginning of the code so that we can easily change the number of coins, fruits and ghosts that will be created:
Following the constants we are going to create some variables so keep all the information about the game and also to start the initialization of Phaser:
We will need some images, sprites and sounds. You may use any assets you like, but we are also providing some examples so that you can easily start testing (they can also be downloaded here):
Images
Tileset containing almost every image needed to build the gameRestart button
Sounds and music
When the player gets a coinWhen the user collects a fruitWhen the user gets hurt by a ghostWhen the user kills a ghostBackground music always playing
Loading all the assets
So that we can use all those images, sprites and music, they have to be loaded previously. That is the purpose of the “preload()” function. We will also include here the initialization of the joystick so that we can use it in another section of this unit.
Proposed exercise: Game initialization
Create a specific folder for your game in your domain to put all the code and assets inside. After that, create an “index.html” inside that folder with the code below, and also create an “assets” folder to put all the images and sounds, which can be downloaded here. After uploading everything to your domain, test the game using the url of that folder in your browser, and you should get a full black screen. Click on it and the background music should start playing.
You can see the result here (you can also look at the source code by pressing Ctrl+U).
Download any other music file you like to use it as a background music and change the JavaScript code inside the “preload()” function to set the file name accordingly. Also in the “initSounds()” function you will find that you may change the volume of the music (volume:0.4). Try changing that value from 0.1 to 1 and you will notice that the volume of the music will also change.
You may find in the following sites some free images and sounds which you can download and use for your own game:
Once we have initialized Phaser and after loading all the assets, we can go ahead and display the maze on the screen.
Proposed exercise: Displaying the maze
Add the function “createWorld()” (provided below) to your code, and modify the calling function “create()” so that it is called when the game is started (also shown below). Finally test everything in your browser from your domain.
About the music, do not forget to click on the screen after loading the game (the music will not start playing unless the browser gets the focus). Also, you can see the result here (you can look at the source code by pressing Ctrl+U).
This is the new function you have to insert in your code to create the world:
function createWorld() {
map = this.make.tilemap({ key: "map" });
// Parameters are the name you gave the tileset in Tiled and then the key of the tileset image in
// Phaser's cache (i.e. the name you used in preload)
tileset = map.addTilesetImage("tileset", "tiles");
// Parameters: layer name (or index) from Tiled, tileset, x, y
belowLayer = map.createLayer("Below Player", tileset, 0, 0).setDepth(1);
worldLayer = map.createLayer("World", tileset, 0, 0).setDepth(2);
aboveLayer = map.createLayer("Above Player", tileset, 0, 0).setDepth(3);
worldLayer.setCollisionByProperty({ collides: true });
this.physics.world.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
// Find empty tiles where new zombies, coins or health can be created
emptyTiles = worldLayer.filterTiles(tile => (tile.index === -1));
this.scale.resize(map.widthInPixels, map.heightInPixels).refresh();
}
Do not forget to update the “create()” function to create the world. This is the only line you should add to that function:
function create() {
...
createWorld.call(this);
}
Proposed exercises: Modifying the maze
Download the tile map editor from this link and install it on your computer (if you don’t have installed it yet). Go to the assets folder and open the file “map.tmx” which contains the map of the example explained in this unit. Select the layer “World” (on the right corner, at the top) and change anything you like (by adding or removing some objects to or from the map). When you finish, export the map to JSON format using the option “File” -> “Export as…”, and overwrite the file “map.json”, located inside the “assets” folder. Finally check the results using your browser.
Do not forget to fully refresh the contents of the browser by pressing Ctrl+F5, since the map and some other contents are usually cached by the browser. Then you can check that the map has been updated with the changes you have made.
The animations (sprites)
We will use sprites to create the player and the animated objects. Let’s have a look at the the picture containing all the sprites:
Tileset containing all the sprites
We may appreciate that we have several groups of images. This way, we may easily create the animations:
The coin: Pictures from 0 to 7
The point: Picture 8
The ghosts:
Blue: Pictures from 17 to 20
Pink: Pictures from 21 to 24
Yellow: Pictures from 25 to 28
Green: Pictures from 34 to 37
Orange: Pictures from 38 to 41
Red: Pictures from 42 to 45
The player:
Left: Pictures from 51 to 58
Right: Pictures from 68 to 75
Down: Pictures from 85 to 92
Up: Pictures from 102 to 109
The fruits: Pictures 46, 47, 114, 143
Controlling the player’s animations
So that this game can be used in both desktop and mobile devices, we have to initialize both the cursor keys and the joystick. Once this is done, we must change the player’s velocity and the current animation when the user presses the keys or moves the joystick. In the next exercise we will first define the animations inside the “createAnimations()” function, and after that we will go ahead with the “createPlayer()” function, where we will display the player. We will finally include in the “update()” function some conditions to adjust the player’s speed and current animation.
Proposed exercise: Adding the player
Add the code below to your game and check both the player animations and movements. After that, look for another sprite and change the code accordingly to use your own sprite. Finally, change the values of the constant “speed” (at the top of your code) and check the results.
You can see the result here (you can look at the source code by pressing Ctrl+U). Also, as listed above, you may find many free assets in OpenGameArt and choose other sprites you like.
These are the new functions you have to insert in your code to create the animations and initialize everything related to the player:
function newAnimation(name, rate, sprites, context) {
context.anims.create({
key: name, frameRate: rate, repeat: -1,
frames: context.anims.generateFrameNumbers('sprites', sprites),
});
}
function createAnimations() {
// Player
newAnimation("left", 10, { start: 51, end: 58 }, this);
newAnimation("right", 10, { start: 68, end: 75 }, this);
newAnimation("down", 10, { start: 85, end: 92 }, this);
newAnimation("up", 10, { start: 102, end: 109 }, this);
// Point
newAnimation("point", 1, { start: 8, end: 8 }, this);
// Coin
newAnimation("spinning", 10, { start: 0, end: 7 }, this);
// Fruits
newAnimation("fruits", 2, { frames: [46, 47, 114, 143] }, this);
// Ghosts
newAnimation("blue", 5, { start: 17, end: 20 }, this);
newAnimation("pink", 5, { start: 21, end: 24 }, this);
newAnimation("yellow", 5, { start: 25, end: 28 }, this);
newAnimation("green", 5, { start: 34, end: 37 }, this);
newAnimation("orange", 5, { start: 38, end: 41 }, this);
newAnimation("red", 5, { start: 42, end: 45 }, this);
}
function createPlayer() {
// Object layers in Tiled let you embed extra info into a map - like a spawn point or custom
// collision shapes. In the tmx file, there's an object layer with a point named "Spawn Point"
const spawnPoint = map.findObject("Objects", obj => obj.name === "Spawn Point");
// Create a sprite with physics enabled via the physics system
player = this.physics.add.sprite(spawnPoint.x, spawnPoint.y, "sprites").setDepth(2);
// Watch the player and worldLayer for collisions, for the duration of the scene
this.physics.add.collider(player, worldLayer);
// Show points on empty tiles
emptyTiles.forEach(tile => {
let point = this.physics.add.sprite(tile.pixelX, tile.pixelY, 'sprites').setOrigin(0).anims.play('point', true);
this.physics.add.overlap(player, point, collectPoint, null, this);
});
cursors = this.input.keyboard.createCursorKeys();
// Show the joysticks only in mobile devices
if (!this.sys.game.device.os.desktop) {
joyStick = this.plugins.get('rexvirtualjoystickplugin').add(this, {
x: 380, y: 420, radius: 20,
base: this.add.circle(0, 0, 20, 0x888888).setAlpha(0.5).setDepth(4),
thumb: this.add.circle(0, 0, 20, 0xcccccc).setAlpha(0.5).setDepth(4)
}).on('update', update, this);
joyStick2 = this.plugins.get('rexvirtualjoystickplugin').add(this, {
x: 40, y: 420, radius: 20,
base: this.add.circle(0, 0, 20, 0x888888).setAlpha(0.5).setDepth(4),
thumb: this.add.circle(0, 0, 20, 0xcccccc).setAlpha(0.5).setDepth(4)
}).on('update', update, this);
}
}
function showScore() {
if (!scoreText) scoreText = this.add.text(map.widthInPixels/2, 4, '', { fontSize: (18) + 'px', fill: '#FFF' }).setOrigin(0.5, 0).setScrollFactor(0).setDepth(4);
scoreText.setText('Score:' + score + ' / Lives:' + lives);
}
function collectPoint(player, point) {
bell.play();
point.destroy();
score += 5;
showScore();
}
Also you have to insert some code inside the “update()” function to respond to the cursor keys and the joystick:
function update(time, delta) {
if (gameOver) return;
// Choose the right animation depending on the player's direction
if (player.x > prevX) player.anims.play('right', true);
else if (player.x < prevX) player.anims.play('left', true);
else if (player.y > prevY) player.anims.play('down', true);
else if (player.y < prevY) player.anims.play('up', true);
// If the player goes outside the map
if (player.x < 0) player.x = map.widthInPixels - 20;
else if (player.x > map.widthInPixels) player.x = 0;
let key = player.anims.currentAnim.key;
let blocked = player.body.blocked;
// Reset the velocity when the player touches a wall
if (key == 'right' && blocked.right || key == 'left' && blocked.left) vX = 0;
if (key == 'up' && blocked.up || key == 'down' && blocked.down) vY = 0;
// Horizontal movement
if (cursors.left.isDown || joyStick.left || joyStick2.left) vX = -speed;
else if (cursors.right.isDown || joyStick.right || joyStick2.right) vX = speed;
// Vertical movement
if (cursors.up.isDown || joyStick.up || joyStick2.up) vY = -speed;
else if (cursors.down.isDown || joyStick.down || joyStick2.down) vY = speed;
player.setVelocity(vX, vY);
if ((time - prevTime) > 100) {
prevX = player.x;
prevY = player.y;
prevTime = time;
}
}
And finally do not forget to update the “create()” function to create the animations, the player and the score. You just have to insert some new lines:
function create() {
...
createAnimations.call(this);
createPlayer.call(this);
showScore.call(this);
}
Coins, fruits, and ghosts
We will create many different objects at once using the images from the same tileset. We will just use a loop to print as many objects as we like.
Proposed exercises: Adding the coins
Add the code below to your file and check the results. After that, change the value of the constant “numCoins” (at the top of your code) to check how easily you can customize your game. Refresh the page several times and you will notice that the generated coins appear at random positions. Finally, look for another sprite sheet (it may be a coin or any other object you like) and change the code accordingly to use your own animated object.
You can see the result here (you can look at the source code by pressing Ctrl+U). Also, as listed above, you may find many free assets in OpenGameArt and choose the images you like.
function create() {
...
for (i = 0; i < numCoins; i++) setTimeout(() => createCoin.call(this), Phaser.Math.Between(0, 5000));
}
Proposed exercises: Adding the fruits
Add the code below to your game and check the results. After that, change the value of the constants “numFruits” (at the top of your code) to check how easily you can customize your game. Finally, look for another sprite and change the code accordingly to use your own object to provide the player with extra lives and points.
You may click here to see how the game looks like. As listed above, you may find many free assets in OpenGameArt.
function createFruits() {
let fruits = newObject('fruits', this);
fruits.body.setAllowGravity(false);
this.physics.add.overlap(player, fruits, collectFruits, null, this);
}
function collectFruits(player, fruits) {
bell2.play();
fruits.destroy();
createFruits.call(this);
score += 15;
showScore();
}
function create() {
...
for (i = 0; i < numFruits; i++) setTimeout(() => createFruits.call(this), Phaser.Math.Between(0, 5000));
}
Proposed exercises: Adding the ghosts
Add the code below to your game and check the results. After that, change the value of the constants “numGhosts” (at the top of your code) to check how easily you can customize your game. Finally, look for another sprite and change the code accordingly to display your own ghosts.
You may click here to see how the game looks like. As listed above, you may find many free assets in OpenGameArt.
function newGhost(animation, context) {
const spawnPoint = map.findObject("Objects", obj => obj.name === "Spawn Point 2");
return context.physics.add.sprite(spawnPoint.x, spawnPoint.y, 'sprites').setOrigin(0).anims.play(animation, true).setDepth(2);
}
function createGhost() {
const colors = ['blue', 'pink', 'yellow', 'green', 'orange', 'red'];
let ghost = newGhost(colors[Phaser.Math.Between(0, colors.length - 1)], this).setCollideWorldBounds(true).setBounce(1);
ghost.setVelocity(Phaser.Math.Between(speed / 3, speed / 2), Phaser.Math.Between(-speed / 2, -speed / 3)).body.setAllowGravity(false);
this.physics.add.collider(ghost, worldLayer);
this.physics.add.overlap(player, ghost, hitGhost, null, this);
}
function hitGhost(player, ghost) {
// If the player is already hurt, it cannot be hurt again for a while
if (player.tintTopLeft == 0xFF00FF) return;
if (timeout) {
score += 15;
dead.play();
}
else {
lives--;
hurt.play();
protect(0xFF00FF);
}
showScore();
if (lives == 0) {
this.physics.pause();
gameOver = true;
this.add.image(210, 230, 'restart').setScale(2).setScrollFactor(0).setDepth(4)
.setInteractive().on('pointerdown', () => location.reload());
}
else {
ghost.destroy();
createGhost.call(this);
}
}
function create() {
...
for (i = 0; i < numGhosts; i++) setTimeout(() => createGhost.call(this), Phaser.Math.Between(0, 15000));
}
Enjoy the game!
You may enjoy playing this wonderful game online here.
In this unit we are going to use Phaser together with a tilemap editor to build our world. You may find more information at the official page of the Tiled editor and you may also have a look at some interesting demonstrations at the Phaser’s examples page, and also performing some search on the latest examples.
HTML and CSS code
The first thing we should do is linking the Phaser library. We will use the last version at the time this unit has been written:
After that we are going to insert some CSS code to remove any margin and padding, and also to remove the scrollbars, so that all the window size is used when the game is started:
We are going to define some constants at the beginning of the code so that we can easily change the game appearance, difficulty, and some other parameters:
Following the constants we are going to create some variables so keep all the information about the game and also to start the initialization of Phaser:
We will need some images, sprites and sounds. You may use any assets you like, but we are also providing some examples so that you can easily start testing (they can also be downloaded here):
Images
Tileset to be used to build the mapRestart button
Sprites (player and animated objects)
Player’s sprite sheetEnemy’s sprite sheet Sprite sheet to animate the coinsSprite sheet to animate the health objectsSprite sheet to animate the power object
Sounds and music
When the player gets a coinWhen the user collects a health objectWhen the user gets hurt by a zombieBackground music always playing
Loading all the assets
So that we can use all those images, sprites and music, they have to be loaded previously. That is the purpose of the “preload()” function. We will also include here the initialization of the joystick so that we can use it in another section of this unit.
Proposed exercise: Game initialization
Create a specific folder for your game in your domain to put all the code and assets inside. After that, create an “index.html” inside that folder with the code below, and also create an “assets” folder to put all the images and sounds, which can be downloaded here. After uploading everything to your domain, test the game using the url of that folder in your browser, and you should get a full black screen. Click on it and the background music should start playing.
You can see the result here (you can also look at the source code by pressing Ctrl+U).
Download any other music file you like to use it as a background music and change the JavaScript code inside the “preload()” function to set the file name accordingly. Also in the “initSounds()” function you will find that you may change the volume of the music (volume:0.4). Try changing that value from 0.1 to 1 and you will notice that the volume of the music will also change.
You may find in the following sites some free images and sounds which you can download and use for your own game:
Once we have initialized Phaser and after loading all the assets, we can go ahead and display the map on the screen.
Proposed exercise: Displaying the map
Add the function “createWorld()” (provided below) to your code, and modify the calling function “create()” so that it is called when the game is started (also shown below). Finally test everything in your browser from your domain.
About the music, do not forget to click on the screen after loading the game (the music will not start playing unless the browser gets the focus). Also, you can see the result here (you can look at the source code by pressing Ctrl+U).
This is the new function you have to insert in your code to create the world:
function createWorld() {
map = this.make.tilemap({ key: "map" });
scale *= Math.max(screenWidth/map.widthInPixels, screenHeight/map.heightInPixels);
worldWidth = map.widthInPixels * scale;
worldHeight = map.heightInPixels * scale;
speed *= scale;
joystickSize *= scale;
// Parameters are the name you gave the tileset in Tiled and then the key of the tileset image in
// Phaser's cache (i.e. the name you used in preload)
tileset = map.addTilesetImage("tileset", "tiles");
// Parameters: layer name (or index) from Tiled, tileset, x, y
belowLayer = map.createLayer("Below Player", tileset, 0, 0).setScale(scale).setDepth(1);
worldLayer = map.createLayer("World", tileset, 0, 0).setScale(scale).setDepth(2);
aboveLayer = map.createLayer("Above Player", tileset, 0, 0).setScale(scale).setDepth(3);
worldLayer.setCollisionByProperty({ collides: true });
// Find empty tiles where new zombies, coins or health can be created
emptyTiles = worldLayer.filterTiles(tile => (tile.index === -1 || !tile.collides));
}
Do not forget to update the “create()” function to create the world. This is the only line you should add to that function:
function create() {
...
createWorld.call(this);
}
Proposed exercise: Scaling the map
Change the value of the “scale” variable (at the top of your code) and refresh the map in your browser to check that it is scaled accordingly. You can set values such as “scale = 0.5” and you will notice that the map just fits half the screen. Set also higher values and check the results again.
The animations (sprites)
We will use sprites to create the player and the animated objects. For example, let’s have a look at the player:
Player’s sprite sheet
We may appreciate that we have 36 pictures divided into 4 different groups. This way, we may easily create all four animations:
Up: Pictures from 0 to 8
Left: Pictures from 9 to 17
Down: Pictures from 18 to 26
Right: Pictures from 27 to 35
Controlling the player’s animations
So that this game can be used in both desktop and mobile devices, we have to initialize both the cursor keys and the joystick. Once this is done, we must change the player’s velocity and the current animation when the user presses the keys or moves the joystick. In the next exercise we will first define the animations inside the “createAnimations()” function, and after that we will go ahead with the “createPlayer()” function, where we will display the player. We will finally include in the “update()” function some conditions to adjust the player’s speed and current animation.
Proposed exercise: Adding the player
Add the code below to your game and check both the player animations and movements. After that, look for another sprite and change the code accordingly to use your own sprite. Finally, change the values of the constant “speed” (at the top of your code) and check the results. You will also notice that the camera will follow you on each player’s movement.
You can see the result here (you can look at the source code by pressing Ctrl+U). Also, as listed above, you may find many free assets in OpenGameArt and choose other sprites you like.
These are the new functions you have to insert in your code to create the animations and initialize everything related to the player:
function createAnimations() {
const anims = this.anims;
// Player
anims.create({
key: "left", frameRate: 10, repeat: -1,
frames: this.anims.generateFrameNumbers('player', { start: 9, end: 17 }),
});
anims.create({
key: "right", frameRate: 10, repeat: -1,
frames: this.anims.generateFrameNumbers('player', { start: 27, end: 35 }),
});
anims.create({
key: "up", frameRate: 10, repeat: -1,
frames: this.anims.generateFrameNumbers('player', { start: 0, end: 8 }),
});
anims.create({
key: "down", frameRate: 10, repeat: -1,
frames: this.anims.generateFrameNumbers('player', { start: 18, end: 26 }),
});
anims.create({
key: "waiting", frameRate: 3, repeat: -1,
frames: this.anims.generateFrameNumbers('player', { start: 19, end: 21 }),
});
// Coin
anims.create({
key: "flying", frameRate: 10, repeat: -1,
frames: this.anims.generateFrameNumbers('coin', { start: 0, end: 7 }),
});
// Health
anims.create({
key: "floating", frameRate: 5, repeat: -1,
frames: this.anims.generateFrameNumbers('health', { start: 0, end: 3 }),
});
// Power
anims.create({
key: "growing", frameRate: 5, repeat: -1,
frames: this.anims.generateFrameNumbers('power', { start: 0, end: 3 }),
});
// Zombie
anims.create({
key: "moving", frameRate: 5, repeat: -1,
frames: this.anims.generateFrameNumbers('zombie', { start: 0, end: 11 }),
});
}
function createPlayer() {
// Object layers in Tiled let you embed extra info into a map - like a spawn point or custom
// collision shapes. In the tmx file, there's an object layer with a point named "Spawn Point"
const spawnPoint = map.findObject("Objects", obj => obj.name === "Spawn Point");
// Create a sprite with physics enabled via the physics system. The image used for the sprite has
// a bit of whitespace, so I'm using setSize & setOffset to control the size of the player's body
player = this.physics.add
.sprite(spawnPoint.x * scale, spawnPoint.y * scale, "player")
.setSize(16, 32).setOffset(24, 32).setScale(scale)
.setCollideWorldBounds(true).setDepth(2);
// Watch the player and worldLayer for collisions, for the duration of the scene
this.physics.add.collider(player, worldLayer);
const camera = this.cameras.main;
const world = this.physics.world;
camera.startFollow(player);
camera.setBounds(0, 0, worldWidth, worldHeight);
world.setBounds(0, 0, worldWidth, worldHeight);
cursors = this.input.keyboard.createCursorKeys();
// Do not show the joysticks on desktop devices
if (!this.sys.game.device.os.desktop) {
joyStick = this.plugins.get('rexvirtualjoystickplugin').add(this, {
x: screenWidth - joystickSize * 2,
y: screenHeight - joystickSize * 2,
radius: joystickSize,
base: this.add.circle(0, 0, joystickSize, 0x888888).setAlpha(0.5).setDepth(4),
thumb: this.add.circle(0, 0, joystickSize, 0xcccccc).setAlpha(0.5).setDepth(4)
}).on('update', update, this);
joyStick2 = this.plugins.get('rexvirtualjoystickplugin').add(this, {
x: joystickSize * 2,
y: screenHeight - joystickSize * 2,
radius: joystickSize,
base: this.add.circle(0, 0, joystickSize, 0x888888).setAlpha(0.5).setDepth(4),
thumb: this.add.circle(0, 0, joystickSize, 0xcccccc).setAlpha(0.5).setDepth(4)
}).on('update', update, this);
}
}
Also you have to insert some code inside the “update()” function to respond to the cursor keys and the joystick:
function update(time, delta) {
if (gameOver) return;
let vX = 0, vY = 0;
// Horizontal movement
if (cursors.left.isDown || joyStick.left || joyStick2.left) vX = -speed;
else if (cursors.right.isDown || joyStick.right || joyStick2.right) vX = speed;
// Vertical movement
if (cursors.up.isDown || joyStick.up || joyStick2.up) vY = -speed;
else if (cursors.down.isDown || joyStick.down || joyStick2.down) vY = speed;
// Set and normalize the velocity so that player can't move faster along a diagonal
player.setVelocity(vX, vY).body.velocity.normalize().scale(speed);
// Choose the right player's animation
if (vX < 0) player.anims.play('left', true);
else if (vX > 0) player.anims.play('right', true);
else if (vY < 0) player.anims.play('up', true);
else if (vY > 0) player.anims.play('down', true);
else player.anims.play('waiting', true);
}
And finally do not forget to update the “create()” function to create the animations and the player. You just have to insert a couple of new lines:
function create() {
...
createAnimations.call(this);
createPlayer.call(this);
}
Proposed exercises: Modifying the map
Download the tile map editor from this link and install it on your computer. Go to the assets folder and open the file “town.tmx” which contains the map of the example explained in this unit. Select the layer “World” (on the right corner, at the top) and change anything you like (by adding or removing some objects to or from the map). When you finish, export the map to JSON format using the option “File” -> “Export as…”, and overwrite the file “town.json”, located inside the “assets” folder. Finally check the results using your browser and move the player to go to each part of the map. You will notice again that the camera will follow you on each movement.
Do not forget to fully refresh the contents of the browser by pressing Ctrl+F5, since the map and some other contents are usually cached by the browser. Also move the player and check that the map has been updated with the changes you have made.
Score, coins, power, health, and enemies
We will create many different objects at once using the same images. We will just change the size of the objects by scaling them, and we will use a loop to print as many objects as we like.
Proposed exercise: Adding the score
Using the global variables, we may easily show both the score and number of remaining lives. Add the code below to your game and check the results. After that, change the value of the variable “lives” (at the top of your code) to check how easily you can customize that text.
You can see the result here (you can look at the source code by pressing Ctrl+U).
Add the code below to your file and check the results. After that, change the value of the constant “numCoins” (at the top of your code) to check how easily you can customize your game. Refresh the page several times and you will notice that the generated coins have random sizes and they appear at random positions. Finally, look for another sprite sheet (it may be a coin or any other object you like) and change the code accordingly to use your own animated object.
You can see the result here (you can look at the source code by pressing Ctrl+U). Also, as listed above, you may find many free assets in OpenGameArt and choose the images you like.
function create() {
...
for (i = 0; i < numCoins; i++) setTimeout(() => createCoin.call(this), Phaser.Math.Between(0, 5000));
}
Proposed exercises: Adding the health
Add the code below to your game and check the results. After that, change the value of the constants “numHealth” (at the top of your code) to check how easily you can customize your game. Finally, look for another sprite and change the code accordingly to use your own object to provide the player with extra lives.
You may click here to see how the game looks like. As listed above, you may find many free assets in OpenGameArt.
function createHealth() {
let health = newObject('health', 'floating', this).setSize(16, 32).setScale(1.2 * scale);
health.body.setAllowGravity(false);
this.physics.add.overlap(player, health, collectHealth, null, this);
}
function collectHealth(player, health) {
bell2.play();
health.destroy();
createHealth.call(this);
lives++;
showScore();
}
function create() {
...
for (i = 0; i < numHealth; i++) setTimeout(() => createHealth.call(this), Phaser.Math.Between(0, 5000));
}
Proposed exercises: Adding the power
Add the code below to your game and check the results. After that, change the value of the constants “numPower” (at the top of your code) to check how easily you can customize your game. Finally, look for another sprite and change the code accordingly to use your own power object.
You may click here to see how the game looks like. As listed above, you may find many free assets in OpenGameArt.
function createPower() {
let power = newObject('power', 'growing', this).setSize(32, 64).setScale(scale/2);
power.body.setAllowGravity(false);
this.physics.add.overlap(player, power, collectPower, null, this);
}
function protect(color) {
player.setTint(color);
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => { timeout = false; player.clearTint() }, 5000);
}
function collectPower(player, power) {
bell2.play();
power.destroy();
createPower.call(this);
protect(0xFFFF00);
}
function create() {
...
for (i = 0; i < numPower; i++) setTimeout(() => createPower.call(this), Phaser.Math.Between(0, 5000));
}
Proposed exercises: Adding the enemies
Add the code below to your game and check the results. After that, change the value of the constants “numZombies” (at the top of your code) to check how easily you can customize your game. Finally, look for another sprite and change the code accordingly to display your own enemies.
You may click here to see how the game looks like. As listed above, you may find many free assets in OpenGameArt.
function createZombie() {
const v = Phaser.Math.Between(-speed / 4, -speed / 3);
let zombie = newObject('zombie', 'moving', this).setSize(16, 32).setOffset(16, 32).setScale(1.2 * scale).setBounce(1).setVelocity(0, v);
zombie.body.setAllowGravity(false).setCollideWorldBounds(true);
this.physics.add.collider(zombie, worldLayer);
this.physics.add.overlap(player, zombie, hitZombie, null, this);
}
function hitZombie(player, zombie) {
// If the player is already hurt, it cannot be hurt again for a while
if (player.tintTopLeft == 0xFF0000) return;
if (timeout) {
score += 15;
showScore();
}
else {
protect(0xFF0000);
lives--;
}
dead.play();
showScore();
if (lives == 0) {
this.physics.pause();
gameOver = true;
this.add.image(screenWidth / 2, screenHeight / 2, 'restart')
.setScale(4).setScrollFactor(0).setDepth(4)
.setInteractive().on('pointerdown', () => location.reload());
}
else {
zombie.destroy();
createZombie.call(this);
}
}
function create() {
...
for (i = 0; i < numZombies; i++) setTimeout(() => createZombie.call(this), Phaser.Math.Between(0, 5000));
}
Enjoy the game!
You may enjoy playing this wonderful game online here.