Cocos2d-x HTML5: Refactoring your code to simulate Thread.Sleep(milliSeconds) with setTimeout
I've been working on a JavaScript/HTML5 port of Pocket Bombs in my spare time. After getting most of the core code implemented I set my sights on the most visible feature of the game: the chained explosions that result from critical mass bombs. When implementing this feature on other platforms it has been easy to 'slow down' the chain reaction using something like C#'s Thread.Sleep(ms). Unfortunately in JavaScript there is no such thing as Thread.Sleep.
The 'solution' that is provided by JavaScript is the setTimeout(func, ms) function which will wait for a number of milliseconds then execute the function that is passed into it. Using setTimeout requires a bit of refactoring. In this article I explain what I needed to do to successfully implement a small delay in JavaScript for my Cocos2d-x HTML5 game.
References:
- Cocos2d-x HTML5 [cocos2d-x.org]
- setTimeout [w3schools.com]
Background
When working in a traditional language like C, C++ and even Objective-C all allow you to pause thread execution for a period of time in a direct and simple way. For a game like Pocket Bombs, this comes in handy as I don't want all the explosive chain reactions to go off at once: I want to show them to the player sequentially so they can see how the chain reaction propagates
JavaScript lacks a good way to tell a thread to pause execution for a period of time. What it provides is a method called setTimeout which accepts 2 arguments: The function you want to run and the how much time you want to pass BEFORE running the function. Working in JavaScript (like when you use Cocos2d-x HTML5) requires you to use setTimeout to implement delayed actions.
I'd like to show an example of how to refactor code to work with setTimeout.
Original code
My chain reaction handling logic for the original iOS used a while loop and [NSThread sleepForTimeInterval:.125]. When writing the JS version I started with that as a base and came up with:
explosionHandler: function(){ // Handle the chain reactions on the gameboard // If this method is called there is at least ONE critical mass square var existCriticalMassSquaresOnGameBoard = true while(existCriticalMassSquaresOnGameBoard){ var existCriticalMassSquaresOnGameBoard = this.gameBoard.chainReactionHandler(); // Redraw the squares based on the current bomb count of each square for(var x = 0; x < this.gameBoard.x; x++){ for(var y = 0; y < this.gameBoard.y; y++){ var spriteFramRef; if(this.gameBoard.squares[x][y].bombs == 0){ spriteFrameRef = this.gameBoard.theme + "/0.png"; } else if(this.gameBoard.squares[x][y].bombs < =5){ spriteFrameRef = this.gameBoard.theme + "/"+ this.gameBoard.squares[x][y].owner + "_"+ this.gameBoard.squares[x][y].bombs + ".png" } else{ spriteFrameRef = this.gameBoard.theme + "/"+ this.gameBoard.squares[x][y].owner + "_plus.png" } var frame = new cc.SpriteFrameCache.getInstance().getSpriteFrame(spriteFrameRef); this.gameBoard.squares[x][y].sprite.setDisplayFrame(frame); } } // Force Cocos2d-x HTML5 to redraw the screen cc.Director.getInstance().drawScene(); // Time delay here. How do I do this synchronously in JavaScript? // I Couldn't find anything that would work synchronously so // It looks like I need to refactor to use setTimeout :/ } }
Code after refactoring for setTimeout
In order to change the while loop to a recursive function I had to add a new function that is recursive. This resulted in the addition of a method that can be called by itself to perform the necessary work. Here is the new code segment:
explosionHandler: function(){ // Primary entry point for handling critical mass bomb squares // Set the explosion handler loop in motion this.handleExplosions(true, this); }, handleExplosions: function(continueExploding, thisScreen){ // Recursive function added so I can time-delay // If there are no squares which have a critical mass of bombs, exit if(!continueExploding){ return; } // ---If there are explosions to do, handle them--- // Talk to the Back-end gambeboard logic and have it handle the first set of reactions var existCriticalMassSquaresOnGameBoard = this.gameBoard.chainReactionHandler(); // Redraw the squares based on the current bomb count of each square for(var x = 0; x < this.gameBoard.x; x++){ for(var y = 0; y < this.gameBoard.y; y++){ var spriteFramRef; if(this.gameBoard.squares[x][y].bombs == 0){ spriteFrameRef = this.gameBoard.theme + "/0.png"; } else if(this.gameBoard.squares[x][y].bombs < =5){ spriteFrameRef = this.gameBoard.theme + "/"+ this.gameBoard.squares[x][y].owner + "_"+ this.gameBoard.squares[x][y].bombs + ".png" } else{ spriteFrameRef = this.gameBoard.theme + "/"+ this.gameBoard.squares[x][y].owner + "_plus.png" } var frame = new cc.SpriteFrameCache.getInstance().getSpriteFrame(spriteFrameRef); this.gameBoard.squares[x][y].sprite.setDisplayFrame(frame); } } // Force Cocos2d-x HTML5 to redraw the screen cc.Director.getInstance().drawScene(); // Time-delay before kicking off next set of chain reactions
setTimeout(function(){
// the reference to 'thisScreen' is necessary since the value of 'this' changes when setTimeout runs
thisScreen.handleExplosions(existCriticalMassSquaresOnGameBoard, thisScreen); }, 125); }
Complete Cocos2d-x HTML5 Layer implemented with the recursive method
To keep things in context, here is the code in conjunction with the Layer it lives on:
GameUILayer = cc.Layer.extend({ init: function(){ this._super(); // A bunch of application logic that isn't important for this example return true; }, onEnter: function(){ this._super(); // Required to process touch events }, gameBoard: null, // Keep a reference to gameBoard onTouchesEnded: function(pTouch,pEvent){ // Handle touch events for squares // Application logic to determine which square was touched by the user // Kick off the Explosion handler function if there are any critical mass squares this.explosionHandler(); }, explosionHandler: function(){ // Primary entry point for handling critical mass bomb squares // Set the explosion handler loop in motion this.handleExplosions(true, this); }, handleExplosions: function(continueExploding, thisScreen){ // Recursive function added so I can time-delay // If there are no squares which have a critical mass of bombs, exit if(!continueExploding){ return; } // ---If there are explosions to do, handle them--- // Talk to the Back-end gambeboard logic and have it handle the first set of reactions var existCriticalMassSquaresOnGameBoard = this.gameBoard.chainReactionHandler(); // Redraw the squares based on the current bomb count of each square for(var x = 0; x < this.gameBoard.x; x++){ for(var y = 0; y < this.gameBoard.y; y++){ var spriteFramRef; if(this.gameBoard.squares[x][y].bombs == 0){ spriteFrameRef = this.gameBoard.theme + "/0.png"; } else if(this.gameBoard.squares[x][y].bombs < =5){ spriteFrameRef = this.gameBoard.theme + "/"+ this.gameBoard.squares[x][y].owner + "_"+ this.gameBoard.squares[x][y].bombs + ".png" } else{ spriteFrameRef = this.gameBoard.theme + "/"+ this.gameBoard.squares[x][y].owner + "_plus.png" } var frame = new cc.SpriteFrameCache.getInstance().getSpriteFrame(spriteFrameRef); this.gameBoard.squares[x][y].sprite.setDisplayFrame(frame); } } // Force Cocos2d-x HTML5 to redraw the screen cc.Director.getInstance().drawScene(); // Time-delay before kicking off next set of chain reactions
setTimeout(function(){
// the reference to 'thisScreen' is necessary since the value of 'this' changes when setTimeout runs
thisScreen.handleExplosions(existCriticalMassSquaresOnGameBoard, thisScreen); }, 125); } });
Final Word
Working in JavaScript can be frustrating sometimes. While I enjoy its lightweight objects and have gotten used to its Prototype based inheritance, it doesn't seem to have grown much as a language in the last 18 years. Whole applications are being written in JavaScript despite its limitations. The lack of a simple Thread.Sleep() API is one example of how JavaScript has stayed true to its web browser based, UI Scripting roots.
Frameworks like Cocos2d help smooth over web technologies rough edges. Once I had the boilerplate Cocos code up and running I was able to implement new game functionality quickly and easily.