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.