HTML5 level select screen featuring swipe control, stars, progress saved on local storage, smooth scrolling, pagination and more. Powered by Phaser 3

Two years ago, I built a HTML5 level select screen controlled by swipe with level selection, stars, progress saved on local storage and some other nice features everybody expects from a modern game. I ported it to Phaser 3 mainly because I was really needing something like that for a project, so let’s jump straight to the point and see what I did:
Swipe pages or using the buttons on the bottom to jump straight to a page, select levels and play. Play levels, fail them or complete them with 1, 2 or three stars, unlock levels and do everything you would expect from a professional level select screen. The core of the idea is the background tile sprite the player drags. A series of callbacks and tweens do the rest in this uncommented source code – but you can find the commented Phaser 2 version in the original post. This is not a beginner script as there are more than 200 lines of code, but a more optimized and commented version will come in a few days. I am using it in my latest game so I am polishing it a bit.
var game;
var gameOptions = {
    colors: ["0xffffff","0xff0000","0x00ff00","0x0000ff","0xffff00"],
    columns: 3,
    rows: 4,
    thumbWidth: 60,
    thumbHeight: 60,
    spacing: 20,
    localStorageName: "levelselect"
}
window.onload = function() {
    var gameConfig = {
        width: 320,
        height: 480,
        backgroundColor: 0x222222,
        scene: [playGame, playLevel]
    }
    game = new Phaser.Game(gameConfig);
}
class playGame extends Phaser.Scene{
    constructor(){
        super("PlayGame");
    }
    preload(){
        this.load.spritesheet("levelthumb", "levelthumb.png", {
            frameWidth: 60,
            frameHeight: 60
        });
        this.load.image("levelpages", "levelpages.png");
        this.load.image("transp", "transp.png");
    }
    create(){
        this.stars = [];
        this.stars[0] = 0;
        this.canMove = true;
        this.itemGroup = this.add.group();
        for(var l = 1; l < gameOptions.columns * gameOptions.rows * gameOptions.colors.length; l++){
            this.stars[l] = -1;
        }
        this.savedData = localStorage.getItem(gameOptions.localStorageName) == null ? this.stars.toString() : localStorage.getItem(gameOptions.localStorageName);
        this.stars = this.savedData.split(",");
        this.pageText = this.add.text(game.config.width / 2, 16, "Swipe to select level page (1 / " + gameOptions.colors.length + ")", {
            font: "18px Arial",
            fill: "#ffffff",
            align: "center"
        });
        this.pageText.setOrigin(0.5);
        this.scrollingMap = this.add.tileSprite(0, 0, gameOptions.colors.length * game.config.width, game.config.height, "transp");
        this.scrollingMap.setInteractive();
        this.input.setDraggable(this.scrollingMap);
        this.scrollingMap.setOrigin(0, 0);
        this.currentPage = 0;
        this.pageSelectors = [];
        var rowLength = gameOptions.thumbWidth * gameOptions.columns + gameOptions.spacing * (gameOptions.columns - 1);
        var leftMargin = (game.config.width - rowLength) / 2 + gameOptions.thumbWidth / 2;
        var colHeight = gameOptions.thumbHeight * gameOptions.rows + gameOptions.spacing * (gameOptions.rows - 1);
        var topMargin = (game.config.height - colHeight) / 2 + gameOptions.thumbHeight / 2;
        for(var k = 0; k < gameOptions.colors.length; k++){
            for(var i = 0; i < gameOptions.columns; i++){
                for(var j = 0; j < gameOptions.rows; j++){
                    var thumb = this.add.image(k * game.config.width + leftMargin + i * (gameOptions.thumbWidth + gameOptions.spacing), topMargin + j * (gameOptions.thumbHeight + gameOptions.spacing), "levelthumb");
                    thumb.setTint(gameOptions.colors[k]);
                    thumb.levelNumber = k * (gameOptions.rows * gameOptions.columns) + j * gameOptions.columns + i;
                    thumb.setFrame(parseInt(this.stars[thumb.levelNumber]) + 1);
                    this.itemGroup.add(thumb);
                    var levelText = this.add.text(thumb.x, thumb.y - 12, thumb.levelNumber, {
                        font: "24px Arial",
                        fill: "#000000"
                    });
                    levelText.setOrigin(0.5);
                    this.itemGroup.add(levelText);
                }
            }
            this.pageSelectors[k] = this.add.sprite(game.config.width / 2 + (k - Math.floor(gameOptions.colors.length / 2) + 0.5 * (1 - gameOptions.colors.length % 2)) * 40, game.config.height - 40, "levelpages");
            this.pageSelectors[k].setInteractive();
            this.pageSelectors[k].on("pointerdown", function(){
                if(this.scene.canMove){
                    var difference = this.pageIndex - this.scene.currentPage;
                    this.scene.changePage(difference);
                    this.scene.canMove = false;
                }
            });
            this.pageSelectors[k].pageIndex = k;
            this.pageSelectors[k].tint = gameOptions.colors[k];
            if(k == this.currentPage){
                this.pageSelectors[k].scaleY = 1;
            }
            else{
                this.pageSelectors[k].scaleY = 0.5;
            }
        }
        this.input.on("dragstart", function(pointer, gameObject){
            gameObject.startPosition = gameObject.x;
            gameObject.currentPosition = gameObject.x;
        });
        this.input.on("drag", function(pointer, gameObject, dragX, dragY){
            if(dragX <= 10 && dragX >= -gameObject.width + game.config.width - 10){
                gameObject.x = dragX;
                var delta = gameObject.x - gameObject.currentPosition;
                gameObject.currentPosition = dragX;
                this.itemGroup.children.iterate(function(item){
                    item.x += delta;
                });
            }
        }, this);
        this.input.on("dragend", function(pointer, gameObject){
            this.canMove = false;
            var delta = gameObject.startPosition - gameObject.x;
            if(delta == 0){
                this.canMove = true;
                this.itemGroup.children.iterate(function(item){
                    if(item.texture.key == "levelthumb"){
                        var boundingBox = item.getBounds();
                        if(Phaser.Geom.Rectangle.Contains(boundingBox, pointer.x, pointer.y) && item.frame.name > 0){
                            this.scene.start("PlayLevel", {
                                level: item.levelNumber,
                                stars: this.stars
                            });
                        }
                    }
                }, this);
            }
            if(delta > game.config.width / 8){
                this.changePage(1);
            }
            else{
                if(delta < -game.config.width / 8){
                    this.changePage(-1);
                }
                else{
                    this.changePage(0);
                }
            }
        }, this);
    }
    changePage(page){
        this.currentPage += page;
        for(var k = 0; k < gameOptions.colors.length; k++){
            if(k == this.currentPage){
                this.pageSelectors[k].scaleY = 1;
            }
            else{
                this.pageSelectors[k].scaleY = 0.5;
            }
        }
        this.pageText.text = "Swipe to select level page (" + (this.currentPage + 1).toString() + " / " + gameOptions.colors.length + ")";
        var currentPosition = this.scrollingMap.x;
        this.tweens.add({
            targets: this.scrollingMap,
            x: this.currentPage * -game.config.width,
            duration: 300,
            ease: "Cubic.easeOut",
            callbackScope: this,
            onUpdate: function(tween, target){
                var delta = target.x - currentPosition;
                currentPosition = target.x;
                this.itemGroup.children.iterate(function(item){
                    item.x += delta;
                });
            },
            onComplete: function(){
                this.canMove = true;
            }
        });
    }
}
class playLevel extends Phaser.Scene{
    constructor(){
        super("PlayLevel");
    }
    init(data){
        this.level = data.level;
        this.stars = data.stars;
    }
    create(){
        this.add.text(game.config.width / 2, 20, "Play level " + this.level.toString(), {
            font: "32px Arial",
            color: "#ffffff"
        }).setOrigin(0.5);
        var failLevel = this.add.text(20, 60, "Fail level", {
            font: "48px Arial",
            color: "#ff0000"
        });
        failLevel.setInteractive();
        failLevel.on("pointerdown", function(){
            this.scene.start("PlayGame");
        }, this);
        var oneStarLevel = this.add.text(20, 160, "Get 1 star", {
            font: "48px Arial",
            color: "#ff8800"
        });
        oneStarLevel.setInteractive();
        oneStarLevel.on("pointerdown", function(){
            this.stars[this.level] = Math.max(this.stars[this.level], 1);
            if(this.stars[this.level + 1] != undefined && this.stars[this.level + 1] == -1){
                this.stars[this.level + 1] = 0;
            }
            localStorage.setItem(gameOptions.localStorageName, this.stars.toString());
            this.scene.start("PlayGame");
        }, this);
        var twoStarsLevel = this.add.text(20, 260, "Get 2 stars", {
            font: "48px Arial",
            color: "#ffff00"
        });
        twoStarsLevel.setInteractive();
        twoStarsLevel.on("pointerdown", function(){
            this.stars[this.level] = Math.max(this.stars[this.level], 2);
            if(this.stars[this.level + 1] != undefined && this.stars[this.level + 1] == -1){
                this.stars[this.level + 1] = 0;
            }
            localStorage.setItem(gameOptions.localStorageName, this.stars.toString());
            this.scene.start("PlayGame");
        }, this);
        var threeStarsLevel = this.add.text(20, 360, "Get 3 stars", {
            font: "48px Arial",
            color: "#00ff00"
        });
        threeStarsLevel.setInteractive();
        threeStarsLevel.on("pointerdown", function(){
            this.stars[this.level] = 3;
            if(this.stars[this.level + 1] != undefined && this.stars[this.level + 1] == -1){
                this.stars[this.level + 1] = 0;
            }
            localStorage.setItem(gameOptions.localStorageName, this.stars.toString());
            this.scene.start("PlayGame");
        }, this);
    }
}
If you have questions, feel free to leave a comment or drop me a mail, the best ideas will be developed in next step, meanwhile download the source code.

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