Monday, October 5, 2015

Snake Game Using Cocos2d-x Javascript iOS- PART 7

First small teaser , this is the complete game played by my kids



As you may notice in the previous tutorial we had no touch events to control the snake , as you Remember in the SnakeJS Snake Game Using Cocos2d-x HTML5 - PART 3   
In the code Lines 72 - 95 there is only keyboard handling . lts change that .
The controllers i chose for this tutorial will be buttons to mimic the keyboard's arrow keys.
I aware there are a lot of debate on the topic. which is better swipe gestures or virtual keyboards.
But for the simplicity i will do it with buttons .
For the buttons i used the open source Vector Editor called 
Inkscape .
Couple of squares and i have 4 buttons to use , very simple very basic .






Or this color .. with round angles , Inkscape is really great vector editor .
For quick Inkscape tutorials  I strongly recommend you to visit this site: http://www.2dgameartguru.com/ 



So after we done with the controllers lts dive into our new game code .
The Code :




  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
//global varibales with default values 
var SnakeArray = null; 
var SnakeFood = null; 
var spriteBackGround = null; 
var BackGroundWidth = 0;;
var BackGroundHeight = 0;
var BackGroundPos;
var Direction ="";
var score = 0;  
var cellWidth = 30;
var Enum = { 
   snakecell:0,
   snakefood:1,
   background:2,
   touchup:3,
   touchdown:4,
   touchright:5,
   touchleft:6
   };
var snakeJSGameLayer = null;
var snakeLength = 5; //Length of the snake
var ScoreLabel = null;
var GameOverLabel = null;
var bCollisionDetected = false;
var spriteTouchUP = null;
var spriteTouchDown = null;
var spriteTouchLeft = null;
var spriteTouchRight = null;
var origin = null;
var winSize = null;
var SnakeJSGameLayer = null;

//The Background sprite class holding the game nodes
var SpriteBackGround = cc.Sprite.extend({
 ctor: function(texture, rect) {           
  this._super(texture, rect);
 }
});
//Snake cell class 
var SpriteSnakeCell = cc.Sprite.extend({
 ctor: function(texture) {           
  this._super(texture);
 }
});
//Snake food class 
var SpriteSnakeFood = cc.Sprite.extend({
 ctor: function(texture) {           
  this._super(texture);
 }
});

