After the development of my first two games Tokyo Train and Haunted house I am interested in development of a multiplayer game. For this reason I started to study the Come2Play API.
Links to the project: http://www.come2play.com/developer.asp
The goal of the tutorial is to show how to build a Tic Tac Toe game using these API.
The project can be downloaded from www.mindthemove.com/blog/projects/TicTacToeC2P.zip
These API are basically used to have a communication between one or more clients (in Flash AS3) and the Come2Play server to allow a multiplayer game.
There are 2 type of functions: operations and callbacks.
I am going to use two classes.
- MainTTTC2P that extends ClientGameAPI. This is the main class the acts as controller
- TTTC2PViewer that is the viewer class. In some way, because the game is simple and it is for tutorial purpose, this will work as model as well.
I am not using an event driven approach as in the tutorials from come2play project because I found this confusing the main focus that is to learn how to use the basic API by come2play.
For any reference to the API this is the link: http://come2play.com/API_
Let’s start from MainTTTC2P class.
This class is created in the first frame of the FLA file:
var game:MainTTTC2P = new MainTTTC2P(this) |
The class is declared as:
public class MainTTTC2P extends ClientGameAPI |
In the constructor I am going to get the calling movieclip and calling the super constructor (the constructor of the basic class ClientGameAPI.
Moreover I pass this movieclip to my viewer class that is created here.
The call to doRegisterOnServer is the way for the client to enter the lobby for this game. The call to this function should be done using AS3_vs_AS2.waitForStage function.
public function MainTTTC2P(stageMovieClip:MovieClip) { super(stageMovieClip); theViewer = new TTTC2PViewer (stageMovieClip); keyMove= 0; AS3_vs_As2.waitForStage(this,constructGame);
}// constructor private function constructGame(){ doRegisterOnServer();//registers your game on the server } |
The first callback function that is called is gotCustomInfo. You need to remember that the callback functions inherited by the basic class ClientGameAPI must be declared as “override”.
override public function gotCustomInfo (infoEntries:Array):void { this.myUserId = T.custom(CUSTOM_INFO_KEY_myUserId,null) as int; } |
On this function you get back some custom information. Calling the helper class T you can get some information back from the server. One simple example is to use the constant string “CUSTOM_INFO_KEY_myUserId” to get your user id. Because this is a important information it’s useful to get this stored.
Remember that each player will receive from the server a unique id to be identified. It’s critical for the client flash game to know the user id. In some how this is telling the program who is it.
The second callback function the client will receive is gotMatchStarted. This is triggered as soon as the game is started.
In this function the most important information is the list of the user ids for all the players. Again this is critical to know so this is stored in an internal variable.
I found useful as well store immediately where in this array the client is. This is done using the already stored information about my id.
Let’s have an example. If myUserId =41 and the array received is allPlayerIds=[41,42], this client has index 0 in the array while my opponent will have index 1. Therefore myUserIndex=0; This is another way to know who the client is, in terms of index in the array of all players.
This is the time where to start the new board using theViewer.startNewGame. The viewer class should also know what is myUserIndex, therefore I am going to store this on that class.
override public function gotMatchStarted (allPlayerIds:Array/*int*/, finishedPlayerIds:Array/*int*/, serverEntries:Array/*ServerEntry*/):void { this.allPlayerIds = allPlayerIds; this.myUserIndex = allPlayerIds.indexOf(myUserId);
theViewer.startNewGame(this.myUserIndex); addEventListener(Event.ENTER_FRAME,timerHandler); setupFirstPlayer(); } |
In this function I also start a loop using the ENTER_FRAME event. This is a function that will be called periodically with the fps set up in the FLA. This is a way to check if the viewer class has set up any new moves. This is an alternative to the event driven solution found in the come2play tutorials.
We will discuss this later.
The last function is used to define the first player. Because the game is in turns, only one player can make a move. Therefore this information must be set up.
A simple way is to define as starting player the first player in the index of all user ids. This information is stored in the currentturnIndexPlayer in the viewer class.
You need to think to the 2 clients that received the same array of the player ids (allPlayerIds). Therefore both clients have the user ids in order in this array: the first element of the array (allPlayerIds[0]) will be the same for both clients.
The serve must be notified by all clients about the current player. To do this each client must call the doAllSetTurn function. This is the way the server can be sure that all clients agreed to have one current player.
private function setupFirstPlayer() { var firstPlayerIndex: int = 0; theViewer.currentturnIndexPlayer = firstPlayerIndex; doAllSetTurn(allPlayerIds[firstPlayerIndex],-1); } |
It is time now to see the viewer class.
In the constructor the movieclip is stored.
Then the board is created. I found useful to have a linear array of 9 elements instead of a double array 3x3. The index of the linear array will be:
0 1 2
3 4 5
6 7 8
In order to display an empty square or a square filled with the “x” or with the “o” I have created a Movie Clip symbol called Square and I have exported for Actionscript.
In the frame=1 there is an empty square. In frame=2 there is the “x”, while in frame=3 there is the “o”.
To create a new symbol it’s just enough to create a new instance of the class Square:
var squareNew = new Square(); |
The I set up the Square movieclip in the right position and to the right frame (squareNew.gotoAndStop(EMPTYFRAME);). Then I add this new symbol to the stage and to an arrayOfSquares that I use to keep track of the clicked squares.
To be noted I have used a dynamic property called “myBoardIndex” to store the index of the movie clip in the board. For example the first movie clip (on the left top corner) will have myBoardIndex=0. The one in the center of the board will have myBoardIndex=4, etc.
public function TTTC2PViewer (stageMovieClip:MovieClip) { this.stageMovieClip = stageMovieClip; arrayOfSquares = new Array(9); for(i=0;i<9;i++){ var squareNew = new Square(); squareNew.x=SQUAREPOSITION[i].x; squareNew.y=SQUAREPOSITION[i].y; squareNew.myBoardIndex= i; squareNew.gotoAndStop(EMPTYFRAME); squareNew.addEventListener(MouseEvent.MOUSE_DOWN, clickHandler); arrayOfSquares[i] = squareNew; stageMovieClip.addChild(squareNew); squareClickedIndex = NOCLICKEDSQUARE; } }// constructor |
Each movieclip is registered with a MOUSE_DOWN event. The called function is clickHandler.
public function clickHandler(event:MouseEvent){ if (!isMyTurn()) return; var clicked:Square = event.target as Square; if ((arrayOfSquares[clicked.myBoardIndex].currentFrame)==EMPTYFRAME){ setSquare(myUserIndex,clicked.myBoardIndex); squareClickedIndex = clicked.myBoardIndex; } } |
This function is immediately exit if it is not the turn of this client. This is done comparing the myUserIndex value with the currentturnIndexPlayer value.
public function isMyTurn():Boolean{ return (this.currentturnIndexPlayer==this.myUserIndex); } |
Then the target of the event is used to know which square was clicked. Only if the square is empty (that is checked using the currentFrame of the MovieClip then the square is changed calling the function setSquare (this function will be called to set the square also to set an opponent move). This is done simply using the procedure gotoAndStop to the playerIndex value (added by 2 because the first symbol “x” is to the frame 2 as you can see in the timeline for Square).
public function setSquare(playerIndex:int, squareIndex:int){ arrayOfSquares[squareIndex].gotoAndStop(playerIndex+OFFSETFIRSTSYMBOL); } |
The last action on the clickHandler function is to set the variable squareClickedIndex to the index of the board (value from 0 to 8). Normally this variable is set to -1 to indicate that no click is yet made. As soon as this variable is set to a different value, this indicates the place in the board where the click was performed.
This is the signal that the main class will check in the polling cycle inside the timerHandler. Actually there are 2 condition must be fulfilled: must be my turn and the theViewer.squareClickedIndex must be different than -1 (=theViewer.NOCLICKEDSQUARE).
public function timerHandler(event:Event):void { if ( theViewer.isMyTurn() && (theViewer.squareClickedIndex!=theViewer.NOCLICKEDSQUARE) ) sendMoveToServer(); } |
If so the sendMoveToServer is called. The main purpose of this function is to send to the server the message that a move was made by the current player using the function doStoreState.
public function sendMoveToServer(){ var myKey = this.myUserId+"_"+this.keyMove; var userEntry:UserEntry = UserEntry.create(myKey,theViewer.squareClickedIndex,false); var userEntries:Array = new Array(); userEntries.push(userEntry); doStoreState(userEntries); theViewer.squareClickedIndex = theViewer.NOCLICKEDSQUARE; keyMove++; } |
The doStoreState function needs only one parameter that must be an Array of type UserEntry. Actually the array can contain only one element.
To create a variable of type UserEntry the static function create is used:
UserEntry.create(theKey,theValue,isSecret) where:
- theKey must be unique if you want to store a new message. If you use a key already present this will override the previous one.
- theValue is the actual message that you want to send
- isSecret indicates if the other clients will be allowed to see the message sent
Both theKey and theValue can be any Object. There are different way to choose how to create the key and the value. My choise was the following:
For the key I created a String of this type “myUserId_keyMove” where keyMove is incremented at each move (at each click). So for example with player with id=41 the first key sent will be “41_0”, the second “41_1” etc... While for the opponent (with id=42 for example) will be “42_0”, “42_1”, etc…
The value will be simply the index of the board where the click was performed. This is the critical information to send to the opponent so that the opponent can display the square with the right symbol.
After sending the message to the server , the incremental keyMove variable is updated. And the theViewer.squareClickedIndex variable is set to -1 so that next time (and when it will be again my turn) it will be possible to click again.
Now that the message is sent let’s suppose to move to the opponent client. This client must receive the message and display the opponent’s symbol on the board on the right position (board index).
This is done in the gotStateChanged function where the server sends to the client the serverEntries array containing the moves in terms of ServerEntry elements.
Because we sent only 1 message, only the first element serverEntries[0] will be used.
In this variable the property storedByUserId is the id of the client who sent this message. Because the client who actually sent the message is also receiving this information, we need to check if the message was sent by the client itself. Only if this was sent by the opponent the function theViewer.setSquare is called to display the opponent symbol.
While the client who sent the message is not doing this simply because we already set this on the clickHandler function. That was a choice. An alternative would have been to set the symbol for the client itself after he received back the gotStateChange event from the server.
override public function gotStateChanged(serverEntries:Array):void { var serverEntry:ServerEntry = serverEntries[0];
if (serverEntry.storedByUserId != this.myUserId){ theViewer.setSquare(allPlayerIds.indexOf(serverEntry.storedByUserId),serverEntry.value); }
setNextPlayerTurn();
// CHECK if the game is over var winnerIndex:int = theViewer.winnerPlayer(); if (winnerIndex>-1) { setupWinner(winnerIndex); } } |
In any case the setNextPlayerTurn is called. First thing the currentIndexPlayer is updated with the next player. This is simply done adding 1 and set back to 0 the index in case it’s greater than the allPlayerIds array length.
private function setNextPlayerTurn(){ theViewer.currentturnIndexPlayer++; if (theViewer.currentturnIndexPlayer >= allPlayerIds.length) theViewer.currentturnIndexPlayer = 0;
doAllSetTurn(allPlayerIds[theViewer.currentturnIndexPlayer],-1); } |
Then the doAllSetTurn is sent to the server with the id of the new active player. It is also possible to set a maximum number of millisecond for the move (or -1 to ignore this).
This soAllSetTurn function must be called by all the clients to have any effect. And this is the reason why the gotStateChange function this is called from both clients. This is also useful to have both clients updated in terms of current player index.
The last action in the gotStateChange is to check if the game is over. If so the setupWinner function is called.
The main purpose of this function is to call the doAllEndMatch function to inform the server the game is ended. Again, as all the doAll…. functions, this must be called by all the players.
private function setupWinner(winnerIndex:int){ var finishedPlayers:Array = new Array(); var playerMatchOver:PlayerMatchOver; for (var i:int=0; i if ( i==winnerIndex ){ playerMatchOver= PlayerMatchOver.create(allPlayerIds[i],100,100); }else{ playerMatchOver= PlayerMatchOver.create(allPlayerIds[i],0,0); } finishedPlayers.push(playerMatchOver); }
doAllEndMatch(finishedPlayers); } |
The parameter sent to the server must be an array of type PlayerMatchOver. Each element must contain the id of all players that have finished the game. In our case both players end the game simultaneously.
Therefore there is a loop in the allPlayeIds array to create a variable of type PlayerMatchOver. Again the static method “create” is used with 3 parameters:
- userId
- theScore. Set to 100 if the player wins and 0 if the player looses.
- thePot. The pot is used if the game allows players to bet during the game. In our case this will be 100% if the player wins, and 0% if the player looses.
2 comments:
Cool. I did some blogging work for Come2Play . Have you sent them this link?
Do you have a direct email contact? Can't find any contact information on your blog or website.
Yehuda
Looks good. The documentation on the site is a bit lacking, so this should be really helpful.
Post a Comment