I love playing video games, especially console and mobile games. I learned about Phaser (an open source framework for building games with JavaScript) last year and have wanted to try out game development using the skills I already have as a web developer.
Turns out, it's easier than I expected! Thanks to Phaser, along with Ionic, Capacitor, and Vue, I was able to get a mobile game up and running on an iOS device working only a few hours here and there over two weeks.
Blog Series Overview
❓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.
This blog series will walk through how to use Phaser, Ionic Vue, and Capacitor to build a mobile game of your own and run it on an actual mobile device.
For this series, we'll make a simplified version of the Ionitron game linked above to illustrate the concepts you'll need to learn to make your own game.
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.
What We're Building
This is a Vue mobile app built with Ionic Framework components that houses a Phaser game. We'll also use Capacitor to compile the app to native IOS and Android so it can run on mobile devices.
The app has the following features:
- A Play tab that launches the Phaser game for play
- An About tab that describes the game
- A Scores tab that shows your score history for the game
Let's get started!
Table of Contents
Creating a new Ionic Vue app
First we'll create a new Ionic Vue app using the Tabs starter template.
I'll use the Ionic CLI for this, but you can also use the Ionic VS Code Extension if you prefer.
Install the Ionic CLI:
npm install -g @ionic/cli
Then run the following to create a new app:
ionic start phaserGame tabs --type vue
Then, cd
into the directory and run ionic serve
to start the dev server and open the web version of our app in a browser. Switch your browser to mobile view using DevTools as you're working to see how the app will look on mobile.
We now have an Ionic Vue app with three tabs on the bottom for navigation.
Change the tab names to "Play", "About", and "Scores", and update the router and file names to correspond with the new names. Feel free to update the icons as well.
You'll need to change the following files:
src/router/index.ts
- All the files in
src/views
Here is the git commit with the changes for this section.
Adding Phaser to the app
Phaser Installation
Phaser has an NPM package we can install into our project.
Run npm install phaser
. This will add the dependency to your package.json
.
"dependencies": {
...
"phaser": "^3.60.0",
}
Here is the git commit with the changes for this section.
Creating a Phaser game component
Let's create a component that will manage launching our Phaser game.
Create a PhaserContainer.vue
file inside src/components
.
This component will contain the following:
- A button to start the game
- A
div
that will be the parent for the Phaser game - A click handler for the button that hides the button and runs a Phaser launch command
Add the following template
to your PhaserContainer
component:
// src/components/PhaserContainer.vue
<template>
<div id="game">
<ion-button v-if="showButton" @click="handleClickStart">Start
</ion-button>
</div>
</template>
The id of "game" on the div
will be passed to Phaser so it knows what HTML element is the parent for our game.
Now add the following script
to your component:
// src/components/PhaserContainer.vue
<script setup lang="ts">
import { ref } from 'vue'
import { IonButton } from '@ionic/vue';
import { Game} from 'phaser';
// binds to the v-if on our button to toggle visibility
const showButton = ref(true)
// Creates the new Phaser Game instance
function launch() {
// We'll fill this in later
}
function handleClickStart() {
// hides launch button
showButton.value = false;
// Runs the launch function
launch();
}
</script>
We'll also add some styling to make our button in the middle of the screen.
// src/components/PhaserContainer.vue
<style scoped>
#game {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
}
</style>
Now our PhaserContainer
component is ready! We just need to add it to our PlayPage
view.
// src/views/PlayPage.vue
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Play</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Play</ion-title>
</ion-toolbar>
</ion-header>
<PhaserContainer/>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/vue';
import PhaserContainer from '@/components/PhaserContainer.vue';
</script>
Here is the git commit with the changes for this section.
Creating Phaser launch function
Our PhaserContainer
component is working, but nothing happens when we click "Start" because we still need to write our launch()
function. Let's do that next!
Our launch()
function returns a new Phaser.Game
object with the configuration for our game, as well as the scene(s) for the game.
To keep our component clean and enforce separation of concerns, let's move our launch()
function to a separate file.
Create a new game
directory within src
with a new game.js
file.
Note: While Phaser does have some TypeScript support, I ran into several issues when trying to use Phaser with TS. For simplicity, we'll use JS for our Phaser game code.
Within this file, add the following code:
// src/game/game.js
import { Game, AUTO, Scale} from "phaser";
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: MainScene,
});
}
There are several things happening here, so let's break them down.
- We are returning a Phaser
Game
object that contains a configuration object. - The type of
AUTO
means that Phaser will detect if it should use WebGL or Canvas to render the game - The scale property handles our sizing and display. The
Scale.RESIZE
mode means the game will resize to fit the parent window. If you want your game to always have the same aspect ratio, useScale.FIT
instead. -
Scale.CENTER_BOTH
means the game is centered both vertically and horizontally within the parent - Using
window.innerWidth * window.devicePixelRatio
and the corresponding height values will make the game dynamic to the window, and ensure quality rendering on high-DPI (retina) displays - The
parent
value is the id of our parentdiv
HTML element - We're setting a
backgroundColor
for the game, this is optional - We're setting a Physics engine type of
arcade
. This is suitable for simple games and is more performant for mobile.
The last property is scene
, and this defines the scenes in our game.
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.
We'll explore more about Scenes later in this series. For now, we'll start with a single Main Scene to start the game.
Here is the git commit with the changes for this section.
Creating our first Phaser Scene
We've passed a value of MainScene
to our scene
property for our game, so let's create that next.
In the same game.js
file, update your Phaser import statement to include Scene
.
// src/game/game.js
import { Game, AUTO, Scale, Scene} from "phaser";
Next, add the following code, again in the same file.
// src/game/game.js
export class MainScene extends Scene {
constructor () {
super({ key: 'MainScene' })
}
create () {
this.add.text(100, 100, "Hello Phaser!", {
font: "24px Courier",
fill: "#ffffff",
});
}
}
Scenes in Phaser are classes that extend the Phaser Scene class. The key 'MainScene' is how we'll reference this scene in our game.
The create()
block is one of the Phaser lifecycle methods. We'll learn more about other lifecycle methods in later posts in this series.
For now, know that we are using the create()
block to add some initial text to our game. The first two parameters 100, 100
refers to the x and y coordinates of the text.
Here is the git commit with the changes for this section.
Putting it all together
The last thing we need to do is update our PhaserContainer
component to use our new launch function and Phaser scene.
Update the script
tag to import the launch()
function, and remove the the function declaration within the component.
// src/components/PhaserContainer.vue
<script setup lang="ts">
import { ref } from 'vue'
import { IonButton } from '@ionic/vue';
import { launch } from '@/game/game.js';
// binds to the v-if on our button to toggle visibility
const showButton = ref(true)
function handleClickStart() {
// hides launch button
showButton.value = false;
// Runs the launch function
launch();
}
</script>
Now we when we click 'Start', our Phaser game launches!
Here is the git commit with the changes for this section.
What's Next
In the next post in this series, we'll make our game more interesting and learn more about Phaser concepts like Scenes, Game Objects, Physics, Assets, and more.
Stay tuned!