var SpriteTouch = cc.Sprite.extend({
 _listener:null,
    _fixedPriority:0,
    _removeListenerOnTouchEnded: false,

    ctor: function(priority,texture){
        this._super(texture);
        this._fixedPriority = priority || 0;
    },

    setPriority:function(fixedPriority){
        this._fixedPriority = fixedPriority;
    },

    onEnter:function(){
        this._super();

        var selfPointer = this;
        var listener = cc.EventListener.create({
            event: cc.EventListener.TOUCH_ONE_BY_ONE,
            swallowTouches: true,
            onTouchBegan: function (touch, event) {
                var locationInNode = selfPointer.convertToNodeSpace(touch.getLocation());
                var s = selfPointer.getContentSize();
                var rect = cc.rect(0, 0, s.width, s.height);
    var target = event.getCurrentTarget();
                if (cc.rectContainsPoint(rect, locationInNode)) {
                    selfPointer.setColor(cc.color.RED);            
                    if(bCollisionDetected)
     {
       bCollisionDetected = false;
       GameOverLabel.visible = false;
       cc.director.resume();
       snakeJSGameLayer.CreateSnake();
       snakeJSGameLayer.CreateFood();
       return true;      
     }
                    switch(selfPointer.getTag())
                    {
                     case Enum.touchdown:
                     {
                        if(direction != "up") direction = "down";
                      break;
                     }
                     case Enum.touchup:
                     {
                      if(direction != "down") direction = "up";
                      break;
                     }
                     case Enum.touchright:
                     {
                         if(direction != "left") direction = "right";
                      break;
                     }
                     case Enum.touchleft:
                     {
                         if(direction != "right") direction = "left";
                      break;
                     }
                    }
                    return true;
                }
                return false;
            },
            onTouchMoved: function (touch, event) {
                //this.setPosition(this.getPosition() + touch.getDelta());
            },
            onTouchEnded: function (touch, event) {
                selfPointer.setColor(cc.color.WHITE);
                if(selfPointer._removeListenerOnTouchEnded) {
                    cc.eventManager.removeListener(selfPointer._listener);
                    selfPointer._listener = null;
                }
            }
        });

        if(this._fixedPriority != 0)
            cc.eventManager.addListener(listener, this._fixedPriority);
        else
            cc.eventManager.addListener(listener, this);
        this._listener = listener;
    },

    onExit: function(){
        this._listener && cc.eventManager.removeListener(this._listener);
        this._super();
    },

    removeListenerOnTouchEnded: function(toRemove){
        this._removeListenerOnTouchEnded = toRemove;
    },

    getListener: function() {
        return this._listener;
    }
});

 
//Game Main container class, holding all game logic functions
SnakeJSGameLayer = cc.Layer.extend({
    sprite:null,
    ctor:function () {
      
       this._super();
       origin = cc.director.getVisibleOrigin();
       winSize = cc.winSize;
       //Create the background sprite 
       spriteBackGround = new SpriteBackGround(res.blank_png,
                                cc.rect(0,0,winSize.width-50,winSize.height-90));
       spriteBackGround.setAnchorPoint(0,0);
       spriteBackGround.setTag(Enum.background);
       BackGroundWidth = spriteBackGround.getBoundingBox().width;
       BackGroundHeight = spriteBackGround.getBoundingBox().height;
       //Calculate the background sprite positon by subtracting SpriteBackGround position from Main layer position 
       spriteBackGround.x = (winSize.width - BackGroundWidth)/2;
       spriteBackGround.y = (winSize.height - BackGroundHeight)/2;       
       this.addChild(spriteBackGround,1);
       BackGroundPos = {x:spriteBackGround.x, y:spriteBackGround.y};      
       //Add the score lable
       ScoreLabel = new cc.LabelTTF(setLabelString(score), "Arial", 38); 
       ScoreLabel.x = winSize.width / 2;
       ScoreLabel.y = winSize.height / 2 + 200;       
       this.addChild(ScoreLabel, 5); 
       //Add the game over lable as none visible 
       var redColor = cc.color(0, 0, 0);
       //GameOverLabel = new cc.LabelTTF("Game Over press SPACE to restart!", "Arial", 38); 
    GameOverLabel = new cc.LabelTTF("", "Arial", 30); 
       GameOverLabel.x = winSize.width / 2;
       GameOverLabel.y = winSize.height / 2;  
       GameOverLabel.fillStyle = redColor
       this.addChild(GameOverLabel, 5); 
       GameOverLabel.visible = false;
       this.SetLableText(3);
       //if (cc.sys.os == cc.sys.OS_IOS || cc.sys.os == cc.sys.OS_ANDROID) 
       {
    
         this.CreateTouchControllers();
       }
       //create the snake and the food 
       this.CreateSnake();
       this.CreateFood();
       
       if (cc.sys.os === cc.sys.OS_WINDOWS) 
       {
     
     if ('keyboard' in cc.sys.capabilities) {
    cc.eventManager.addListener({
     event: cc.EventListener.KEYBOARD,
     onKeyPressed: function (key, event) {
        var target = event.getCurrentTarget();
        //Only if space key is pressed and Collision Detected the game restart
        if(bCollisionDetected && key == "32")
        {
       bCollisionDetected = false;
       GameOverLabel.visible = false;
       cc.director.resume();
       target.CreateSnake();
       target.CreateFood();
       return;      
        }
        if(key == "37" && direction != "right") direction = "left";
        else if(key == "38" && direction != "down") direction = "up";
        else if(key == "39" && direction != "left") direction = "right";
        else if(key == "40" && direction != "up") direction = "down";
      } 
    }, this);
   } else {
    cc.log("KEYBOARD Not supported");
   }  
        }           
        //This is the main game loop
        this.schedule(this.GameLoop,0.2);
 
        return true;
    },
    CreateSnake:function() {
        score = 0;
        ScoreLabel.setString(setLabelString(score));
        direction = "right";
        if(( typeof SnakeArray != 'undefined' && SnakeArray instanceof Array ) && SnakeArray.length > 0 )
        {                
            for(var i = 0; i< SnakeArray.length; i++)             
            {
                    this.removeChild(SnakeArray[i],true);                   
            }
        } 
        SnakeArray = [];   
        var elmsToRemove = SnakeArray.length - snakeLength;
        if(elmsToRemove>1)
        {
            SnakeArray.splice(snakeLength-1,elmsToRemove);
        }
        for(var i = snakeLength-1; i>=0; i--)
        {
           var spriteSnakeCell = new SpriteSnakeCell(res.snakecell_png);
           spriteSnakeCell.setAnchorPoint(0,0);
           spriteSnakeCell.setTag(Enum.snakecell);          
           var xMov = (i*cellWidth)+BackGroundPos.x;
           var yMov = (spriteBackGround.y+BackGroundHeight)-cellWidth;
           spriteSnakeCell.x = xMov;
           spriteSnakeCell.y = yMov;
           this.addChild(spriteSnakeCell,2);           
           SnakeArray.push(spriteSnakeCell); 
        }
    },            
    CreateFood:function() {   
        //Check if food Exist , remove it from the game sprite
        if(this.getChildByTag(Enum.snakefood)!=null)
        {
            this.removeChildByTag(Enum.snakefood,true); 
        }       
        var spriteSnakeFood = new SpriteSnakeFood(res.snakefood_png);
        spriteSnakeFood.setAnchorPoint(0,0);
        spriteSnakeFood.setTag(Enum.snakefood);     
        this.addChild(spriteSnakeFood,2); 
        var rndValX = 0;
        var rndValY = 0;
        var min = 0; 
        var maxWidth = BackGroundWidth;
        var maxHeight = BackGroundHeight;
        var multiple = cellWidth;
        //Place it in some random position 
        rndValX = generate(min,maxWidth,multiple);
        rndValY = generate(min,maxHeight,multiple);
        var irndX = rndValX+BackGroundPos.x;
        var irndY = rndValY+BackGroundPos.y;
        SnakeFood = {
            x: irndX , 
            y: irndY  
        };
     
       spriteSnakeFood.x = SnakeFood.x; 
       spriteSnakeFood.y = SnakeFood.y;  
    },
    //The function will be called every 0.2 milii secound
    GameLoop:function (dt) {
        //get the snake head cell 
        var SnakeHeadX = SnakeArray[0].x;
        var SnakeHeadY = SnakeArray[0].y;
        //check which direction it is heading  
        switch(direction)
        {
            case "right":
                 SnakeHeadX+=cellWidth;
            break;
            case "left":
                 SnakeHeadX-=cellWidth;
            break;
            case "up":
                 SnakeHeadY+=cellWidth;
            break;
            case "down":
                  SnakeHeadY-=cellWidth;
            break;
            default:
                cc.log("direction not defind");
        }
        //Check if the snake head Collided with the walls or with it self 
        if(CollisionDetector(SnakeHeadX, SnakeHeadY, SnakeArray))
        {                 
                bCollisionDetected = true; 
                GameOverLabel.visible = true;
                cc.director.pause();
                return;
        }
        //Check if the snake head Collided with the food
        if(SnakeHeadX == SnakeFood.x && SnakeHeadY == SnakeFood.y)
        {
                //Add snake cell after the head position
                var spriteSnaketail = new SpriteSnakeCell(res.snakecell_png);
                spriteSnaketail.setAnchorPoint(0,0);
                spriteSnaketail.setTag(Enum.snakecell);
                this.addChild(spriteSnaketail,2);
                spriteSnaketail.x = SnakeHeadX;
                spriteSnaketail.y = SnakeHeadY;
                SnakeArray.unshift(spriteSnaketail);  
                //Add point to the points display
                ScoreLabel.setString(setLabelString(score++));
                //Create new food in new position
                this.CreateFood();         
        }
        else
        {       
                var spriteSnakeCellLast = SnakeArray.pop(); //pops out the last cell
                spriteSnakeCellLast.x = SnakeHeadX; 
                spriteSnakeCellLast.y = SnakeHeadY;
                SnakeArray.unshift(spriteSnakeCellLast);
        }
 
    },
 SetLableText:function(mode) {
  if(mode == 1)
  {
   GameOverLabel.setString("Game Over press SPACE to restart!");
  }
  else if(mode == 2)
  {
   GameOverLabel.setString("Game Over press any arrows to restart!");
  }
  else if(mode == 3)
  {
   GameOverLabel.setString("Game Over press any arrows or SPACE to restart!");
  }
 },
    CreateTouchControllers:function() {
     //create controllers   
    spriteTouchDown = new SpriteTouch(30,res.arrow_down);      
       spriteTouchDown.setAnchorPoint(0,0);
       spriteTouchDown.x = 0;
       spriteTouchDown.y = 0;
       spriteTouchDown.setTag(Enum.touchdown); 
        
       spriteTouchUP = new SpriteTouch(30,res.arrow_up);      
       spriteTouchUP.setAnchorPoint(0,0);
       spriteTouchUP.x = 0;
       spriteTouchUP.y = spriteTouchDown.getBoundingBox().height +20;
       spriteTouchUP.setTag(Enum.touchup); 
       
       spriteTouchRight = new SpriteTouch(30,res.arrow_right);      
       spriteTouchRight.setAnchorPoint(0,0);
       spriteTouchRight.x = winSize.width - spriteTouchDown.getBoundingBox().width -20;
       spriteTouchRight.y = 0;
       spriteTouchRight.setTag(Enum.touchright); 
       
       spriteTouchLeft = new SpriteTouch(30,res.arrow_left);      
       spriteTouchLeft.setAnchorPoint(0,0);
       spriteTouchLeft.x = spriteTouchRight.x - spriteTouchLeft.getBoundingBox().width-20;
       spriteTouchLeft.y = 0;
       spriteTouchLeft.setTag(Enum.touchleft); 
              
       this.addChild(spriteTouchUP,10);
       this.addChild(spriteTouchDown,10);
       this.addChild(spriteTouchLeft,10);
       this.addChild(spriteTouchRight,10);
    }  
});
//The snake Collision Obstacles are side walls  and itself
function CollisionDetector(snakeHeadX,snakeHeadY,snakeArray)
{
        if(snakeHeadX < spriteBackGround.x || 
                snakeHeadX > BackGroundWidth || 
                snakeHeadY < spriteBackGround.y || 
                snakeHeadY > ((spriteBackGround.y+BackGroundHeight)-cellWidth))
        {
            return true;
        }
        for(var i = 0; i < snakeArray.length; i++)
        {
                if(snakeArray[i].x == snakeHeadX && snakeArray[i].y == snakeHeadY)
                {
                 return true;
                }
        }
        return false;
}
//Rendom number generator 
function generate(min, max, multiple) 
{
    var res = Math.floor(Math.random() * ((max - min) / multiple)) * multiple + min;
    return res;
}
//Convert number to string
function setLabelString(str)
{
    var stringScore = parseInt(score).toString();
    return stringScore;   
}
//Main Game container 
var SnakeJSScene = cc.Scene.extend({
    onEnter:function () {
        this._super();
        snakeJSGameLayer = new SnakeJSGameLayer();
        this.addChild(snakeJSGameLayer);
    }
});



