Handling of Moving Tweens in a Responsive Game in Phaser

If you went through previous articles on making of a responsive game in Phaser, we handled scaling and placement of individual elements according to the available width and height for the game. I moved on to writing next responsive game in Phaser and came across another scenario which requires additional handling. In Number Spread game we did not use any tween to move elements between two points. In my next game I planned to create a domino game where effects like dealing domino tiles to the players were required to be handled. While aligning and scaling of the elements were done exactly how it was done in Number Spread game, additional scenario which I needed to handle was managing the tweens which were used to deal domino tiles to the players.

Consider the scenario where I made a chained tweens set to deal domino tiles to the players. I resized my game halfway through the dealing of tiles. At this point my RESIZE method was called and I could now handle whatever was required to be done and I had additional work to do since the tweens set I created for dealing the tiles had not yet finished and their target positions (x,y co-ordinates) were calculated according to the previous available width and height. Now I needed to handle two things here; First, Move and scale tiles which are already dealt; Second, scale and modify the tiles which are yet not dealt. We know from Number Spread game articles how to take care of the first. The second one needs handling of two things – Scaling of tiles as well as modification of the target position of those tiles in the tweens itself so that tweens know how to deliver the remaining tiles to the correct new positions. For that purpose we would need to keep a temporary reference of current running tweens in our game. In RESIZE method we will pause the running tweens, change their target positions and then resume the tweens again.

As we did in the previous game, we will first divide game screen into various parts to place different elements. We are going to again use different layout for portrait and landscape orientations. See below for the layout I decided for the game (layout is posted just to give you a better understanding of various elements’ positioning when you are going through the code)

First I am going to create some classes for handling dominoes and decks in my game. These classes would be used and extended later on in the game. Currently it is the bare minimum code to handle a deck of dominoes.

// ---------------Domino---------------------
function Domino(rank1, rank2, frame, counter) {
    // rank2 is always equal to or greater than rank1
    this.rank1 = rank1;
    this.rank2 = rank2;
    this.frame = frame;
    this.counter = counter;
};
// ---------------Deck---------------------
function Deck(noOfDecks, game, left, top, properties) {
    this.dominoes = [];
    this.game = game;
    this.dominoSpacerX = 0;
    this.dominoSpacerY = 0;
    this.left = left;
    this.top = top;
    this.noOfDecks = noOfDecks ? noOfDecks : 0;
    this.display = false;

    if (properties) {
        this.properties = properties;
        if (this.properties.anchor)
            this.anchor = { x: this.properties.anchor.x, y: this.properties.anchor.y };

        if (this.properties.frame)
            this.frame = this.properties.frame;
    }

    if (this.noOfDecks > 0)
        this.initDominoes();
};

Deck.prototype.initDominoes = function () {
    var counter = 0;
    for (var i = 0; i < this.noOfDecks; i++) {
        for (var rank1 = 0; rank1 <= 6; rank1++) {
            for (var rank2 = rank1; rank2 <= 6; rank2++) {
                counter++;
                this.dominoes.push(new Domino(rank1, rank2, (counter - 1), counter));
            }
        }
    }
};

Deck.prototype.shuffle = function (count) {
    for (var i = 0; i < count; i++) {
        shuffle(this.dominoes);
    }
};

Deck.prototype.draw = function (toDeck, count) {
    for (var i = 0; i < count; i++) {
        var domino = this.dominoes.pop();
        toDeck.addDomino(domino);
    }
};

Deck.prototype.addDomino = function (domino) {
    this.dominoes.push(domino);
};

Deck.prototype.getPositionX = function (index, leftMargin, tileScale, tileMargin) {
    var scale = tileScale ? tileScale: 1;
    var localMargin = (leftMargin && this.dominoSpacerX > 0) ? leftMargin : 0;
    var localTileMargin = (tileMargin && this.dominoSpacerX > 0) ? tileMargin : 0;
	return (this.left + index * this.dominoSpacerX * scale - localMargin + index * localTileMargin + this.dominoSpacerX * scale / 2);
};

Deck.prototype.getPositionY = function (index) {
    return (this.top + index * this.dominoSpacerY);
};

