“Spellfall” HTML5 prototype made with Phaser updated to 2.8.2 and reusing tweens to improve performance

Read all posts about "" game
With the update of Phaser CE to 2.8.2, I am updating a 3 years old prototype – Spellfall – to the latest Phaser version, adding more comments to the source code and above all reusing tweens – actually, reusing THE tween – to save memory, CPU and improve performance. Rather than creating a new tween at every player move, we create only one tween and update its target property according to the tile to be moved. Also, a new to method is called each time to update tween destination. Finally, to prevent multiple to calls to create “waypoints” before the final tween destination, we have to reset its timeline property to an empty array. This is very important, as I see people asking how to “reset” a tween destination in various threads and forums. Well, just set timeline property to an empty array. Let’s have a look at the result:
Just drag a tile over another tile to swap them. Yes, this is the simplest approach to a “match 3” game design. And now, the source code with all required comments:
// the game itself
var game;

// global variable containing all game options
var gameOptions = {

    // width of the game, in pixels
    gameWidth: 300,

    // height of the game, in pixels
    gameHeight: 300,

    // size of each game tile, in pixels
    tileSize: 50,

    // number of tiles per row/column
    fieldSize: 6,

    // different kind of tiles in game
    tileTypes: 6,

    // zoom ratio to be applied on a tile when the player picks it up
    pickedZoom: 1.1
}
window.onload = function() {
    game = new Phaser.Game(gameOptions.gameWidth, gameOptions.gameHeight);
    game.state.add("PreloadGame", preloadGame);
    game.state.add("PlayGame", playGame);
    game.state.start("PreloadGame");
}
var preloadGame = function(game){}
preloadGame.prototype = {
    preload: function(){

        // these four lines will make the game run at the maximum scale allowed, while keeping it centered in the browser window
        game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
        game.scale.pageAlignHorizontally = true;
        game.scale.pageAlignVertically = true;
        game.stage.disableVisibilityChange = true;

        // loading the spritesheet containing all tiles
        game.load.spritesheet("tiles", "tiles.png", gameOptions.tileSize, gameOptions.tileSize);
    },
    create: function(){
        game.state.start("PlayGame");
    }
}
var playGame = function(game){}
playGame.prototype = {
    create: function(){

        // the tween we will use and recycle through the entire prototype
        this.tileTween = game.add.tween();

        // once the tween is completed update tileArray array and wait for input again
        this.tileTween.onComplete.add(function(){
            this.tileGroup.add(this.tileArray[this.landingRow][this.landingCol]);
            game.input.onDown.add(this.pickTile, this);
            var temp = this.tileArray[this.landingRow][this.landingCol];
            this.tileArray[this.landingRow][this.landingCol] = this.tileArray[this.movingRow][this.movingCol];
            this.tileArray[this.movingRow][this.movingCol] = temp;
        }, this);

        // dragging a tile is not allowed at the moment
        this.dragging = false;

        // tileArray is the two dimension array which will contain all tiles
        this.tileArray = [];

        // adding groups. movingTileGroup needs to be above tileGroup so moving tiles
        // will always have an higher z index and will always stay on top of the game
        this.tileGroup = game.add.group();
        this.movingTileGroup = game.add.group();

        // placing all tiles on stage
        for(var i = 0; i < gameOptions.fieldSize; i++){
    		this.tileArray[i] = [];
    		for(var j = 0; j < gameOptions.fieldSize; j++){

                // tossing a random number between 0 and tileTypes - 1, included
    			var randomTile = game.rnd.integerInRange(0, gameOptions.tileTypes - 1);

                // creation of the tile itself
                var theTile = game.add.sprite(j * gameOptions.tileSize + gameOptions.tileSize / 2, i * gameOptions.tileSize + gameOptions.tileSize / 2, "tiles", randomTile);

                // setting tile registration point at its center
                theTile.anchor.setTo(0.5);

                // adding tile to tileArray
                this.tileArray[i][j] = theTile;

                // adding tile to tileGroup
                this.tileGroup.add(theTile);
    		}
		}

        // waiting for player input
        game.input.onDown.add(this.pickTile, this);
    },
    pickTile: function(e){

        // saving input coordinates
		this.startX = e.position.x;
		this.startY = e.position.y;

		// retrieving picked row and column
		this.movingRow = Math.floor(this.startY / gameOptions.tileSize);
		this.movingCol = Math.floor(this.startX / gameOptions.tileSize);

        // moving the tile to the upper group, so it will surely be at top of the stage
		this.movingTileGroup.add(this.tileArray[this.movingRow][this.movingCol]);

        // zooming the tile
		this.tileArray[this.movingRow][this.movingCol].width = gameOptions.tileSize * gameOptions.pickedZoom;
		this.tileArray[this.movingRow][this.movingCol].height = gameOptions.tileSize * gameOptions.pickedZoom;

        // now dragging is allowed
		this.dragging = true;

        // updating listeners
		game.input.onDown.remove(this.pickTile, this);
        game.input.onUp.add(this.releaseTile, this);

    },
    releaseTile: function(){

        // removing the listener
        game.input.onUp.remove(this.releaseTile, this);

        // returning the tile to its originary group
        this.tileGroup.add(this.tileArray[this.movingRow][this.movingCol]);

        // determining landing row and column
        this.landingRow = Math.floor(this.tileArray[this.movingRow][this.movingCol].y / gameOptions.tileSize);
        this.landingCol = Math.floor(this.tileArray[this.movingRow][this.movingCol].x / gameOptions.tileSize);

        // resetting the moving tile to its original size
        this.tileArray[this.movingRow][this.movingCol].width = gameOptions.tileSize;
        this.tileArray[this.movingRow][this.movingCol].height = gameOptions.tileSize;

        // swapping tiles, both visually and in tileArray array...
        this.tileArray[this.movingRow][this.movingCol].x = this.landingCol * gameOptions.tileSize + gameOptions.tileSize / 2;
        this.tileArray[this.movingRow][this.movingCol].y = this.landingRow * gameOptions.tileSize + gameOptions.tileSize / 2;

        // ...but only if there moving and landing tiles are different!!
        if(this.movingRow != this.landingRow || this.movingCol != this.landingCol){

            // placing the tile to move on the upper group
        	this.movingTileGroup.add(this.tileArray[this.landingRow][this.landingCol]);

            // destination tile will move to start tile with a tween
        	this.tileTween.target = this.tileArray[this.landingRow][this.landingCol];

            // important!! We need to reset the timeline to prevent to create more and more waypoints
            this.tileTween.timeline = [];

            // setting tween destination
            this.tileTween.to({
                x: this.movingCol * gameOptions.tileSize + gameOptions.tileSize / 2,
        		y: this.movingRow * gameOptions.tileSize + gameOptions.tileSize / 2
        	}, 800, Phaser.Easing.Cubic.Out);
            this.tileTween.start();
        }

        // else just let the player be able to swap another tile
        else {
            game.input.onDown.add(this.pickTile, this);
        }

        // we aren't dragging anymore
        this.dragging = false;
    },
    update: function(){
        if(this.dragging){

            // checking x and y distance from starting to current input location
			var distX = game.input.worldX - this.startX;
            var distY = game.input.worldY - this.startY;

            // updating tile position
            this.tileArray[this.movingRow][this.movingCol].x = this.movingCol * gameOptions.tileSize + gameOptions.tileSize / 2 + distX;
            this.tileArray[this.movingRow][this.movingCol].y = this.movingRow * gameOptions.tileSize + gameOptions.tileSize / 2 + distY;
		}
    }
}

Using only one tween for the entire prototype is a great improvement as it will save Phaser to manage garbage collection, resulting in an overall performance improvement as well as a good practice. Next time I will add more animations, meanwhile you can 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