Lets go and examine the code it changed quite a lot. i will cover the main parts that has changed .

  1. Lines 15 - 19 added new enumeration keys to be set to our new controllers tags .
    Lines 25 - 28 added new variables that will hold the new controllers sprites.
  2. Lines 52 - 147 this is the Controller class with be created 4 times to represent our
    game left/right/top/down controllers.
    Lets break this class to parts :
    Lines 57 - 60 The class constructor it must get the priority, and the texture parameters
    The priority must be high so the sprite will be click and not the sprite below it.
    And the texture is the controller image we made in Inkscape.
    Line 66 , onEnter:function()  
    This function will be invoked each time user has touched the Controller sprite .
  3. Lines 80 - 88 this is where the collision is detected while we are controlling our snake movement . if detected reset the game if one of the controllers are clicked. 
  4. Lines 89 - 112 the switch case here will be invoked each touch . and according to
    the tag of the object it setting the directions.
  5. the rest of the "SpriteTouch" class is standard methods that we are not going to use.
    but they are needed to be there .
  6. Line 171 ScoreLabel now changed and no longer has static text , now it is determinate its text
    according to  setLabelString(scroe) function from Line 324 . 
  7. Line 188 this function constructing the controllers sprites 
    I comment the //if (cc.sys.os == cc.sys.OS_IOS || cc.sys.os == cc.sys.OS_ANDROID)
    This line means that you can control the appearance of the CreateTouchControllers() function.

    In this example i decided to enable the controllers no matter what OS the game running on  
  8. Lines 342 - 355 this is the function that was mentioned in section no' 6 Basclly we have 3 modes to display the "End Game" captions feel free to add more
    mode == 1 is when we are on the web or desktop
    mode ==2 is when  we are on any OS
    mode ==3 is when we are on Mobile or Desktop/Web
  9. Lines 356 - 386 this is the function that position the controllers on the game screen
    Here you can also position the them in any way you see it right .

