Sharing your completed Phaser game with the world is an exhilarating experience. While it's desktop-friendly and easy to upload to a web host, an even larger audience is waiting on mobile devices! Imagine seeing your game on the App Store or Google Play, reaching new users, monetizing, and expanding your impact. It may seem daunting, but with a few mobile-specific tweaks and the power of Capacitor, transforming your game into an Android and iOS app is surprisingly straightforward.

Capacitor is a cross-platform native runtime for web apps. Essentially, it provides tooling to transform a web app into a hybrid iOS and Android app, as well as JavaScript APIs to connect to native mobile device features like geolocation, Bluetooth, and more.

In this tutorial, you'll learn how to deploy your Phaser game to iOS and Android using Capacitor. We'll make an existing game more mobile-friendly by adding touch controls, resizing the game for mobile devices, and more. Let’s dive into the process and prepare the game for the mobile market!

starshake-missiles

Prerequisites

Before we begin, ensure you have the following installed on your machine:

If you're new to mobile development and using Windows, start with Android.

Next, download a copy of the excellent Phaser by Example book. This free resource walks you through the process of creating several different games. In this tutorial, we'll modify the space shooter game starshake from Chapter 2. Here are some handy links to review:

Here's the updated game running on an iPhone 14 Pro:

Clone the phaser-by-example repository and navigate to the starshake folder.

Mobile Deployment using Capacitor

Let's begin by bringing Capacitor into the project to get all mobile tooling in place for testing and development. In just a few steps, we'll have a native iOS and Android app alongside our Phaser game code!

Install Capacitor

First, from within the phaser-by-example directory, navigate to the starshake directory and install Capacitor:

cd starshake
npm install @capacitor/core @capacitor/cli

Next, initialize Capacitor:

npx cap init

You'll be prompted to provide your app's name and ID. The ID should be in reverse domain notation, like com.example.starshake. Next, create a production build of the game:

npm run build

Since this project uses Vite, a dist directory containing the game's static files (HTML, CSS, JavaScript) is created. Now, we need to tell Capacitor where the built files are located. Open capacitor.config.json and check that the webDir property points to the dist build directory. For example:

{
  "appId": "com.example.starshake",
  "appName": "PhaserGame",
  "webDir": "dist",
  "bundledWebRuntime": false
}

Add Mobile Platforms

With all configurations in place, we're ready to add the Android and iOS platforms. To add them, run the following commands:

npx cap add android
npx cap add ios

Android and iOS native projects are created at the top level of the Starshake project. These are real mobile projects that should be committed to source control. Capacitor uses the npx cap copy command to copy your built web assets into the native projects. Run this command every time you build your web project:

npm run build
npx cap copy

Open and Build Native Projects

Your development machine must be set up for iOS and Android development if you want to run the game on a simulator or real device. Follow the Capacitor documentation to setup your preferred environment. Once you have Xcode and/or Android Studio installed, open them:

npx cap open ios
npx cap open android

To run the game on a device, press the "play" button. Running on a simulator is less complicated to set up, but I recommend testing on a physical device before shipping to the app stores.

Congrats! The Starshake game is now running on your device. However, it looks quite off—most of the game isn't visible!

Mobile Design Changes

Let's make some changes to the game so it runs better on a mobile device. To see these changes take effect quickly, let's take a quick detour to set up live debugging. With those steps in place, the app reloads and displays the latest changes each time you save a file. This is incredibly handy for rapid iteration.

Debugging

In a terminal, run npm run dev to start the game on localhost. Note that the URL is similar to http://localhost:8080. Open up capacitor.config.json and add a server entry, configuring the url field with the local web server's IP address and port:

"server": {
  "url": "http://localhost:8080",
  "cleartext": true
},

In another terminal window, run npx cap copy to copy the updated Capacitor config into all native projects. If the app was running on a device or in a simulator, stop it and restart it so the config changes take effect.

Desktop to Mobile Sizing

Phaser uses pixels for all positioning and scaling. Hard-coded values like those used by the Starshake game work fine in desktop browsers but not on mobile. To calculate the correct width and height, we'll multiply inner height/weight with devicePixelRatio, which represents the ratio of pixel sizes: the size of one CSS pixel to the size of one physical pixel. This is useful when dealing with the difference between rendering on a standard display versus a HiDPI or Retina display, which uses more screen pixels to draw the same objects, resulting in a sharper image. Here's a snippet of the updated main.js file:

