Build a highly customizable mobile friendly HTML5 level selection screen controllable by tap and swipe written in TypeScript and powered by Phaser

If you are building a game with levels, then you need a level selection screen, and I am showing you how to do this since 2014 with the post HTML5 Phaser Tutorial: how to create a level selection screen with locked levels and stars.

A lot of time passed, and my last example was written in 2018 with the post HTML5 level select screen featuring swipe control, stars, progress saved on local storage, smooth scrolling, pagination and more. Powered by Phaser 3.

Now it’s time to move to TypeScript so I built a new prototype, allowing even more space for customization and using classes for a better code reusability.

Look at the prototype:

You can flip pages by swiping or tapping the bottom buttons. If you have a mobile phone, you can try it at this link.

This example was built using one HTML file and 6 TypeScript files.

They are still uncommented because I need to add more features, but they are quite easy to understand, let’s see them all in detail:

index.html

The web page which hosts the game, which will run inside thegame element.

<!DOCTYPE html>
<html>
	<head>
        <style type = "text/css">
            * {
                padding: 0;
                margin: 0;
            }
            body{
                background: #000;
            }
            canvas {
                touch-action: none;
                -ms-touch-action: none;
            }
        </style>
        <script src = "main.js"></script>
    </head>
	<body>
        <div id = "thegame"></div>
	</body>
</html>

gameOptions.ts

Configurable game options. Changing these values you can adjust the game to fit your needs.

// CONFIGURABLE GAME OPTIONS

export const GameOptions = {
    pages : 6,
    tintColors: [0xff0000, 0x00ff00, 0x0000ff],
    columns: 3,
    rows: 4,
    thumbWidth: 60,
    thumbHeight: 60,
    spacing: 20
}

main.ts

This is where the game is instanced, with all Phaser related options

// MAIN GAME FILE

// modules to import
import Phaser from 'phaser';
import { PreloadAssets } from './preloadAssets';
import { PlayGame } from './playGame';

// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
    mode : Phaser.Scale.FIT,
    autoCenter : Phaser.Scale.CENTER_BOTH,
    parent : 'thegame',
    width : 320,
    height : 480
}

// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
    type : Phaser.AUTO,
    backgroundColor : 0x222222,
    scale : scaleObject,
    scene : [PreloadAssets, PlayGame]
}

// the game itself
new Phaser.Game(configObject);

preloadAssets.ts

Here we preload all assets to be used in the game.

// CLASS TO PRELOAD ASSETS

// this class extends Scene class
export class PreloadAssets extends Phaser.Scene {

    // constructor    
    constructor() {
        super({
            key : 'PreloadAssets'
        });
    }

    // method to be execute during class preloading
    preload(): void {

        this.load.spritesheet("levelthumb", "assets/levelthumb.png", {
            frameWidth : 60,
            frameHeight : 60
        });

        this.load.spritesheet('levelpages', 'assets/levelpages.png', {
            frameWidth: 30,
            frameHeight: 30
        });

        this.load.image('transp', 'assets/transp.png');
	}

    // method to be called once the instance has been created
	create(): void {

        // call PlayGame class
        this.scene.start('PlayGame');
	}
}

playGame.ts

Main game file, where we handle input and visual effects.

// THE GAME ITSELF

// modules to import
import { GameOptions } from './gameOptions';
import { LevelThumbnail } from './levelThumbnail';
import { PageSelector } from './pageSelector';

// this class extends Scene class
export class PlayGame extends Phaser.Scene {

    canMove : boolean;
    itemGroup : Phaser.GameObjects.Group;
    pageText : Phaser.GameObjects.Text;
    gameWidth : number;
    gameHeight : number;
    scrollingMap : Phaser.GameObjects.TileSprite;
    currentPage : number;
    pageSelectors : PageSelector[];

    // constructor
    constructor() {
        super({
            key: 'PlayGame'
        });
    }

    // method to be called once the class has been created
    create() : void {

        this.initializeProperties();
        this.addBackground();
        this.addInfoText();
        this.addLevelThumbnails();

        this.input.setDraggable(this.scrollingMap);
        this.input.on('drag', this.handleDrag, this);
        this.input.on('dragend', this.handleDragEnd, this);
    }    