That's It now we have 1 code base for Web , Desktop , iOS .

You can Check the source code and the controllers resource files in this GitHub repository:
https://github.com/meiry/SnakeJS-Final




Sunday, October 4, 2015

Snake Game Using Cocos2d-x Javascript iOS - PART 6

Now we come to the interesting part of this  tutorial series, we will take our game and compile it as iOS game .
For this Tutorial we need some prerequisites before we start .
  1. Mac. 
  2. iOS Device for testing . i strongly recommend to test the games on real device .
  3. Xcode 7, in this version you can test on your device without paying the 99$ .

1. We will use the same project we created in: Snake Game Using Cocos2d-x HTML5 - PART 1
Fill free to create the project again in your Mac and copy only the source files and the res directory.
The source files and the Xcode project files located in ( i create my projects in Projects directory) :
../cocos2d-x-3.8.1/Projects/SnakeJS/frameworks/runtime-src/proj.ios_mac/SnakeJS.xcodeproj


2. Click on SnakeJS.xcodeproj and if you installed Xcode 7 ( Remember 7 ...) it should load the project and you suppose to see this in Xcode :


3. Connect your iOS device into your MAC , Xcode should recognize it .
Then in Xcode go and set the active scheme to: SnakeJS-Mobile -> Your-IPhone-name




4. Lets hit "build" to see that every thing is fine by now ,  but before that we must set in Xcode :
  1. Set the deployment target to 7 and above in the General -> Deployment section.
  2. Also the current Cocos2d-x 8.1 have some kind of bug with bitecode set to Yes
    so lts change it to: No
    Go to Targets : SnakeJS->mobile then on the right screen
    Build Options ->Enable Bitcode = No
If you skip the above section Xcode will show you this error :
XCODE 7 and ENABLE_BITCODE=YES setting does not work

Now when all is set
Go to Product -> Build.
Wait few minutes , let the compilation to end .

5. If all went right you will see the game launching on your device and immediately start to play .
If you try to move it you will notice that none of the Touch controls are working
This is simply because in our code we don't have any touch handling .
We will write the code to handle this in next chapter .

We are ready to continue building our "SnakeJS" game for iOS Device in the next tutorial :
Snake Game Using Cocos2d-x Javascript iOS- PART 7

Play the final SnakeJS game :
http://meiry.github.io/SnakeJS-html5/publish/html5/
The SnakeJS source code:
https://github.com/meiry/SnakeJS-html5



Saturday, October 3, 2015

Snake Game Using Cocos2d-x Javascript Desktop - PART 5

Now that the game coding is done we can use the same code base almost without change for our game desktop version.
To run the main.js file which is the game entry point we need to compile Cocos2d-x RunTime which will load our game.

for this we use:
  1. Visual studio 2013 c++  community version . 
  2. The same code base we created in Snake Game Using Cocos2d-x HTML5 - PART 1 .


1.  Open VC++   then go to FILE -> Open -> Project/Solution . and navigate to where you created the SnakeJS project .  then navigate to deeper directory named:  proj.win32  :
 \cocos2d-x-3.8.1\Projects\SnakeJS\frameworks\runtime-src\proj.win32
And load the "SnakeJS" VC++ solution project called SnakeJS.sln .|



2. Once we done loading the Cocose2d-x Runtime source code into VC++ we ready to compile it.
we don't need Debug version for this example because for now we are not going to change any c++ source code ,
In VC++ :
  1. in the projects list on the right select our "SnakeJS" project . 
  2. go to the upper tool box there is drop down list with "Debug" selected . change it to "Release"




3. In VC++ which SnakeJS project selected go to and click on : BUILD -> Build SnakeJS
Wait until it is done to compile.




When it is done to compile. you should see in the VC++ bottom Output window  scroll to the end .


7>  4 File(s) copied
7>          1 file(s) copied.
7>          1 file(s) copied.
========== Build: 7 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========