// main.js
const config = {
  type: Phaser.AUTO,
  scale: {
    mode: Scale.FIT,
    width: window.innerWidth * window.devicePixelRatio,
    height: window.innerHeight * window.devicePixelRatio
  },
// snip

This looks much better! You'll notice that we still can't play the game. It assumes you'll have a keyboard and can press the spacebar to start it.

Tap Tap!

Opening up splash.js, capture the "pointerup" event, and same as pressing the spacebar, call the transitionToChange() function:

// splash.js
create() {
   this.input.keyboard.on("keydown-SPACE",() => this.transitionToChange(),this);

   // On mobile, tap the screen to start the game
   this.input.on('pointerup', () => this.transitionToChange(), this);
}

Adjust the Spaceship

Now that we can advance past the splash screen, the game begins, but we cannot move the spaceship or fire missiles. Let's make the spaceship draggable by capturing the drag event when touching the screen. Within player.js, pass the X and Y coordinates into the setPosition method:

// player.js
setControls() {
    // mobile: make the spaceship draggable on the screen
    this.setInteractive({ draggable: true })
    .on('drag', function(pointer, dragX, dragY){
        this.setPosition(dragX, dragY);
    }, this);
}

We can move the spaceship around with our fingers instead of a keyboard. Next, we need a way to fire missiles. For simplicity, we'll switch from a keyboard press to always firing. We'll add a 200 millisecond delay between missiles. If we don't add this, the missiles will fire incredibly fast, which would definitely be "OP!"

First, create a variable representing the next time to fire a missile every 200 milliseconds in the constructor.

constructor(scene, x, y, name = "player1", powerUp = "water") {
  this.nextShotTime = 200;
}

Below the code that shoots after the spacebar is pressed in the update method, add logic to shoot missiles continuously every 200 milliseconds. Subtract the delta (the delta time in milliseconds since the last frame) from nextShotTime, and once it reaches zero, reset the counter and fire a missile:

// player.js
  update(timestep, delta) {
    if (Phaser.Input.Keyboard.JustDown(this.SPACE)) {
      this.shoot();
    }

    // Continually shoot missiles every 200 ms
     this.nextShotTime -= delta;

     if (this.nextShotTime < 0) {
         this.nextshotTime = 200;
         this.shoot();
     }
}

Finally, over in game.js, update the main game loop to pass time and delta values into the Player update function:

// game.js
update(time, delta) {
    if (this.player) this.player.update(time, delta);
    this.foes.update();
    this.background.tilePositionY -= 10;
  }

Safe Area Support

The game is completely playable now! The last mobile aspect we need to account for is the "safe area," the view area that isn't covered by a navigation bar, tab bar, toolbar, or other views a window might provide. Safe areas are essential for avoiding a device's interactive and display features, like the iPhone's notch or Dynamic Island.

When I ran the game on my iPhone, for example, I noticed the player scores were not fully visible on the screen. To address this, open index.html and add viewport-fit=cover to the viewport meta tag. This tells the browser to use all available screen space.

<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0" />

We must also use safe-area-inset-top to ensure our game isn't cut off by form factors such as the notch. The safe-area-inset-* variables are four environment variables that define a rectangle by its top, right, bottom, and left insets from the edge of the viewport, which is safe to put content into without risking it being cut off by the shape of a non‑rectangular display. Within the head's style tag, add the following, then apply the main class to the body:

// index.html
.main {
   padding-top: max(env(safe-area-inset-top));
}
// index.html
<body class="main">
  // snip
</body>

Much better! The added padding ensures we can see the game scores no matter the device used.

Mobile vs. Desktop Play Instructions

The last change to make the game mobile-friendly is simple but will matter to the user: displaying different how-to-play instructions based on whether the player is on mobile or desktop. Currently, the game assumes we're playing on desktop, so we can leverage Phaser's game.device API (or Capacitor's Capacitor.isNativePlatform() API) ' to show different instructions based on the platform.

Back over in splash.js, we can tell the player to use the arrows to move the spaceship if on desktop or drag it with their finger if on mobile using a check to this.game.device.os.desktop:

// splash.js
showInstructions() {
    this.add
      .bitmapText(this.center_width, 450, "wendy", 
        this.game.device.os.desktop ? "Arrows to move" : "Move by dragging spaceship", 60)
      .setOrigin(0.5)
      .setDropShadow(3, 4, 0x222222, 0.7);
    if (this.game.device.os.desktop) {
      this.add
        .bitmapText(this.center_width, 500, "wendy", "SPACE to shoot", 60)
        .setOrigin(0.5)
        .setDropShadow(3, 4, 0x222222, 0.7);
    }

starshake-title-screen

Next Steps

With just a handful of code additions and changes, we've successfully made the Phaser game Starshake more mobile-friendly. There are more tweaks you could explore! Consider updating outro.js to use mobile touch controls and instructions. Or, add mobile device functionality using a Capacitor plugin. With the ability to reach users from the app stores, a massive part of the gaming market has opened up for you to explore. Enjoy!