Monday, January 25, 2016

Multiplayer Card Game using WebSockets,Java Netty Server ,Cocos2d-x-HTML5 - Part 8

Game Client Source code overview

In this post i will teach you about the client class that handles the game logic , as you remember
The "Real" game is played on the server BUT we as players need to visualize the game.
The Game logic in the client have many tasks to do but all the tasks are stateless .
That means in simple words there is no data saved on the client each action made 
will first make request to the server and after confirmation it will make its move , 
of course all the actions will be informed in real time to the other players.
also in our game , this Game layer will take the control of the WebSockets callbacks .

and will handle the building the request JSON protocol and the Decoding the returned
JSON from the server And Act upon the events received .

Here is JSON communication example :

User Login to the game with user name send login request to the server:
The JSON format:
{
"event": 2,
"username": "Pla"
}
The Server confirms the new Player and it send back to the current client and other client:
response JSON format :
{
"winnercards": "",
"winner": -1,
"activecardid": "c47",
"endgame": -1,
"players": [],   //Here is the array of the other players in the room
"deck": "",
"id": 0,
"event": 3,
"activeplayerid": 0,
"registertionnum": 0,
"username": "Pla",
"numcardsleft": 25

}



In this end of this tutorial the login should look like this :
2 browsers open , to simulate 2 players doing login
and each one see the other in real time




GameScene.js

Game logic class 



  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
var spriteFrameCache = cc.spriteFrameCache;
var size = null;
var MENU_TAG = 1;
var CENTER_DECK = 2;
var CARD = 3;
var TEXT_INPUT_FONT_SIZE_PLAYER = 20;
 
var GameLayer = cc.Layer.extend({
    cardDeckSprite:null,
    sprite:null,
    listener1:null,
    jsonData:null,
    textFieldUserNameCapton:null,
    currentPlayer:null,
    otherPlayers:[],
    ctor:function (_jsondata) {
        this.jsonData = _jsondata;
        //after succesful login we want to take control on massages coming from server
        //so we attache this new callback function to the websocket onmessage
        ws.onmessage = this.ongamestatus.bind(this);
        ws.onclose = this.onclose.bind(this);
        ws.onerror = this.onerror.bind(this);
        
        this._super();
        size = cc.winSize;        
        return true;
    },
     
    onEnter:function () {       
        this._super();   
            // Make sprite1 touchable
        this.listener1 = cc.EventListener.create({
            event: cc.EventListener.TOUCH_ONE_BY_ONE,
            swallowTouches: true,
            onTouchBegan: function (touch, event) {
                event.getCurrentTarget().invokeTurn(); 
                return true;
            },
            onTouchEnded: function (touch, event) {
            }
        });
        this.setUserObject(this.listener1);
        cc.eventManager.addListener(this.listener1, this);
        spriteFrameCache.addSpriteFrames(res.sprites_plist, res.sprites_png); 
           
        var userName = this.jsonData.username;
        this.textFieldUserNameCapton = new cc.TextFieldTTF("Hello "+userName,
            TEXT_INPUT_FONT_NAME,
            TEXT_INPUT_FONT_SIZE_PLAYER);
        this.textFieldUserNameCapton.setTextColor(cc.color.RED);
        this.textFieldUserNameCapton.x = size.width / 2;
        this.textFieldUserNameCapton.y = size.height-(TEXT_INPUT_FONT_SIZE_PLAYER+100);
        this.addChild(this.textFieldUserNameCapton,10);  
        this.eventHandler(this.jsonData.event);
     },
    onExit:function () {
         
        this._super();
        spriteFrameCache.removeSpriteFramesFromFile(res.sprites_plist);
        spriteFrameCache.removeSpriteFramesFromFile(res.sprites_png);
        cc.eventManager.removeListener(this.listener1);
    }, 
    eventHandler:function(event)
    {
        switch (event) {
            case Events.LOGIN_DONE:
            {
              this.setupCurrentPlayer();
              break;
            }
            case Events.NEW_USER_LOGIN_DONE:
            {
              this.setupOtherPlayerS();
              break;
             
            }
            case Events.PLAY_DONE:
            {
              this.setPlayState();
              break;
             
            }
        }
        this.setTurnMassage();
    },
    setTurnMassage:function()
    {
       var userName = this.jsonData.username;
       var activePlayerId = this.jsonData.activeplayerid;
       if(activePlayerId === this.currentPlayer.id)
       {
           this.textFieldUserNameCapton.setString("Hello "+userName+" Its your turn"); 
       }
       else
       {
           this.textFieldUserNameCapton.setString("Hello "+userName); 
       }
    },
    onCallbackMoveTo:function (nodeExecutingAction,player) {
            this.currentPlayer.updatePlayerNumberOfCardsCaption(this.jsonData.numcardsleft);
            this.otherPlayers[0].updatePlayerNumberOfCardsCaption(this.jsonData.players[0].numcardsleft);       
    },
    setPlayState:function()
    {
        this.currentPlayer.setNewCardById(this.jsonData.activecardid);
        this.updatePlayer(this.currentPlayer,this.jsonData); 
        if(this.jsonData.players.length>0)
        {
            for(var i=0;i<this.jsonData.players.length;i++)
            {
                if(this.jsonData.players[i].event === Events.PLAY_DONE)
                {
                    this.otherPlayers[i].setNewCardById(this.jsonData.players[i].activecardid);
                    this.updatePlayer(this.otherPlayers[i],this.jsonData.players[i]);
                }
            } 
        }
        //handle animation
        var pos = null;
        var activePlayerId = this.jsonData.activeplayerid;
        if(activePlayerId !== this.currentPlayer.id)
        {
            pos = this.currentPlayer.getPosition();
        }
        else
        {
            //TODO this fix this hard coded position getter
            pos = this.otherPlayers[0].getPosition();
        }
        var cardInDeck = this.jsonData.deck;
        this.animateCard(cardInDeck,pos);
    },
    animateCard:function(_cardInDeckId,_pos)
    {
        var cardName =  cards[_cardInDeckId];
        this.cardDeckSprite = new cc.Sprite("#"+cardName);
        this.cardDeckSprite.attr({
                x: _pos.x,//(cc.winSize.width / 2) ,
                y: _pos.y//(cc.winSize.height / 2)
            });
        this.addChild(this.cardDeckSprite,1,CENTER_DECK); //TODO handel removeble when not needed by tag name
        var posMid = cc.p(size.width/2,size.height/2); 
        var action = cc.sequence(
                    cc.moveTo(0.5, posMid),
                    cc.callFunc(this.onCallbackMoveTo,this,this.cardDeckSprite));
        this.cardDeckSprite.runAction(action);          
    },
    invokeTurn:function()
    {
       if(this.currentPlayer.id == this.currentPlayer.activeplayerid)
       {
          var config = {
                        event:Events.PLAY,
                        username:this.currentPlayer.username,
                        id:this.currentPlayer.id,
          };  
          var message = Encode(config);
          ws.send(message);
       }
       else
       {
          console.log("GameScene->invokeTurn() not its turn:"+this.currentPlayer.id); 
       }
    },
    setupCurrentPlayer:function()
    {
        this.currentPlayer = new Player(this.jsonData.id,this.jsonData.username,
                                                        this.jsonData.activecardid);        
        this.updatePlayer(this.currentPlayer,this.jsonData);      
        
        this.addChild(this.currentPlayer,1); 
        this.positionPlayer(this.currentPlayer); 
        if(this.jsonData.players.length>0)
        {
            for(var i=0;i<this.jsonData.players.length;i++)
            {
                if(this.jsonData.players[i].event === Events.NEW_USER_LOGIN_DONE)
                {
                    this.setupOtherPlayer(i);
                }
            } 
        }     
    },
    setupOtherPlayerS:function()
    {
        if(this.jsonData.players.length>0)
        {
            for(var i=0;i<this.jsonData.players.length;i++)
            {
                if(this.jsonData.players[i].event === Events.LOGIN_DONE)
                {
                    this.setupOtherPlayer(i);
                }
            } 
        }
    },
    setupOtherPlayer:function(inx)
    {
        this.otherPlayers[inx] = new Player(this.jsonData.players[inx].id,
                                            this.jsonData.players[inx].username,
                                            this.jsonData.players[inx].activecardid);      
        this.updatePlayer(this.otherPlayers[inx],this.jsonData.players[inx]);
        
        this.addChild(this.otherPlayers[inx],1); 
        this.positionPlayer(this.otherPlayers[inx]); 
        
    },
    updatePlayer:function(_player,jsonObj)
    {
        _player.activeplayerid = jsonObj.activeplayerid;
        _player.activecardid =  jsonObj.activecardid;
        _player.event =  jsonObj.event;
        _player.registertionnum =  jsonObj.registertionnum; 
        _player.winner =  jsonObj.winner; 
        _player.winnercards =  jsonObj.winnercards; 
        _player.numcardsleft =  jsonObj.numcardsleft;          
    },
    positionPlayer:function(_player)
    {
        if(_player.registertionnum === 0)
        {
            _player.attr({
                x: (cc.winSize.width / 2) + 150,
                y: (cc.winSize.height / 2)
            });
            
        }
        else if (_player.registertionnum === 1)
        {
            _player.attr({
                x: (cc.winSize.width / 2) - 150,
                y: (cc.winSize.height / 2)
            });
        }
    },
    ongamestatus:function(e) {
          console.log("GameScene->.ws.onmessage():"+e.data);
          if(e.data!==null || e.data !== 'undefined')
          { 
              this.jsonData = Decode(e.data);
              this.eventHandler(this.jsonData.event);
         }
     }
     ,   
    onclose:function (e) {

    },
    onerror:function (e) {

    }
});  
var EnterWorldScene = cc.Scene.extend({    
    session:null,
    ctor:function (_session) {
        this.session = _session;
        this._super();
        size = cc.winSize;        
        return true;
    },
    onEnter:function () {
        this._super();
        var layer = new GameLayer(this.session);
        this.addChild(layer);
    }    
});


  1. Lines 9 - 15 : members of the class ,
    cardDeckSprite : the sprite that holds the middle deck cards.
    listener1: the cocos2d-x object which listen to touch events from teh user
    jsonData: the JSON data received from the server.
    textFieldUserNameCapton : User name of the current user
    currentPlayer : current user that login
    otherPlayers: array that holds the Other players in the room data  , so wee can see the updates they made 
  2. Lines 16 -26 : class constructor that do several important things ,
    First - it gets the initial JSON data from the server that received when User LOGIN event
    confirmed.
    Second  - 20 to 22 it takes control of the WebSocket build in callbacks . and bind them to
    this class local callbacks .
  3. Lines 32 - 41 : set up Cocos2d-x new EventListener to detect on devices that enable touches
    The touch event and mouse events when it disabled . will invoke the invokeTurn() function .
  4. Lines 47 - 54 : set the user name according to the data received from the server 
  5. Lines 63 -83 : Event Handler function that is triggered first time when user enter the game room in line 54 .
    and then it triggered each time there is massage from the server  line 241.
  6. Line 84 : after each event there is need to update the players with who turn is now 
  7. Lines 99 -  101 : this callback function is called to invoke the players updatePlayerNumberOfCardsCaption function after the animation of the cards is finished. ( see lines 143 - 145 )
  8. Lines 103 -131 : The function is called when the PALY_DONE event is received from the server.
    Lines 105 -117 :  the current player get updated , and also the other players in the room get updated with new states .
    Lines 119 -131 : trigger the animation of the cards.
  9. Lines 133 - 147 : card animation function , animate the moving card into the middle card deck.
  10. Lines 148 - 164 : this function is triggered when player touch the screen or by mouse in desktop or finger on mobile device see paragraph 3 above .
    it will only be invoked if the current player is the active player . that means the one which is his turn in the game .
    then it will prepare the JSON massage to send to the server .
  11. Lines 165 - 183 : setup the current player this is called once when the LOGIN_DONE event
    is received from the server.
  12. Lines 184 - 196 : set up the other players in he game room , this is for us to see what is the status of the other players . this is triggered when the server sends the client NEW_USER_LOGIN_DONE event .
  13. Lines 197 - 207 : create new other player and set it to the otherPlayers client array .
  14. Lines 208 - 217 : update player object with new data.
  15. Lines 218 -235 : position the players in the game room based on order they joined the game
    the data is kept in the JSON registertionnum  member and set to the player registertionnum member .
  16. Lines 236 - 250 : those functions are the new calback functions that are bound to our client
    WebSocket object , see paragraph 2 above .
Thats it!  this is the last part of 8 parts tutorial .
 there are things i still need to fix and refine will do those over time.
I hope it was informative .

1 comment:

  1. Thank you for your step by step guide and overview on game client source code. It gives an insight on so many things!

    ReplyDelete

Note: Only a member of this blog may post a comment.