4. Now we are ready to start our WindTakeFS game Navigate to :
cocos2d-x-3.8.1\Projects\SnakeJS\frameworks\runtime-src\proj.win32\Release.win32\
look for SnakeJS.exe click it to execute the game .


And your game should look like this :


We are ready to build our "SnakeJS" game for iOS the next tutorial :
Snake Game Using Cocos2d-x Javascript iOS- PART 6 


Play the final SnakeJS game:
http://meiry.github.io/SnakeJS-html5/publish/html5/
The SnakeJS source code:
https://github.com/meiry/SnakeJS-html5


Snake Game Using Cocos2d-x HTML5 - PART 4

In this final tutorial we going to minify all this gigantic cocos2d-x javascript + html framework
To 1 small JavaScript file.
To do this magic we should go back to when we setup the Cocos2d-x first.

1. When you run the python "setup.py" script it ask you to set the ANT_ROOT global variable value .as Shown here:





Once done , we are ready to minify our project , if you follow my tutorial :
Debug Cocos2d-x HTML5 Project using NetBeans IDE and Chrome browser in 10 easy steps.

2. So now it is the time to copy back the files to the cocos2d-x directory structure , in my pc it is under : cocos2d-x-3.8.1\cocos2d-x-3.8.1\Projects\SnakeJS




3. Open command line window in our SnakeJS root and type the following command :

1
cocos compile -p web -m release
4. The Final game should be in this directory :Projects\SnakeJS\publish\html5






 5. That's it ! you can run the game in your server using the good old index.html entry point .


We are ready to build our "SnakeJS" game for Windows desktop  the next tutorial :
Snake Game Using Cocos2d-x Javascript - PART 5 desktop

Play the final SnakeJS game :
http://meiry.github.io/SnakeJS-html5/publish/html5/
The SnakeJS source code:
https://github.com/meiry/SnakeJS-html5

Friday, October 2, 2015

Snake Game Using Cocos2d-x HTML5 - PART 3


Lets see the finished source code first and then i will explain the logic of each section in the code


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
//global varibales with default values 
var SnakeArray = null; 
var SnakeFood = null; 
var spriteBackGround = null; 
var BackGroundWidth = 0;;
var BackGroundHeight = 0;
var BackGroundPos;
var Direction ="";
var score = 0;  
var cellWidth = 30;
var Enum = {snakecell:0, snakefood:1 , background:2};
var snakeJSGameLayer = null;
var snakeLength = 5; //Length of the snake
var ScoreLabel = null;
var GameOverLabel = null;
var bCollisionDetected = false;