    initializeProperties() : void {
        this.canMove = true;
        this.gameWidth = this.game.config.width as number;
        this.gameHeight = this.game.config.height as number;
        this.itemGroup = this.add.group();
        this.currentPage = 0;
        this.pageSelectors = [];
    }

    addBackground() : void {   
        this.scrollingMap = this.add.tileSprite(-10, 0, GameOptions.pages * this.gameWidth + 20, this.gameHeight, 'transp');
        this.scrollingMap.setOrigin(0, 0);
        this.scrollingMap.setInteractive();
    }

    addInfoText() : void {
        this.pageText = this.add.text(this.gameWidth / 2, 16, 'Swipe to select level page (1 / ' + GameOptions.pages + ')', {
            font : '18px Arial',
            color : '#ffffff',
            align : 'center'
        });
        this.pageText.setOrigin(0.5);
    }

    addLevelThumbnails() : void {
        let rowLength : number = GameOptions.thumbWidth * GameOptions.columns + GameOptions.spacing * (GameOptions.columns - 1);
        let leftMargin : number = (this.gameWidth - rowLength) / 2 + GameOptions.thumbWidth / 2;
        let columnHeight : number = GameOptions.thumbHeight * GameOptions.rows + GameOptions.spacing * (GameOptions.rows - 1);
        let topMargin : number = (this.gameHeight - columnHeight) / 2 + GameOptions.thumbHeight / 2;
        for (let k : number = 0; k < GameOptions.pages; k ++) {
            for (let i : number = 0; i < GameOptions.columns; i ++) {
                for(let j : number = 0; j < GameOptions.rows; j ++) {
                    let posX : number = k * this.gameWidth + leftMargin + i * (GameOptions.thumbWidth + GameOptions.spacing);
                    let posY : number = topMargin + j * (GameOptions.thumbHeight + GameOptions.spacing);
                    let levelNumber: number = k * (GameOptions.rows * GameOptions.columns) + j * GameOptions.columns + i;
                    let thumb : LevelThumbnail = new LevelThumbnail(this, posX, posY, 'levelthumb', levelNumber, levelNumber != 0);
                    thumb.setTint(GameOptions.tintColors[k % GameOptions.tintColors.length]);
                    this.itemGroup.add(thumb);
                    var levelText = this.add.text(thumb.x, thumb.y - 12, thumb.levelNumber.toString(), {
                        font: '24px Arial',
                        color: '#000000'
                    });
                    levelText.setOrigin(0.5);
                    this.itemGroup.add(levelText);
                }
            }
            this.pageSelectors[k] = new PageSelector(this, this.gameWidth / 2 + (k - Math.floor(GameOptions.pages / 2) + 0.5 * (1 - GameOptions.pages % 2)) * 40, this.gameHeight - 40, 'levelpages', k);
            this.pageSelectors[k].pageIndex = k;
            this.pageSelectors[k].setTint(GameOptions.tintColors[k % GameOptions.tintColors.length])
            if (k == this.currentPage) {
                this.pageSelectors[k].setFrame(1);
            }
            else {
                this.pageSelectors[k].setFrame(0);
            }
        }      
    }

    handleDrag(pointer : Phaser.Input.Pointer) : void {
        if (this.canMove) {
            let deltaX : number = pointer.position.x - pointer.prevPosition.x;
            if (this.scrollingMap.x + deltaX > 0) {
                deltaX = -this.scrollingMap.x;
            }
            let rightLimit : number = -((GameOptions.pages - 1) * this.gameWidth + 20)
            if (this.scrollingMap.x + deltaX < rightLimit) {
                deltaX = rightLimit - this.scrollingMap.x; 
            }
            this.scrollingMap.x += deltaX;
            let items : Phaser.GameObjects.Sprite[] = this.itemGroup.getChildren() as Phaser.GameObjects.Sprite[];
            items.map((item) => {
                item.x += deltaX;
            });   
        }
    }

