Welcome to Part Two of this four-part series on building a mobile game using open source technologies. We'll be using Phaser, along with Ionic, Capacitor, and Vue.
βAre you interested in a video walkthrough version of this blog post? Let me know in the comments! If there's enough interest I can put one together.
The source code for this tutorial is here, and I'll reference specific commits throughout so you can see the specific changes for each section.
This post is heavily influenced by the tutorial from the official Phaser docs (we'll even use the assets they provide as well). I recommend going through the tutorial if you want to learn more about Phaser concepts. We'll keep it pretty high level here for brevity.
In this post, we'll actually walk through making the Phaser game! It's a big section and there are a lot of new concepts to learn, so let's jump right in!
Table of Contents
- Creating Scenes
- Adding Game Objects
- Responding to Player Input
- Dynamically Generating Game Objects
- Adding Collision & Overlap Handlers
- What's Next
Creating Scenes
We talked about scenes briefly last week. Scenes are a core concept in Phaser and other game dev frameworks. You can think of Scenes like Views in a web app, but with some additional flexibility. Your player will navigate between Scenes, and sometimes multiple Scenes can run simultaneously.
Right now we have a single MainScene in our app.
In your game
directory, create a PlayScene.js
and ScoreScene.js
file. We'll work with the PlayScene.js
file first, as this will contain the majority of our game.
Add the following code to your Play scene:
// src/game/PlayScene.js
import { Scene } from "phaser";
export class PlayScene extends Scene {
constructor () {
super({ key: 'PlayScene' })
}
create () {
this.add.text(100, 100, "PlayScene", {
font: "24px Courier",
fill: "#ffffff",
});
}
}
Then, in your game.js
file, remove the MainScene
class we defined in the last blog post. We'll need to update our import statement and config to include the new PlayScene
instead.
// src/game/game.js
import { Game, AUTO, Scale } from "phaser";
import { PlayScene } from "./PlayScene.js";
export function launch() {
return new Game({
type: AUTO,
scale: {
mode: Scale.RESIZE,
width: window.innerWidth * window.devicePixelRatio,
autoCenter: Scale.CENTER_BOTH,
height: window.innerHeight * window.devicePixelRatio,
},
parent: "game",
backgroundColor: "#201726",
physics: {
default: "arcade",
},
scene: PlayScene,
});
}
It's not much, but now we have separate files for our scenes, which will make it easier to work with them.
Here is the git commit with the changes for this section.
Adding Game Objects
When working with Phaser, almost everything we interact with is a Game Object. Game Objects can:
- Have Physics applied
- Be images, text, sprites, etc.
- Be Static or Dynamic
- Be categorized into Groups
- And much, much more.
For our game, we'll be working with the following objects:
- A player sprite that moves left and right with animation
- A platform static object that our player walks on
- Left and right arrows for input to move our player
- A star object that falls from the top of the screen and increases the score on collision with the player
- A bomb object that falls from the top of the screen and ends the game on collision with the player
Within the public
directory in the root of your project, create a new assets
folder. Save the above linked images in this folder with the following file names:
player.png
platform.png
leftarrow.png
rightarrow.png
star.png
bomb.png
Detour: Adjusting Game Window
Before we move onto adding Game Objects, we need to make a small fix to our app that I discovered while writing. Right now, we have a collapsible header on iPhone. This could affect our game window size. Update the Play, About, and Scores pages in your /src/views
directory to remove :fullscreen=true
from the <ion-content>
component, and remove the entire <ion-header>
component and children that is inside <ion-content>
. Your updated Page template should look like this:
// src/views/PlayPage.vue
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Play</ion-title>
</ion-toolbar>
</ion-header>
<ion-content >
<PhaserContainer />
</ion-content>
</ion-page>
</template>
Now we can move on!
Here is the git commit with the changes for this section.
Preloading Assets
Right now, inside our PlayScene
class, we have a constructor
and a create ()
method. There are two other lifecycle methods provided by Phaser: preload ()
and update ()
.
The preload ()
method is used to preload assets that will be used in our game.
Update your PlayScene.js
file to preload the assets we just saved. Go ahead and also remove the text in the create ()
method.
// src/game/PlayScene.js
import { Scene } from "phaser";
export class PlayScene extends Scene {
constructor () {
super({ key: 'PlayScene' })
}
preload ()
{
this.load.image('star', 'assets/star.png');
this.load.image('bomb', 'assets/bomb.png');
this.load.image('platform', 'assets/platform.png');
this.load.image('leftArrow', 'assets/leftarrow.png');
this.load.image('rightArrow', 'assets/rightarrow.png');
this.load.spritesheet('player',
'assets/player.png',
{ frameWidth: 32, frameHeight: 48 }
);
}
create () {
}
}
The star, bomb, platform, and arrow images are added with this.load.image()
, where we pass a unique key and the path for the image file.
The player is loaded using what's called a sprite sheet. A sprite sheet contains multiple poses of our player in the same image. This is helpful for animating our player. We use this.load.spritesheet()
, again passing a key and path, but also setting the frameWidth
and frameHeight
of each individual pose (or "frame") in our image.
Displaying Images
Our asset are preloaded, but nothing is showing on the screen! We need to create game objects using our image assets.
Phaser uses pixels for all positioning and scaling. Many examples simply provide hard-coded values, which can work fine on browsers in a desktop device, but not for mobile.
Because of this, we need to do some math to make our displayed images responsive to the user's device screen.
NOTE I'd recommend playing around with these values based on the device screen. In a more complex example here, I am scaling everything and adding logic checks rather than hard-coding values. However, for simplicity, I'm hard-coding for an iPhone 12 Pro in this tutorial.
Phaser provides this.scale.width
and this.scale.height
that returns the current screen width and height values. We can use these to calculate the center of our screen and where we should place objects.
If we think about our game layout, we'll need to calculate the height of a game play area, as well as the height of a controls area. This will help us determine where on the Y axis we need to place our platform, player, and arrow controls.
NOTE: We need to calculate the game size within our
create ()
method and not in ourgameState
because the game needs to be initiated first to access the screen size values.
Add the following code to your create ()
method.
//game/PlayScene.js
...
create () {
// sets game values based on screen size
this.screenWidth = this.scale.width;
this.screenHeight = this.scale.height;
this.screenCenterX = this.screenWidth / 2;
this.controlsAreaHeight = this.screenHeight * 0.2;
this.gameAreaHeight = this.screenHeight - this.controlsAreaHeight;
// adds the player, platform, and controls
this.platform = this.physics.add.staticImage(0, this.gameAreaHeight, 'platform').setOrigin(0, 0).refreshBody();
this.player = this.physics.add.sprite(this.screenCenterX, this.gameAreaHeight - 24, 'player');
this.leftArrow = this.add.image(this.screenWidth * 0.1, this.gameAreaHeight + 40, 'leftArrow').setOrigin(0, 0).setInteractive()
this.rightArrow = this.add.image(this.screenWidth * 0.7, this.gameAreaHeight + 40, 'rightArrow').setOrigin(0, 0).setInteractive()
}
In the first section, we've calculated the center of the screen. We've also set a controlsAreaHeight
to take up about 20% of the bottom of the screen, and the gameAreaHeight
to be the difference.
To create a game object, we'll use the relevant add
method, passing an X coordinate, Y coordinate, and image key.
Let's break these down individually.
this.platform = this.physics.add.staticImage(0, this.gameAreaHeight, 'platform').setOrigin(0, 0).refreshBody();
We've added the platform as a static image, which gives it physics properties so other game objects can interact with the platform. One thing to note here is setOrigin()
. By default Phaser positions images based on the center of the image. By changing the origin to (0, 0)
, I am telling Phaser to position starting from the bottom left corner instead. Whenever we change the position or scale of a static body, we need to tell Phaser to adjust for this change. That is what refreshBody()
is doing here.
this.player = this.physics.add.sprite(this.screenCenterX, this.gameAreaHeight - 24, 'player');
For the player, I'm adding a sprite. For the Y coordinate, I'm doing some math and placing the player at the gameAreaHeight
, but subtracting half the player height. This is because, again, Phaser positions from the center of the image. This results in the player standing nicely on top of the platform.
If I wanted to, I could use setOrigin()
to position from the bottom instead, but I wanted to demonstrate how you would position an item by default in Phaser as well.
this.leftArrow = this.add.image(this.screenWidth * 0.1, this.gameAreaHeight + 40, 'leftArrow').setOrigin(0, 0).setInteractive()
this.rightArrow = this.add.image(this.screenWidth * 0.7, this.gameAreaHeight + 40, 'rightArrow').setOrigin(0, 0).setInteractive()
For the arrows, I'm sticking with setOrigin(0, 0)
because it's easier for me to position based on the lower left corner with the math I'm doing. I'm positioning the arrows 40 pixels (half the size of the arrows) below the gameAreaHeight
, the left arrow 10% of the way from the left and the right arrow 70% of the way from the left of the screen edge.
I'm using setInteractive()
so that we can assign touch/click handlers to the arrows.
Animating Game Objects
If you save what we have so far, you see our player standing on the platform and facing the left. This is because Phaser is using the first frame of our spritesheet by default.
Let's set up some animations so when our character moves, we can leverage all the poses in the spritesheet. This code comes mostly from the official Phaser tutorial, so I won't dig into too much. I did add a logic check so it doesn't create a new animation if one already exists when the game restarts.
But you can see we are defining an animation key, providing an image key, stating what frames to loop through, and establishing a loop for each animation. The "turn" animation is the player simply facing forward.
//game/PlayScene.js
create () {
...
// adds animations for player
if (!this.anims.exists('left')) {
this.anims.create({
key: "left",
frames: this.anims.generateFrameNumbers('player', { start: 0, end: 3 }),
frameRate: 10,
repeat: -1,
});
}
if (!this.anims.exists('turn')) {
this.anims.create({
key: "turn",
frames: [{ key: 'player', frame: 4 }],
});
}
if (!this.anims.exists('right')) {
this.anims.create({
key: "right",
frames: this.anims.generateFrameNumbers('player', { start: 5, end: 8 }),
frameRate: 10,
repeat: -1,
});
}
}
Our player is still facing left because we'll leverage these animations once our player starts moving in the next section.
Here is the git commit with the changes for this section.
Responding to Player Input
In order for our player to animate, he needs to move! Let's add some event handlers for our arrow buttons next.
First, we need to make sure our player is ready for the physics of moving and interacting with our game world.
Setting Player Physics
Add the following code inside your create ()
method, underneath the animations we just added.
// src/game/PlayScene.js
// sets player physics
this.player.body.setGravityY(300);
this.player.setCollideWorldBounds(true);
// adds collider between player and platforms
this.physics.add.collider(this.player, this.platform);
For gravity, we could set a default game-level gravity. However, we want our player to move differently than our stars and bombs, so we're setting it on the object directly instead. setColliderWorldBounds()
to true
means our player cannot go off screen, and adding a collider between the player and platform means the player will stay on top of the platform as he moves.
Adding Event Handlers
Now we'll add event handlers for pointerdown
and pointerup
events. These will translate to touch events once we're on a mobile device.
Still in our create ()
method, add the following:
// src/game/PlayScene.js
// event handlers for arrow input
this.moveLeft = false;
this.moveRight = false;
this.leftArrow.on('pointerdown', () => {
this.moveLeft = true;
});
this.leftArrow.on('pointerup', () => {
this.moveLeft = false;
});
this.rightArrow.on('pointerdown', () => {
this.moveRight = true;
});
this.rightArrow.on('pointerup', () => {
this.moveRight = false;
});
We are using two variables, moveLeft
and moveRight
, to track whether our player is in motion based on what arrows are being pressed.
However, we are not actually telling the player to move yet. Where do we do that? In our update ()
method.
Handling movement in update method
So far we have primarily been working in our create ()
method, which is executed when the game is initialized.
In comparison, the update ()
method runs every frame of the game. This is where we can control actions that need to update once the game has already started.
Inside our update ()
method, add the following:
// src/game/PlayScene.js
update () {
if (this.moveLeft && !this.moveRight) {
this.player.setVelocityX(0 - 200);
this.player.anims.play('left', true);
}
else if (this.moveRight && !this.moveLeft) {
this.player.setVelocityX(200);
this.player.anims.play('right', true);
}
else {
this.player.setVelocityX(0);
this.player.anims.play('turn');
}
}
Here, when moveLeft
is set to true by the pointerdown
event handler, we set the player velocity to move left, and play the left
animation. We handle the moveRight
boolean change the same way.
By default, our player is not moving in either direction, with the turn
animation playing, which results in the player facing forward.
If you save now, you can try it out for yourself!
Here is the git commit with the changes for this section.
Dynamically Generating Game Objects
Our player can now move in the game world, but it's pretty boring. Let's give him something to do by generating stars for him to collect and bombs for him to avoid.
Add the following code to the end of your create ()
method:
// src/game/PlayScene.js
...
create () {
...
// Adds generated stars
this.stars = this.physics.add.group({
gravityY: 300,
});
const createStar = () => {
const x = Math.random() * this.screenWidth;
const star = this.stars.create(x, 0, 'star');
}
const createStarLoop = this.time.addEvent({
// random number between 1 and 1.2 seconds
delay: Math.floor(Math.random() * (1200 - 1000 + 1)) + 1000,
callback: createStar,
callbackScope: this,
loop: true,
});
}
Let's break down what's happening in each block.
First, we are creating a physics group called stars
. This is helpful when we have a category of game objects and want the same physics applied to each individual game object in that group. In this case, we are applying a gravityY
of 300.
Next, we are writing a function called createStar
that creates a new star
object within our stars
group, placing it at a random X coordinate and the 0 Y coordinate (top of the screen) using the key for our star image.
Finally, we are using this.time.addEvent
provided by Phaser to create a loop. This Phaser method works like setTimeout
, in that you provide a delay in MS and a callback function. We are also referencing our this
object for the callbackScope
and setting loop
to true so it repeats.
Once you save and refresh, you'll see stars falling from the sky at random X positions and at random intervals between 1 and 1.2 seconds.
We'll repeat the process for our bombs. However, we want our bombs to drop much faster but also appear less frequently.
Add the following code to create ()
:
// src/game/PlayScene.js
...
create () {
...
// Adds generated bombs
this.bombs = this.physics.add.group({
gravityY: 900,
});
const createBomb = () => {
const x = Math.random() * this.screenWidth;
const bomb = this.bombs.create(x, 0, 'bomb');
bomb.setScale(2).refreshBody();
}
const createBombLoop = this.time.addEvent({
// random number between 4.5 and 5 seconds
delay: Math.floor(Math.random() * (5000 - 4500 + 1)) + 4500,
callback: createBomb,
callbackScope: this,
loop: true,
});
Now we have bombs that drop every 4.5-5 seconds at a high speed. I also scaled up the bombs with setScale(2).refreshBody()
to double the original image size so they are easier to see as they fall.
You should now see stars and bombs falling down on your player!
Here is the git commit with the changes for this section.
Adding Collision & Overlap Handlers
We are almost done! All we have to do now is handle what happens when our player interacts with the stars and bombs.
Adding Colliders
You may remember that we added a collider earlier between the player and platform.
this.physics.add.collider(this.player, this.platform);
When we add a collider in Phaser without a callback, the default behavior is for the two objects to simply block or push against each other upon collision, without passing through each other.
We can set a collider with a callback function to complete additional steps when two items collide.
For example, our stars and bombs should not go past the platform. Let's add a collider for these and destroy the star or bomb when it collides with the platform.
// src/game/PlayScene.js
...
create () {
...
// Adds colliders between stars and bombs with platform
this.physics.add.collider(this.stars, this.platform, function(object1, object2) {
const star = (object1.key === 'star') ? object1 : object2;
star.destroy();
});
this.physics.add.collider(this.bombs, this.platform, function(object1, object2) {
const bomb = (object1.key === 'bomb') ? object1 : object2;
bomb.destroy();
});
This code will trigger the callback function whenever the two objects collide, determine which object is the object we went to destroy, then call the Phaser-provided destroy()
method on that object. Our stars and bombs should now disappear when they collide with the platform.
Adding Overlaps
Finally, we need to add overlap handlers that:
- increase the game score when our player overlaps with a star
- ends the game when the player overlaps with a bomb
An overlap is different than a collider in that it only checks if two objects overlap, rather than preventing them from colliding. We'll use overlap for the interactions between our player with stars and bombs.
Add the following, again to your create ()
method:
// src/game/PlayScene.js
...
create () {
...
// Adds overlap between player and stars
this.score = 0;
this.scoreText = this.add.text(this.screenCenterX, this.gameAreaHeight + 16, 'Score: 0', { fontSize: '16px', fill: '#000' }).setOrigin(0.5, 0.5);
this.physics.add.overlap(this.player, this.stars, function(object1, object2) {
const star = (object1.key === 'player') ? object1 : object2;
star.destroy();
this.score += 10;
this.scoreText.setText('Score: ' + this.score);
}, null, this);
In the first block, we are creating a starting score of 0 and displaying that text on the screen over the platform. This should look familiar to how we created other game objects, passing an X and Y value and then the text to this.add.text()
. We can also set CSS values in an object passed to the fourth parameter.
Then, we are creating an overlap between the player and the stars group with a callback function that does four things:
- Checks which overlap object is the star
- Destroys the star
- Increases the score by 10 points
- Resets the score text with
this.scoreText.setText()
We need to pass the additional null
and this
parameters to this.physics.add.overlap()
so that we have access to our this
object inside the callback function.
We can repeat this for our bombs.
// src/game/PlayScene.js
...
create () {
...
// Adds overlap between player and bombs
this.physics.add.overlap(this.player, this.bombs, function(object1, object2) {
const bomb = (object1.key === 'player') ? object1 : object2;
bomb.destroy();
createStarLoop.destroy();
createBombLoop.destroy();
this.physics.pause();
this.gameOverText = this.add.text(this.screenCenterX, this.screenHeight / 2, 'Game Over', { fontSize: '32px', fill: 'red' }).setOrigin(0.5, 0.5);
this.input.on('pointerup', () => {
this.score = 0;
this.scene.restart();
})
}, null, this);
This is similar to our star overlap in that we are checking which object is the bomb, then destroying it. However, after that, we have functionality that handles the end of the game.
createStarLoop.destroy();
createBombLoop.destroy();
this.physics.pause();
These lines essentially stop our game world by destroy the loops that create new stars and bombs, as well as pausing all the physics taking place in the game.
Then, we add "Game Over" text to the middle of the screen.
Finally, we are adding a pointerup
event handler that lets the user restart the game. This resets the score to 0 and restarts the game on click/tap. It's important that this event handler is inside your overlap callback so it only fires after the player interacts with a bomb.
Congratulations, you now have a game!
Here is the git commit with the changes for this section.
What's Next
Next week, we'll create our Score scene, talk about transitioning scenes, as well as how to interact between our Phaser game and the Ionic Vue app by exporting scores.
Stay tuned!
Top comments (11)
Impressive tutorial and write up! ππ
A video would be great! π
Same can be done with react native right? And with ionic or Cordova directly. What is the benefit of using Vue in this case at all?
Great tutorial though, although nowadays making games with JS is just not efficient enough with popularization of platforms such as Godot. Back in the days I'd kill to find an article like this, pity couldn't find much.
Iβm not sure if you can use Phaser with React Native. RN has its own ecosystem of compatible libraries, where with Ionic you can use any of the same libraries youβd use for a web app. On initial search it looks like there is a Phaser Expo library but I havenβt used it.
Ionic Framework is the UI toolkit for building the mobile app that houses the Phaser game and isnβt explicitly required. You could code your game/app with any framework or just JS and use Capacitor (built by Ionic) or Cordova to compile to native mobile.
I prefer to use a JS framework with Ionic components (you can use Angular, React, Vue, or vanilla JS). I like Vue because it has a great DX. We havenβt leveraged it much so far in the tutorial, but itβs helpful when managing data and state across components, which weβll dig into more in the next section.
Additionally, while there are some great resources on Ionic Angular and Phaser, I couldnβt find any existing resources for the combination of Ionic Vue and Phaser, so I decided to write up a tutorial based on my experience.
Got you. Leveraging Vue states in game development may prove useful and actually quite convenient. It is great that there are people filling the gaps of un-tutorialed combos. Looking forward to next parts.
I loved your post! I'm looking forward to the next parts. I'm trying it out with Android, and it's working wonderfully for me. I only modified the following line of code in the PhaserContainer.vue file with this:
// @ts-ignore
import { launch } from './../game/Game.js';
since it was telling me that it couldn't find any declaration for the Game.js file.
Thank you!!
Thanks for the great guide. That's what I was looking for. After all, I'm in the process of developing an online game. I want to create a similar platform like bestcasinoplay.com/online-slots/al... because as far as I know, it is very popular. This is a great option to make money.
I find this tutorial pretty cool, but why are you using ionic + vue with phaser? What is the point? doesn't it make it bloated?
It depends on your app. If you are building everything in Phaser and donβt have much or any app functionality outside the game, then you may not need a framework.
Ionic Vue provides components and functionality for building the UI of a mobile app that can be helpful if you want to have app functionality outside the game. Then, Capacitor lets you compile for native iOS and Android without needing to connect to a web server. You could also build a Phaser game and compile to native with just Capacitor if you donβt need mobile app components or functionality.
Check out this video and others from OpenForge for more on the benefits of combining Ionic and Phaser: youtu.be/oCq_tXX-0HQ?si=H4wzHeRi-H...
Ultimately youβll choose the stack that works best for your app.
Cecelia, great series! Is part 3 still coming at some point?
Hi Mike, I know this is a long time coming, but I finally published parts 3 and 4. You can find them here: dev.to/ceceliacreates/working-with...