//The Background sprite class holding the game nodes
var SpriteBackGround = cc.Sprite.extend({
 ctor: function(texture, rect) {           
  this._super(texture, rect);
 }
});
//Snake cell class 
var SpriteSnakeCell = cc.Sprite.extend({
 ctor: function(texture) {           
  this._super(texture);
 }
});
//Snake food class 
var SpriteSnakeFood = cc.Sprite.extend({
 ctor: function(texture) {           
  this._super(texture);
 }
});
//Game Main container class, holding all game logic functions
var SnakeJSGameLayer = cc.Layer.extend({
    sprite:null,
    ctor:function () {
      
       this._super();
       var winSize = cc.winSize;
       //Create the background sprite 
       spriteBackGround = new SpriteBackGround(res.blank_png,
                                cc.rect(0,0,winSize.width-50,winSize.height-90));
       spriteBackGround.setAnchorPoint(0,0);
       spriteBackGround.setTag(Enum.background);
       BackGroundWidth = spriteBackGround.getBoundingBox().width;
       BackGroundHeight = spriteBackGround.getBoundingBox().height;
       //Calculate the background sprite positon by subtracting SpriteBackGround position from Main layer position 
       spriteBackGround.x = (winSize.width - BackGroundWidth)/2;
       spriteBackGround.y = (winSize.height - BackGroundHeight)/2;       
       this.addChild(spriteBackGround,1);
       BackGroundPos = {x:spriteBackGround.x, y:spriteBackGround.y};      
       //Add the score lable
       ScoreLabel = new cc.LabelTTF(setLabelString(score), "Arial", 38); 
       ScoreLabel.x = winSize.width / 2;
       ScoreLabel.y = winSize.height / 2 + 200;       
       this.addChild(ScoreLabel, 5); 
       //Add the game over lable as none visible 
       var redColor = cc.color(0, 0, 0);
       GameOverLabel = new cc.LabelTTF("Game Over press SPACE to restart!", "Arial", 38); 
       GameOverLabel.x = winSize.width / 2;
       GameOverLabel.y = winSize.height / 2;  
       GameOverLabel.fillStyle = redColor
       this.addChild(GameOverLabel, 5); 
       GameOverLabel.visible = false;
       //create the snake and the food 
       this.CreateSnake();
       this.CreateFood();
       
       if ('keyboard' in cc.sys.capabilities) {
            cc.eventManager.addListener({
                event: cc.EventListener.KEYBOARD,
                onKeyPressed: function (key, event) {
                   var target = event.getCurrentTarget();
                   //Only if space key is pressed and Collision Detected the game restart
                   if(bCollisionDetected && key == "32")
                   {
                        bCollisionDetected = false;
                        GameOverLabel.visible = false;
                        cc.director.resume();
                        target.CreateSnake();
                        target.CreateFood();
                        return;      
                   }
                   if(key == "37" && direction != "right") direction = "left";
                   else if(key == "38" && direction != "down") direction = "up";
                   else if(key == "39" && direction != "left") direction = "right";
                   else if(key == "40" && direction != "up") direction = "down";
                 } 
            }, this);
        } else {
            cc.log("KEYBOARD Not supported");
        }        
         
        //This is the main game loop
        this.schedule(this.GameLoop,0.2);
        
        return true;
    },
    CreateSnake:function() {
        score = 0;
        ScoreLabel.setString(setLabelString(score));
        direction = "right";
        if(( typeof SnakeArray != 'undefined' && SnakeArray instanceof Array ) && SnakeArray.length > 0 )
        {                
            for(var i = 0; i< SnakeArray.length; i++)             
            {
                    this.removeChild(SnakeArray[i],true);                   
            }
        } 
        SnakeArray = [];   
        var elmsToRemove = SnakeArray.length - length;
        if(elmsToRemove>1)
        {
            SnakeArray.splice(snakeLength-1,elmsToRemove);
        }
        for(var i = snakeLength-1; i>=0; i--)
        {
           var spriteSnakeCell = new SpriteSnakeCell(res.snakecell_png);
           spriteSnakeCell.setAnchorPoint(0,0);
           spriteSnakeCell.setTag(Enum.snakecell);          
           var xMov = (i*cellWidth)+BackGroundPos.x;
           var yMov = (spriteBackGround.y+BackGroundHeight)-cellWidth;
           spriteSnakeCell.x = xMov;
           spriteSnakeCell.y = yMov;
           this.addChild(spriteSnakeCell,2);           
           SnakeArray.push(spriteSnakeCell); 
        }
    },            
    CreateFood:function() {   
        //Check if food Exist , remove it from the game sprite
        if(this.getChildByTag(Enum.snakefood)!=null)
        {
            this.removeChildByTag(Enum.snakefood,true); 
        }       
        var spriteSnakeFood = new SpriteSnakeFood(res.snakefood_png);
        spriteSnakeFood.setAnchorPoint(0,0);
        spriteSnakeFood.setTag(Enum.snakefood);     
        this.addChild(spriteSnakeFood,2); 
        var rndValX = 0;
        var rndValY = 0;
        var min = 0; 
        var maxWidth = BackGroundWidth;
        var maxHeight = BackGroundHeight;
        var multiple = cellWidth;
        //Place it in some random position 
        rndValX = generate(min,maxWidth,multiple);
        rndValY = generate(min,maxHeight,multiple);
        var irndX = rndValX+BackGroundPos.x;
        var irndY = rndValY+BackGroundPos.y;
        SnakeFood = {
            x: irndX , 
            y: irndY  
        };
     
       spriteSnakeFood.x = SnakeFood.x; 
       spriteSnakeFood.y = SnakeFood.y;  
    },
    //The function will be called every 0.2 milii secound
    GameLoop:function (dt) {
        //get the snake head cell 
        var SnakeHeadX = SnakeArray[0].x;
        var SnakeHeadY = SnakeArray[0].y;
        //check which direction it is heading  
        switch(direction)
        {
            case "right":
                 SnakeHeadX+=cellWidth;
            break;
            case "left":
                 SnakeHeadX-=cellWidth;
            break;
            case "up":
                 SnakeHeadY+=cellWidth;
            break;
            case "down":
                  SnakeHeadY-=cellWidth;
            break;
            default:
                cc.log("direction not defind");
        }
        //Check if the snake head Collided with the walls or with it self 
        if(CollisionDetector(SnakeHeadX, SnakeHeadY, SnakeArray))
        {                 
                bCollisionDetected = true; 
                GameOverLabel.visible = true;
                cc.director.pause();
                return;
        }
        //Check if the snake head Collided with the food
        if(SnakeHeadX == SnakeFood.x && SnakeHeadY == SnakeFood.y)
        {
                //Add snake cell after the head position
                var spriteSnaketail = new SpriteSnakeCell(res.snakecell_png);
                spriteSnaketail.setAnchorPoint(0,0);
                spriteSnaketail.setTag(Enum.snakecell);
                this.addChild(spriteSnaketail,2);
                spriteSnaketail.x = SnakeHeadX;
                spriteSnaketail.y = SnakeHeadY;
                SnakeArray.unshift(spriteSnaketail);  
                //Add point to the points display
                ScoreLabel.setString(setLabelString(score++));
                //Create new food in new position
                this.CreateFood();         
        }
        else
        {       
                var spriteSnakeCellLast = SnakeArray.pop(); //pops out the last cell
                spriteSnakeCellLast.x = SnakeHeadX; 
                spriteSnakeCellLast.y = SnakeHeadY;
                SnakeArray.unshift(spriteSnakeCellLast);
        }
 
    }    
});
//The snake Collision Obstacles are side walls  and itself
function CollisionDetector(snakeHeadX,snakeHeadY,snakeArray)
{
        if(snakeHeadX < spriteBackGround.x || 
                snakeHeadX > BackGroundWidth || 
                snakeHeadY < spriteBackGround.y || 
                snakeHeadY > ((spriteBackGround.y+BackGroundHeight)-cellWidth))
        {
            return true;
        }
        for(var i = 0; i < snakeArray.length; i++)
        {
                if(snakeArray[i].x == snakeHeadX && snakeArray[i].y == snakeHeadY)
                {
                 return true;
                }
        }
        return false;
}
//Rendom number generator 
function generate(min, max, multiple) 
{
    var res = Math.floor(Math.random() * ((max - min) / multiple)) * multiple + min;
    return res;
}
//Convert number to string
function setLabelString(str)
{
    var stringScore = parseInt(score).toString();
    return stringScore;   
}
//Main Game container 
var SnakeJSScene = cc.Scene.extend({
    onEnter:function () {
        this._super();
        snakeJSGameLayer = new SnakeJSGameLayer();
        this.addChild(snakeJSGameLayer);
    }
});

  1. Lines 1 - 16 global variables , the scope of those will be accessible to all functions and objects.
  2. Lines 19 - 35 the game class's ,
    SpriteBackGround: contains all the games players snake and food.
    SpriteSnakeCell: the snake is build from these objetcs.
    SpriteSnakeFood: the red square which the snake need to eat.
  3. Lines 37 - 217 is the main layer that hold all the games element , all of them .
  4. Lines 39 the Layer constructor , it will initialize only once when the game start
    so all the initials setup of the game for example creating the snake (line 69) and the food (line 70)  and the main game sprite (line 44) will happen here.
  5. Lines 72 - 95 capture the keyboard events from the user.
    lines 78 - 86 when collision detected and the space key is pressed , restart the game .
    lines  87 - 90 when keyboard left / right /top /down keys pressed detected.
  6. Line 98 this is one of the most important functions in Cocos2d-x or any game in general.
    AKA : The Game Main Loop scheduler which fire the "Main Game loop function"
    in our case it is : GameLoop function (line 162).
  7. Lines 102 - 131 CreateSnake function creates the snake from SpriteSnakeCell objects .
    lines 106 -102 checks to see if there is already snake array defined and visible in the game before removing it , this function creates new snake ,only 1 snake allowed in the game.
    lines 119 - 130 the snake creation loop.
  8. Lines 132 -156 CreateFood function , create 1 food square each time called on random location on the game layer . only 1 food object allowed
  9. Lines 162 - 216 GameLoop function , in my view the most important function in the game logic it is function that invoked each N second in our case every 0.2 millisecond from the schedule function Line 98 . this is not always accurate it depends on the game structure and CPU.
    lines 167 - 183 detect which direction is pressed by the user so the program could direct it in the right direction 
  10. Lines 185 - 191 call the CollisionDetector if collision detected do as follow :
    1 . set the global variable bCollisionDetected to true.
    2.  set GameOverLabel to be visible so there is indication the game is over.
    3.  pause all cocos2d-x engine work as result the game stop,
  11. Lines 193 - 217 if the snake "head" cell that is the element in place 0 in the SnakeArray array
    remember we are working only with the snake head see lines 164 - 165.
    if the location of the head going to as the food location the program need to remove the food object and increase the snake size lines 196 - 206 .
    if not just continue the snake move lines 210 - 213.
  12. lines 219 -236 CollisionDetector function checks if the snake head collide the game spriteBackGround boundaries .
    checks also if the snake head doesn't collide it self lines 228 - 234 .
  13. Lines 238 -242 generate function simple random number generator. used to place the food object.
  14.  Lines 244 - 248 setLabelString function convert int value type to string type 
  15. Lines 250 - 255 create the game Scene , this is the root node of all our game stage nodes.