    handleDragEnd(pointer : Phaser.Input.Pointer) : void {
        if (this.canMove) {
            let deltaX : number = pointer.downX - pointer.position.x;
            if (deltaX == 0) {
                this.canMove = false;
                let items : Phaser.GameObjects.Sprite[] = this.itemGroup.getChildren() as Phaser.GameObjects.Sprite[];
                items.map((item) => {
                    if (item instanceof LevelThumbnail) {
                        let boundingBox : Phaser.Geom.Rectangle = item.getBounds();
                        if (Phaser.Geom.Rectangle.Contains(boundingBox, pointer.position.x, pointer.position.y)) {
                            if (item.locked) {
                                this.tweens.add({
                                    targets : [item],
                                    alpha : 0.2,
                                    duration : 50,
                                    ease : 'Cubic.easeInOut',
                                    yoyo : true,
                                    repeat : 2,
                                    callbackScope: this,
                                    onComplete : this.thumbTweenComplete
                                })
                            }
                        }
                    } 
                }); 
            }
            else {
                if (Math.abs(deltaX) > this.gameWidth / 5) {
                    this.changePage(deltaX > 0 ? 1 : -1);    
                }
                else {
                    this.changePage(0);
                }
            }
        }
    }

    thumbTweenComplete(tween : Phaser.Tweens.Tween, item : Phaser.GameObjects.GameObject) : void {
        this.canMove = true;
    }

    goToPage(page : number) : void {
        if (this.canMove) {
            let difference : number = page - this.currentPage;
            this.changePage(difference);
        }
    }

    changePage(amount : number) : void {
        this.canMove = false;
        if (this.currentPage + amount < 0 || this.currentPage + amount > GameOptions.pages - 1) {
            amount = 0;
        }
        this.currentPage += amount;
        for (let k : number = 0; k < GameOptions.pages; k ++) {
            if (k == this.currentPage) {
                this.pageSelectors[k].setFrame(1);
            }
            else {
                this.pageSelectors[k].setFrame(0);
            }
        }
        this.pageText.text = 'Swipe to select level page (' + (this.currentPage + 1).toString() + ' / ' + GameOptions.pages + ')';
        this.tweens.add({
            targets: [this.scrollingMap],
            x: this.currentPage * this.gameWidth * -1 - 10,
            duration: 300,
            ease: 'Cubic.easeOut',
            callbackScope: this,
            onUpdate : this.pageTweenUpdate,
            onComplete : this.pageTweenComplete
        })
    }

    pageTweenUpdate(tween: Phaser.Tweens.Tween, target : Phaser.GameObjects.Sprite, position : number) : void {
        position = target.x;
        let items : Phaser.GameObjects.Sprite[] = this.itemGroup.getChildren() as Phaser.GameObjects.Sprite[];
        items.map((item) => {
            let current : number = tween.data[0].current as number;
            let previous : number = tween.data[0].previous as number;
            item.x += current - previous;
        });
    }

    pageTweenComplete() : void {
        this.canMove = true;
    }
    
    
}

pageSelector.ts

This is the class representing the small page selector at the bottom of the screen.

// PAGE SELECTOR

import { PlayGame } from "./playGame";

// this class extends Sprite class
export class PageSelector extends Phaser.GameObjects.Sprite {

    pageIndex : number;
    parentScene : PlayGame;

    constructor(scene : PlayGame, x : number, y : number, key : string, pageIndex : number) {

        super(scene, x, y, key);
      
        this.pageIndex = pageIndex;
        scene.add.existing(this);
        this.setInteractive();
        this.parentScene = scene;

        this.on('pointerdown', this.handlePointer);
         
      
    }

    handlePointer() : void {
        this.parentScene.goToPage(this.pageIndex);
    }

    
}

levelThumbnail.ts

This is the class representing the level thumbnail

// LEVEL THUMBNAIL

// this class extends Sprite class
export class LevelThumbnail extends Phaser.GameObjects.Sprite {

    levelNumber : number;
    locked : boolean;

    constructor(scene : Phaser.Scene, x : number, y : number, key : string, level : number, locked : boolean) {
        super(scene, x, y, key);
        scene.add.existing(this);
        this.levelNumber = level;
        this.locked = locked;
        this.setFrame(locked ? 0 : 1);
    }
}

Thanks to this prototype, now you can have your level selection screen with all required mobile friendly features.

Next time I’ll show you how to select and play levels, meanwhile download the source code of the entire project.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

215 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stairs
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers