Android Games in Capacitor with JavaScript

Published on Thursday, October 29th, 2020

So you want to put a web game out on the app store(s)? Let's take a look at capacitorjs and some HTML5 Canvas game code.

In the past I would have used something like Cordova, but this new thing from the folks at Ionic has TypeScript support out of the box for their native APIs and support for using any Cordova plugins you might miss.

TLDR show me the code

Capacitor Setup

The capacitor project setup is pretty straightforward from their docs, it can drop in place in an existing project or create a brand new project from scratch.

I opted for the brand new project:

> npx @capacitor/cli create

Android Setup

Before you try to run the project

  1. Download Android Studio Android Studio
  2. Open it up and update if needed (first time initialization takes some time)
  3. Accept your Java licenses, I haven't run Java in close to a decade 😱
    • In powershell, navigate to C:\Users\<username>\AppData\Local\Android\Sdk\tools\bin>
    • Set your java home temporarily $env:JAVA_HOME = 'C:\Program Files\Android\Android Studio\jre'
    • Run .\sdkmanager.bat --licenses and select y for each

Starting the App

Now that we have Android all setup we can start the app with the capacitor command line.

The gist is that it copies the final compiled html/css/js assets from your favorite frontend frameworks and build tools into the native container

> npx cap copy android

After that we can open it in Android Studio with the capacitor commandline

> npx cap open android

Building the project and running the first time can take some time, so be patient after hitting the big green play button.

ProTipTM The Emulator is MEGA slow to start so once you get it on, leave it on. You can redeploy the app to a running emulator with the "re-run" hightlighted below.

If your Android emulator crashes on the first try like mine did with something like The emulator process for AVD Pixel_3a_API_30_x86 was killed, this youtube video was super helpful. For me the problem was disk space, the AVD needs 7GBs of disk space to start so I had to clean out some junk on the laptop 😅

Building Your Canvas Game

The dev cycle is pretty slick, run npm cap copy android to move your built JS living in the www to the right android folder. The default app looks like this after running it in the android emulator.

Setting Up Your JS Build

First let's setup our TypeScript by installing and creating an empty tsconfig.json

> npm install typescript --save-dev --save-exact
> npx tsc --init`

Recently I've been a big fan of parcel(v1) for quick and easy project setup, and it works great with excalibur also webpack is cool too if you need more direct control of your js bundling.

> npm install parcel-bundler --save-dev --save-exact

I copied the generated manifest.json, index.html, and css/ folder out of the original generated www/ and put it into game/.

We need to setup our development and final build script in the package.json. The npm "start" script tells parcel to run a dev server and use game/index.html as our entry point to the app and follow the links and build them (notice the magic inline <script src="./main.ts"></script>) ✨

<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Game Test</title>
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">

<link rel="manifest" href="./manifest.json">
<link rel="stylesheet" href="./css/style.css">
</head>
<body>
<script src="./main.ts"></script>
</body>
</html>

In this setup I'm sending all my built output with --dist-dir into the www directory, which is what capacitor will copy to android. I went ahead and deleted the provided default app in the www directory.

/* package.json */
{
"name": "my-cool-game",
"scripts": {
"start": "parcel game/index.html --no-autoinstall --dist-dir www",
"typecheck": "tsc -p . --noEmit",
"build": "parcel build game/index.html --dist-dir www"
}
...
}

External links

We need parcel to copy the manifest.json to the output without bundling it, there is a small little plugin which allows you to enable globs as external links not to be touched .

> npm install parcel-plugin-html-externals --save-dev --save-exact

Configured like so:

/* package.json */
"externals": {
"./*.json": false
},

Game assets

For our game we probably have assets we want copied to the output, there is another plugin for that

> npm install parcel-plugin-asset-copier --save-dev --save-exact`

Configured like this:

/* package.json */
"assetsPath": "game/assets"

Vanilla Canvas code

To start with I have a really awesome game that shows the fps and a red square. This shows how get started from scratch with the HTML Canvas.

// main.ts
const canvas = document.createElement('canvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;

canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
document.body.appendChild(canvas);

let lastTime = performance.now();
const mainloop: FrameRequestCallback = (now) => {
const delta = (now - lastTime)/1000;
lastTime = now;
ctx.fillStyle = 'blue';
ctx.fillRect(0, 0, canvas.width, canvas.height);

ctx.font = '50px sans-serif';
ctx.fillStyle = 'lime';
ctx.fillText((1/delta).toFixed(1), 20, 100);

ctx.fillStyle = 'red';
ctx.fillRect(canvas.width/2, canvas.height/2, 40, 40);

requestAnimationFrame(mainloop);
}

mainloop(performance.now());

Using Excalibur🗡

Using the Excalibur engine with capacitor and parcel will be a breeze! Really any web based game engine could be substituted here if you want. Here is the source on github!

> npm install excalibur --save-exact

Update the main.ts with some Excalibur

import { Actor, DisplayMode, Engine, Input, Loader, Texture } from "excalibur";

const game = new Engine({
displayMode: DisplayMode.FullScreen,
pointerScope: Input.PointerScope.Canvas
});

const sword = new Texture('assets/sword.png');
const loader = new Loader([sword]);

game.start(loader).then(() => {
game.input.pointers.primary.on('move', event => {
const delta = event.pos.sub(actor.pos);
actor.vel = delta;
// Original asset is at a 45 degree angle need to adjust
actor.rotation = delta.toAngle() + Math.PI/4;
});

const actor = new Actor({
x: game.halfDrawWidth,
y: game.halfDrawHeight,
width: 40,
height: 40
});
actor.addDrawing(sword);

game.add(actor);
});

Tada! 🎉

Hope this helps you web game devs out there!

-Erik