We are ready to publish our "SnakeJS" game in the next tutorial :
Snake Game Using Cocos2d-x HTML5  - PART 4


Play the final SnakeJS game:
http://meiry.github.io/SnakeJS-html5/publish/html5/
The SnakeJS source code:
https://github.com/meiry/SnakeJS-html5

Tuesday, September 29, 2015

Snake Game Using Cocos2d-x HTML5 - PART 2

To keep the tutorial short as possible , we will not learn the Cocos2d-x engine components .
i planing to write about the engine infrastructure and also its API's in the Future.
 Cocos2d-x is huge engine so don't expect to know all , but you will learn enough to get started.

1. Change the cocos2d-x-3.8.1\NetBeansProjects\SnakeJS\public_html\index.html  file
So the game canvas will be in proportional size and not stretched all over .
index.html:

NetBeansProjects\SnakeJS\public_html\index.html 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>SnakeJS</title>
    <link rel="icon" type="image/GIF" href="res/favicon.ico"/>
    <meta name="viewport" content="width=480, initial-scale=1">
    <meta name="apple-mobile-web-app-capable" content="yes"/>
    <meta name="full-screen" content="yes"/>
    <meta name="screen-orientation" content="portrait"/>
    <meta name="x5-fullscreen" content="true"/>
    <meta name="360-fullscreen" content="true"/>
    <style>
        body, canvas, div {
            -moz-user-select: none;
            -webkit-user-select: none;
            -ms-user-select: none;
            -khtml-user-select: none;
            -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
        }
    </style>
</head>
<body style="text-align: center; padding:0; margin: 0; background: #FFFFFF;">
<script src="res/loading.js"></script>
<div style="display:inline-block;width:auto; margin: 0 auto; background: #000; position:relative; border:5px solid black; border-radius: 10px; box-shadow: 0 5px 50px #333">
    <canvas id="gameCanvas" width="800" height="450"></canvas>
</div>
 
