In this tutorial, you’ll read about the main steps to build your own WPA HTML5 game. The game is deployed at close to no cost using Github pages and uses a simple monetizing strategy using Adsense.
The game we are building is Bricks. It’s a shape stacking game similar to the old classic Tetris but on a squared grid and there without gravity, with… out… stress…
The Setup
First thing we need to do is setup our project main structure and build system. This will help us in our dev process and later during deployment. For that we use Vite, the next generation frontend tooling. So while this project will use Vanilla JS without a framework, we still want frontend tooling to help us bundle the app and its assets.
In a few steps we bootstrap a new Vite Vanilla JS project:
npm create vite@latest
Follow the instructions and that’s it, let’s start cooking!
Progressive Web App you say?
A PWA allows you to host a Mobile/Desktop app just like you would host a static website, without having to go through the App store publication process and remain fully self published. The conditions to qualify as PWA are more detailed in such article but in short you need to have:
- an app icon,
- host your site on https,
- register a service worker
- include a web app manifest file
First thing we need is to create a manifest.webmanifest
file. In this tutorial we use a Vite plugin to simplify the PWA registration process so instead of adding the file, we add it top oue Vite config file vite.config.file
as below:
...
export default ({ mode }) => {
return defineConfig({
plugins: [
VitePWA({
registerType: 'autoUpdate',
manifest: {
"start_url": "/index.html",
"background_color": "#7cc4ff",
"theme_color": "#7cc4ff",
"description": "Fit as many piece as you can",
"display": "standalone",
"icons": [
{
"src": "icon.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"name": "Bricks",
"short_name": "Bricks",
}
}),
...
});
}
See the entire file here
The next step is to add the JS code to register the service worker:
import { registerSW } from "virtual:pwa-register";
if ("serviceWorker" in navigator && !/localhost/.test(window.location)) {
registerSW();
}
Ok so we got a PWA now but we’re still missing the install button!
// Code to handle install prompt on desktop
let deferredPrompt;
const addBtn = document.querySelector('#install-btn');
window.addEventListener('beforeinstallprompt', (e) => {
// Prevent Chrome 67 and earlier from automatically showing the prompt
e.preventDefault();
// Stash the event so it can be triggered later.
deferredPrompt = e;
// Update UI to notify the user they can add to home screen
addBtn.style.display = 'block';
addBtn.addEventListener('click', () => {
// hide our user interface that shows our A2HS button
addBtn.style.display = 'none';
// Show the prompt
deferredPrompt.prompt();
// Wait for the user to respond to the prompt
deferredPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the A2HS prompt');
} else {
console.log('User dismissed the A2HS prompt');
}
deferredPrompt = null;
});
});
});
See the entire file here
The code registers a service worker and enable a app install button. This feature seems to only reliably work on Chrome Desktop and Android and will create an app Icon on your main screen. The user can then access the app directly without opening their browser.
See the entire file here
Let’s make a game
So now we have a basic PWA setup, we can start building our game.
One Stop Store
While I tried to keep it simple and Vanilla, without too many dependencies, I still wanted to have a global state manager. The following is a bare bone Object implementation that will manage the state of our game.
Define a Single State
Heavily inspired by VueX and Flux architecture we define a single state as below:
const state = {
score: 0,
bankPieces: [
new Piece(Piece.PIECE_NONE.type),
new Piece(Piece.PIECE_NONE.type),
new Piece(Piece.PIECE_NONE.type)
],
backupPiece: new Piece(Piece.PIECE_NONE.type),
grid: new Grid(),
changes: {
grid: {
previous: 0,
value: 1,
},
bank: {
previous: 0,
value: 1,
}
}
};
Define Mutations
Mutations are function that mutate the state. they are the only allowed ways of mutating our state. This way logic remain centralized and easy to understand and maintain.
mutate: {
resetBank () {
state.bankPieces.forEach(piece => piece.shuffle())
state.changes.bank.value += 1;
},
undo () {
...
},
restart () {
...
},
hasRendered (target) {
state.changes[target].previous = state.changes[target].value;
},
bumpScoreBy (points) {
state.score += points;
},
setBackupPiece (piece) {
state.changes.bank.value += 1;
state.backupPiece.copyPiece(piece);
},
removeFromBank (piece) {
...
},
checkWins () {
...
},
},
Define Getters
Finally we expose a few getters so that the view layer can easily consume the state and render it without extra logical efforts
getters: {
getPieceById: pieceId => {
switch (pieceId) {
case 'bank-piece-1':
return state.bankPieces[0];
case 'bank-piece-2':
return state.bankPieces[1];
case 'bank-piece-3':
return state.bankPieces[2];
case 'backup-piece':
return state.backupPiece;
}
},
canAcceptBackup: () => state.backupPiece.type === Piece.PIECE_NONE.type,
gridChanged: () => state.changes.grid.previous !== state.changes.grid.value,
bankChanged: () => state.changes.bank.previous !== state.changes.bank.value,
}
See the entire file here
Game Objects
Our game needs a the following objects:
Cell.js
see code
A cell is the most basic primitive in the game. It can be set or unset.
Grid.js
see code
This class is a collection of cells to form a grid. It controls the logic of the grid, helping retrieving the state of a cell, and setting the state ready for the rendering.
Piece.js
see code
A piece is a collection of cells, with an array of points an a type name to be easily recognized, rotated and shuffled.
Draggable.js
see code
This class manages the drag and drop interactions. keeping track of the mouse/touch position to handle dragging pieces to the grid.
Putting it together
With all this earlier work breaking down the game complexity to a global state and several helper objects, we can finally put our game together. At this point it should almost feel like a Lego toy…
In our case main.js will do the job. this is where we initialize all the objects and hook our rendering function, re-rendering the UI only when the store tells us to:
function update() {
window.requestAnimationFrame(update);
if (getters.gridChanged()) {
redrawGrid();
redrawScore();
}
if (getters.bankChanged()) {
redrawBank();
}
}
Thanks for the global state management, we can check if the state has changed and render the UI on every new frame, only as needed.
See the entire file here
Monetizing with AdSense
Assuming that you already have an account setup and approved, you just need to inject the ad snippets at the right place. For that we use the Vite HTML plugin so we can render our Ad code only on production. We add the following to our Vite config:
import { createHtmlPlugin } from 'vite-plugin-html' const htmlPlugin = mode => { let banner = ''; if (mode === 'production') { banner = `
<ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-XXXXXXXXXX" data-ad-slot="XXXXXXXX" data-ad-format="auto" data-full-width-responsive="true"></ins> <script> (adsbygoogle = window.adsbygoogle || []).push({}); </script>`;
} …
Next we can add the banner
alias we just created to our HTML template:
<%- banner -%>
See index.html and vite.config.js for reference.
Wrapping Up
Of course this tutorial only skims the surface and attempts to summarize the main steps. You can find the full code on github and try the demo here
As time goes and allows, I will try to expand this tutorial into more detailed sections. If you have any questions, feel free to reach out! Hope this helps! Cheers!