function shuffle(array) {
    var i = 0
      , j = 0
      , temp = null;

    for (i = array.length - 1; i > 0; i -= 1) {
        j = Math.floor(Math.random() * (i + 1));
        temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

Now let us look at the game code

var TheGame = {
};

TheGame.Params = {
	baseWidth: 1920,
	baseHeight: 1080,
	marginMultiplier: 0.9,
	widthMultiplerPortrait: 0.9,
    widthMultiplerLandScape: 0.8,
    maxTiles: 7,
	minPadding: 50,
	backFrame: 29,
    blankFrame: 28,
	tileSize: {
		width: 50,
		height: 98
	}
};

TheGame.Boot = function (game) { };

TheGame.Boot.prototype =  {
    init: function () {
        this.scale.scaleMode = Phaser.ScaleManager.RESIZE;
    },
    preload: function () {
        this.load.image("loading", "loadingback.png");
    },
    create: function () {
        this.state.start("Loading");
    }	
};

TheGame.Loading = function (game) {
};

TheGame.Loading.prototype = {
    init: function () {
    },
    preload: function () {
        var loadingBar = this.add.sprite(this.world.centerX, this.world.centerY, "loading");
        loadingBar.anchor.setTo(0.5);
        this.load.setPreloadSprite(loadingBar);

        this.load.spritesheet("dominoes", "dominoes_2.png", 50, 98);
        this.load.image("reload", "reload.png");

    },
    create: function () {
       this.state.start("TheGame");
    }
};


TheGame.MyGame = function (game) {
};

TheGame.MyGame.prototype = {
    preload: function () {
    },
    create: function () {
        this.isMoving = false;
        this.group = this.game.add.group();
        this.dominoesMap = {};
 
		// Dealer Deck
        this.dealer = new Deck(1, this.game, -200, -200, { anchor: { x: 0.5, y: 0.5 } });
        this.dealer.shuffle(5);

        // Player 1 Deck
        this.player1Deck = new Deck(0, -200, -200, { anchor: { x: 0.5, y: 0.5 } });
        this.player1Deck.dominoSpacerX = TheGame.Params.tileSize.width;
        this.player1Deck.display = true;

        // Player 2 Deck
        this.player2Deck = new Deck(0, -200, -200, { anchor: { x: 0.5, y: 0.5 } });
        this.player2Deck.frame = TheGame.Params.blankFrame;
        this.player2Deck.dominoSpacerX = TheGame.Params.tileSize.width;
        this.player2Deck.display = false;

        this.reloadButton = this.game.add.button(-200, -200, "reload", this.reload, this);
        this.reloadButton.anchor.setTo(0.5);
        this.reloadButton.visible = false;

        this.initDominoControls(this.dealer); // Initialize Domino Controls

        this.positionControls(this.game.width, this.game.height);
		
	    // Wait before dealing domino tiles
        this.game.time.events.add(1000, this.actuateRowDecks, this);
    },
    initDominoControls: function (deck) {
        var dominoes = deck.dominoes;
        var count = dominoes.length;
        for (var i = 0; i < count; i++) {
            var control = this.game.add.sprite(this.dealer.left, this.dealer.top, "dominoes");
            control.frame = TheGame.Params.backFrame;
            control.domino = dominoes[i];
            this.game.physics.arcade.enable(control);
            control.anchor.setTo(0.5, 0.5);
            this.applyScale(control, this.tilesScale);
            this.group.add(control);
            this.dominoesMap[dominoes[i].counter] = control;
        }
    },
    updateTweenXY: function (tween, targetObject, startPosition, endPosition) {
        //begin at current tween position
        tween.timeline[0].vStart = { x: startPosition.x, y: startPosition.y };
        //update the duration of the tween so that it will not overrun.
        tween.timeline[0].duration = tween.timeline[0].duration - tween.timeline[0].dt;
        //set the new tween condition
        tween.timeline[0].vEnd = { x: endPosition.x, y: endPosition.y };
        //reset the dt, if you reset dt it is the same as rewinding your tween to the beginning
        tween.timeline[0].dt = 0;
    },
    positionControls: function (width, height) {
        // pause all running tweens which are moving a sprite
        if (this.currentTweens) {
            for (var i = 0, count = this.currentTweens.length; i < count; i++) {
                if (!this.currentTweens[i].isMoveComplete) {
                    this.currentTweens[i].pause();
                }
            }
        }
        // We would consider landscape orientation if height to width ratio is less than 1.0.
        // Pick any value you like or a preference for landscape or portrait orientation
        var isLandscape = this.isLandscapeOrientation(width, height);
        this.calculateOverallScale(width, height);

        if (isLandscape) {
            // 1x 4x 1x
            // Dealer Deck
            this.dealer.left = width - width * 0.125; // 25% Space
            this.dealer.top = height / 12; // 1x Top 

            // Player 1 Deck
            this.player1Deck.left = this.world.centerX;
            this.player1Deck.top = height - height / 12; // 1x Bottom

            // Player 2 Deck
            this.player2Deck.left = this.world.centerX;
            this.player2Deck.top = height / 12; // 1x Top

            this.scaleSprite(this.reloadButton, width * 0.2, height / 6, 10, 0.5, true);
            this.reloadButton.x = width - width * 0.125;
            this.reloadButton.y = height - height / 12;

            this.positionPlayerDominoes();
            
        } else {
            // 1x 1x 6x 1x 1x
            // Dealer Deck
            this.dealer.left = width * 0.2 / 2;
            this.dealer.top = height / 20;

            // Player 1 Deck
            this.player1Deck.left = this.world.centerX;
            this.player1Deck.top = height - height / 10 - height / 20;

            // Player 2 Deck
            this.player2Deck.left = this.world.centerX;
            this.player2Deck.top = height / 10 + height / 20;

            this.scaleSprite(this.reloadButton, width * 0.2, height / 10, 10, 0.8, true);
            this.reloadButton.x = width - width * 0.2 / 2;
            this.reloadButton.y = height / 20;

            this.positionPlayerDominoes();
        }
    },
    positionPlayerDominoes: function () {
        for (var i = 0, dealerCount = this.dealer.dominoes.length; i < dealerCount; i++) {
            var control = this.getControlAt(this.dealer, i);
            this.applyScale(control, this.tilesScale);
            var positionX = this.dealer.getPositionX(i, 0, this.tilesScale, this.calculatedTileMargin);
            var positionY = this.dealer.getPositionY(i);
            this.setControlPosition(control, positionX, positionY);
        }

        if (this.currentTweens) {
            for (var i = 0, count = this.currentTweens.length; i < count; i++) {
                var deck = this.currentTweens[i].deck;
                var index = this.currentTweens[i].index;
                var margin = this.calculatedTileWidth * deck.dominoes.length / 2 + deck.dominoes.length * this.calculatedTileMargin / 2;
                var control = this.getControlAt(deck, index);
                this.applyScale(control, this.tilesScale);
                var positionX = deck.getPositionX(index, margin, this.tilesScale, this.calculatedTileMargin);
                var positionY = deck.getPositionY(index);

                if (this.currentTweens[i].isMoveComplete) {
                    this.setControlPosition(control, positionX, positionY);
                } else {
                    if (!this.currentTweens[i].isRunning)
                        this.setControlPosition(control, this.dealer.left, this.dealer.top);
                    this.updateTweenXY(this.currentTweens[i], control, {x: this.dealer.left, y: this.dealer.top}, { x: positionX, y: positionY });
                    this.currentTweens[i].resume();
                }
            }
        } 
        else {
            var player1Count = this.player1Deck.dominoes.length;
            var player1Margin = this.calculatedTileWidth * player1Count / 2 + player1Count * this.calculatedTileMargin / 2;
            for (var i = 0; i < player1Count; i++) {
                var control = this.getControlAt(this.player1Deck, i);
                this.applyScale(control, this.tilesScale);
                var positionX = this.player1Deck.getPositionX(i, player1Margin, this.tilesScale, this.calculatedTileMargin);
                var positionY = this.player1Deck.getPositionY(i);
                this.setControlPosition(control, positionX, positionY);
            }

            var player2Count = this.player2Deck.dominoes.length;
            var player2Margin = this.calculatedTileWidth * player2Count / 2 + player2Count * this.calculatedTileMargin / 2;
            for (var i = 0; i < this.player2Deck.dominoes.length; i++) {
                var control = this.getControlAt(this.player2Deck, i);
                this.applyScale(control, this.tilesScale);
                var positionX = this.player2Deck.getPositionX(i, player2Margin, this.tilesScale, this.calculatedTileMargin);
                var positionY = this.player2Deck.getPositionY(i);
                this.setControlPosition(control, positionX, positionY);
            }
        }
    },
    actuateRowDecks: function () {
        this.isMoving = true;
        var tweens = [];
        var tilesCount = 7;
        var margin = tilesCount * this.calculatedTileWidth / 2 + tilesCount * this.calculatedTileMargin / 2;
        for (var i = 0; i < tilesCount; i++) {
            this.dealer.draw(this.player1Deck, 1);
            var tween1 = this.addTweenForLastControlMove(this.player1Deck, 30, 300, margin, this.tilesScale);
            tween1.deck = this.player1Deck;
            tween1.index = i;
            tweens.push(tween1);

            this.dealer.draw(this.player2Deck, 1);
            var tween2 = this.addTweenForLastControlMove(this.player2Deck, 30, 300, margin, this.tilesScale);
            tween2.deck = this.player2Deck;
            tween2.index = i;
            tweens.push(tween2);
        }

        var tweensCount = tweens.length;
        if (tweensCount > 1) {
            var tween = tweens[0];
            for (var i = 1; i < tweensCount; i++) {
                tween.chain(tweens[i]);
                tween = tweens[i];
            }
        }

        this.currentTweens = tweens;

        tweens[tweens.length - 1].onComplete.addOnce(function () {
            this.currentTweens = null;
			// TODO - Processing after dealing the tiles
            this.reloadButton.visible = true;

            this.isMoving = false;
        }, this);
        tweens[0].start();
    },
	calculateOverallScale: function (width, height) {
	    var isLandscape = this.isLandscapeOrientation(width, height);
		if(isLandscape){
			// In landscape mode height = 4x for play area + 1x for player + 1x for bot 
		    var availableHeightPerDomino = (height / 6) * TheGame.Params.marginMultiplier; //
		    var availableWidthPerDomino = (width * 0.5 / TheGame.Params.maxTiles) * TheGame.Params.marginMultiplier; // 50%

			var baseAspectRatio = TheGame.Params.baseWidth / TheGame.Params.baseHeight;
			var currentAspectRatio = width / height;
			
			if(currentAspectRatio > baseAspectRatio){
			    this.overallScale = height / TheGame.Params.baseHeight;
			} else {
			    this.overallScale = width / TheGame.Params.baseWidth;
			}
			this.tilesScale = this.getSpriteScale(TheGame.Params.tileSize.width, TheGame.Params.tileSize.height, availableWidthPerDomino, availableHeightPerDomino, 0, false);
			this.calculatedTileWidth = (TheGame.Params.tileSize.width) * this.tilesScale;
			this.calculatedTileHeight = (TheGame.Params.tileSize.height) * this.tilesScale;
			this.calculatedTileMargin = (1 - TheGame.Params.marginMultiplier) * width / TheGame.Params.maxTiles;
		} else {

		    // In portrait mode height = 1x for score tile + 1x for player domino + 6x for play area + 1x for bot domino + 1x for bot score 

			var availableHeightPerDomino = (height / 10) * TheGame.Params.marginMultiplier; //
			var availableWidthPerDomino = (width / TheGame.Params.maxTiles) * TheGame.Params.marginMultiplier; // 100% of width
			var baseAspectRatio = TheGame.Params.baseWidth / TheGame.Params.baseHeight; // For portrait width and height is switched
			var currentAspectRatio = height / width;
			
			if(currentAspectRatio > baseAspectRatio){
				this.overallScale = width / TheGame.Params.baseWidth;
			} else {
				this.overallScale = height / TheGame.Params.baseHeight;
			}
			this.tilesScale = this.getSpriteScale(TheGame.Params.tileSize.width, TheGame.Params.tileSize.height, availableWidthPerDomino, availableHeightPerDomino, 0, true);
			this.calculatedTileWidth = (TheGame.Params.tileSize.width) * this.tilesScale;
			this.calculatedTileHeight = (TheGame.Params.tileSize.height) * this.tilesScale;
			this.calculatedTileMargin = (1 - TheGame.Params.marginMultiplier) * width / TheGame.Params.maxTiles;
		}
	},
	isLandscapeOrientation: function (width, height) {
	    return height / width < 1.0 ? true : false;
	},
    applyScale: function (sprite, scale) {
        sprite.scale.x = scale;
        sprite.scale.y = scale;
    },
    scaleSprite: function (sprite, availableSpaceWidth, availableSpaceHeight, padding, scaleMultiplier, isFullScale) {
        var scale = this.getSpriteScale(sprite._frame.width, sprite._frame.height, availableSpaceWidth, availableSpaceHeight, padding, isFullScale);
        sprite.scale.x = scale * scaleMultiplier;
        sprite.scale.y = scale * scaleMultiplier;
    },
    getSpriteScale: function (spriteWidth, spriteHeight, availableSpaceWidth, availableSpaceHeight, minPadding, isFullScale) {
        var ratio = 1;
        var currentDevicePixelRatio = window.devicePixelRatio;
        // Sprite needs to fit in either width or height
        var widthRatio = (spriteWidth * currentDevicePixelRatio + 2 * minPadding) / availableSpaceWidth;
        var heightRatio = (spriteHeight * currentDevicePixelRatio + 2 * minPadding) / availableSpaceHeight;
        if (widthRatio > 1 || heightRatio > 1) {
            ratio = 1 / Math.max(widthRatio, heightRatio);
        } else {
            if (isFullScale)
                ratio = 1 / Math.max(widthRatio, heightRatio);
        }
        return ratio * currentDevicePixelRatio;
    },
    setControlPosition: function (control, positionX, positionY) {
        control.x = positionX;
        control.y = positionY;
    },
    addTweenForMove: function (control, deck, index, delay, duration, margin, scale) {
        var positionX = deck.getPositionX(index, margin, this.tilesScale, this.calculatedTileMargin);
        var positionY = deck.getPositionY(index);
        var period = 300;
        if (duration)
            period = duration;
        var tween = this.game.add.tween(control);
        tween.isMoveComplete = false;
        tween.to({ x: positionX, y: positionY }, period, Phaser.Easing.Linear.None, false, delay);
        tween.onStart.addOnce(function (sprite, tween) {
            sprite.bringToTop(mygame.group);
            if (tween.deck.display)
                sprite.frame = sprite.domino.frame;
            else
                sprite.frame = TheGame.Params.blankFrame;

        }, this);
        tween.onComplete.addOnce(function (sprite, tween) {
            tween.isMoveComplete = true;
        }, this);
        return tween;
    },
    addTweenForLastControlMove: function (deck, delay, duration, margin, scale) {
        var index = deck.dominoes.length - 1;
        var control = this.getControlAt(deck, index);
        return this.addTweenForMove(control, deck, index, delay, duration, margin, scale);
    },
    getControlAt: function (deck, index) {
        return this.dominoesMap[deck.dominoes[index].counter];
    },
    getControl: function (domino) {
        return this.dominoesMap[domino.counter];
    },
	resize: function (width, height) {
		this.positionControls(width, height);
	},
	reload: function () {
	    this.state.start("TheGame");
	}
};

var mygame;
window.onload = function () {
	mygame = new Phaser.Game(TheGame.Params.baseWidth, TheGame.Params.baseHeight, Phaser.AUTO);	
	mygame.state.add("Boot", TheGame.Boot);
	mygame.state.add("Loading", TheGame.Loading);
	mygame.state.add("TheGame", TheGame.MyGame);
	mygame.state.start("Boot");
}

The code used to handle tweens on resize is highlighted above. Rest of the resize code is similar to how it was done in the previous Number Spread game article.

Resize the screen when checking the code below or open the demo link on your device by clicking here

UPDATE – I finished whole game which can be played here. Check out the game on your device.


Leave A Comment

Your email address will not be published.