<script src="frameworks/cocos2d-html5/CCBoot.js"></script>
<script cocos src="main.js"></script>
</body>
</html>


2. Open the file \NetBeansProjects\SnakeJS\public_html\src\app.js 
this file will be the game logic starting point .
the file start our main game Scene and Layer , the Layer will hold our game Sprites
they all have Parent child hierarchy relationship to read more about it here:
http://www.cocos2d-x.org/wiki/Scene_Graph 

lts clean the file from the starter code the console generated for us :

NetBeansProjects\SnakeJS\public_html\src\app.js

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var SnakeJSGameLayer = cc.Layer.extend({
    sprite:null,
    ctor:function () {
       
       this._super();
       var size = cc.winSize;

       
        return true;
    }
});

var SnakeJSScene = cc.Scene.extend({
    onEnter:function () {
        this._super();
        var layer = new HelloWorldLayer();
        this.addChild(layer);
    }
});

3. Do not rush to test your code yet,first we need to change the file: NetBeansProjects\SnakeJS\public_html\main.js

Notice we changed the SnakeJSScene line 13 and  SnakeJSGameLayer line 1  variables
We need now to change it also in the main.js file :


NetBeansProjects\SnakeJS\public_html\main.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
cc.game.onStart = function(){
    if(!cc.sys.isNative && document.getElementById("cocosLoading")) //If referenced loading.js, please remove it
        document.body.removeChild(document.getElementById("cocosLoading"));

    // Pass true to enable retina display, disabled by default to improve performance
    cc.view.enableRetina(false);
    // Adjust viewport meta
    cc.view.adjustViewPort(true);
    // Setup the resolution policy and design resolution size
    cc.view.setDesignResolutionSize(800, 450, cc.ResolutionPolicy.SHOW_ALL);
    // The game will be resized when browser size change
    cc.view.resizeWithBrowserSize(true);
    //load resources
    cc.LoaderScene.preload(g_resources, function () {
        cc.director.runScene(new SnakeJSScene());
    }, this);
};
cc.game.run();

The change is in line 15 , the main.js is our game initializator/ bootstrapper/ dispatcher class,
It responsible to how the screen /view size will be and which pre game configuration to set and more
None game logic related stuff .
Now go ahead and press the green play button in NetBeans and see what you got.
Should look like this :


Yep , empty Scene.

4. the game resources are configured in special file called : SnakeJS\public_html\src\resource.js
it contains the image names and paths:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var res = {
    snakecell_png : "res/snakecell.png",
    snakefood_png : "res/snakefood.png",
    blank_png : "res/blank.png"
   
};

var g_resources = [];
for (var i in res) {
    g_resources.push(res[i]);
} 



We are ready to start programming our "SnakeJS" game logic in the next tutorial :
Snake Game Using Cocos2d-x HTML5  - PART 3

Play the final SnakeJS game
http://meiry.github.io/SnakeJS-html5/publish/html5/
The SnakeJS source code:
https://github.com/meiry/SnakeJS-html5

Snake Game Using Cocos2d-x HTML5 - PART 1

The purpose of this tutorial series is to learn how to create true cross platform 2d game
That works with the  same ( 80% - 90% ) source code on mobile web and desktop using Javascript .
And NOT using web browser wrappers components.
First is for the WEB then to win32 Desktop , iOS and android.
The framework is Cocos2d-x version 3.8.1.
The IDE is NetBeans for the web . you can learn why from my previous tutorial .

Lets start from the end and see the final result AKA the finished game , don't expect to see full blown game , remember we are going to learn only the principles ,
The SnakeJS:
http://meiry.github.io/SnakeJS-html5/publish/html5/

The SnakeJS source code:
https://github.com/meiry/SnakeJS-html5

As all programming stuff when first learned there is "Hello World++" project .
In GameDev there are several , for example build : flappy bird , Breakout or Snake.
For this tutorial i will choose Snake , as tribute to the old Nokia  9010.

1.Create new JavaScript project with Cocos2d-x console
The syntax for creating new js project looks like this :

 cocos new -p com.snakeJS.game -l js -d d:\dev\cpp\2d\cocos2d-x-3.8.1\cocos2d-x-3.8.1\Projects SnakeJS  

2 .Create new HTML 5 project in NetBeans IDE name it "SnakeJS"
If you like to recall how to setup the project , please refer to this tutorial .
The directory structure should look like this :
I created this "SnakeJS" under NetBeansProjects directory
And the path structure is :
D:\dev\cpp\2d\cocos2d-x-3.8.1\NetBeansProjects\SnakeJS\public_html
it should look like this:





3.Now quickly copy the relevant files from the new project we create with cocos2d-x
console to the NetBeans new project under the
D:\dev\cpp\2d\cocos2d-x-3.8.1\NetBeansProjects\SnakeJS\public_html\     folder.




In the Framework folder you should copy only the cocos2d-html5 folder and it sub folders
                                   



In NetBeans it should be look like this :

Press "Run Project"  (the green Play button)
Your Chrome should start with a new Tab loading the Cocos2d-x HTML5 starter project

We are ready to start programming our "SnakeJS" web game in the next post :
Snake Game Using Cocos2d-x HTML5-PART 2


Play the final SnakeJS game:
http://meiry.github.io/SnakeJS-html5/publish/html5/
The SnakeJS source code:
https://github.com/meiry/SnakeJS-html5