Chapter 32. A Networked Virtual Environment


The Networked Tic-Tac-Toe Game

NetFourByFour is based on the FourByFour game, retaining most of its game logic, 3D modeling, and GUI interface, and it adds a threaded client/server communications layer.

This development sequence is deliberate, as it allows most of the game-specific and user interface issues to be addressed before networking complexity is introduced.

Figure 31-6 shows the main functional components of NetFourByFour.

Figure 31-6. NetFourByFour clients and server


The top-level server, FBFServer, creates two PlayServerHandler tHReads to manage communication between the players. The server and its threads are thin in that they carry out little processing and act mainly as a switchboard for messages passing between the players.

An advantage of this approach is that most of the client's functionality can be borrowed from the standalone FourByFour, and the server side is kept simple. Processing is carried out locally in the client, whereas server-side processing would introduce networking delays between the user's selection and the resulting changes in the game window. A drawback is the need to duplicate processing across the clients.

Each NetFourByFour client utilizes the Java 3D GUI thread (where PickDragBehavior executes), the application thread for game processing, and an FBFWatcher tHRead to handle messages coming from the server. This threaded model was last seen in the chat application of Chapter 30, but there are some differences in building two-person networked games.

One change is the restriction on the number of participants: A chat system allows any number of users, who may join and leave at any time. A two-person game can begin only when both players are present and stops if one of them leaves. There cannot be more than two players, and I prohibit the mid-game change of players (i.e., substitutes aren't allowed).

Another complication is the need to impose an order on the game play: first player 1, then player 2, then back to player 1, and so on. A chat system doesn't enforce any sequencing on its users.

The ordering criteria seems to suggest that a player (e.g., player 2) should wait until the other player (e.g., player 1) has finished his move. The problem is that when player 2 is notified of the finished move, it will have to duplicate most of player 1's processing to keep its own game state and display current. In other words, player 2's waiting time before its turn will be almost doubled.

Though latency is less of an issue in turn-based games, avoid a doubling in wait time. One solution is for player 2 to be notified as soon as player 1 has selected a move and before it's been executed and rendered. This coding style is illustrated by the activity diagram in Figure 31-7, where the game engines for player 1 and 2 concurrently evaluate player 1's selected move.

Figure 31-7. Concurrent processing of player 1's move


Issues need to be considered with this technique. One is whether the selection will result in too much overhead in the server and second player's client software. This is especially true if the move is rejected by player 1's game engine, which makes the overhead of the network communication and processing by player 2 superfluous. For this reason, do some preliminary, fast testing of the selection before sending it over the network.

Another concern is whether the two game engines will stay synchronized with each other. For example, the processing of player 1's move by player 2's game engine may be much quicker than in player 1's client. Consequently, player 2 may send his turn to player 1 before player 1 has finished the processing of his own move. Player 1's client must be prepared to handle "early" move messages. Of course, early moves may arrive at player 2 from player 1.

This is one reason why it's useful to have a separate "watcher" thread as part of the client. Another reason is to handle server messages, such as the announcement that the other player has disconnected, which may arrive at any time.

Two-Person Communication Protocols

A good starting point for changing a two-person game into a networked application is to consider the communication protocols (e.g., the sequences of messages) required during the various stages of the game. It's useful to consider three stages: initialization, termination, and game play:


Initialization

In a two-person game, initialization is problematic due to the need to have two participants before the game can start and to restrict more than two players from joining.


Termination

This stage is entered when a player decides to stop participating in the game, which may be due to many reasons. When a player leaves the game, the other player must be notified.


Game play

This stage is usually the simplest to specify since it often involves the transmission of a move from one player to another.

In the following diagrams, I usually only consider the cases when player 1 starts the communication (e.g., when player 1 sends a new move to player 2). However, the communication patterns apply equally to the cases when player 2 initiates matters.

Initialization

The initialization stage in NetFourByFour uses the protocol shown in Figure 31-8.

Figure 31-8. Initialization stage protocol in NetFourByFour


The connect message is implicit in the connection created when player 1's client opens a socket link to the server. The handler can send back an ok message containing an assigned ID or can reject the link with a full reply. If the connection was accepted, then an added message would be sent to the other player (if there is one).

The game will commence when the player ID value is 2 in the ok and added messages, meaning that two players are ready to compete.

Termination

The termination stage uses the protocol given in Figure 31-9.

Figure 31-9. Termination stage protocol in NetFourByFour


The five conditions that cause a player to leave the game are listed at the top of Figure 31-9. The player sends a disconnect message and breaks its link with the server. In NetFourByFour, this doesn't cause the player's client to exit (though that is a design possibility). The server sends a removed message to the other player (if one exists), which causes it to break its link because there are now too few players.

The server will send a removed message if its socket link to player 1 closes without a preceding disconnect warning. This behavior is required to deal with network or machine failure.

If the handler dies, then the players will detect it by noticing that their socket links have closed prematurely.

Game play

The game play stage is shown in Figure 31-10.

The three conditions necessary for game play to continue are shown at the top of Figure 31-10. The selected move is sent as a try message via the server and arrives as an otherTurn message. The otherTurn message may arrive at the player while the previous move is still being processed for reasons described above. If player 2 has suddenly departed, perhaps due to a network failure, then the server may send a tooFewPlayers message back to player 1.

Figure 31-10. Game play stage protocol in NetFourByFour


Playing the Game

Figure 31-11 shows two NetFourByFour players in fierce competition. It looks like player 1 is about to win.

Figure 31-11. Two NetFourByFour players


The players have rotated their game boards in different ways, but the markers are in the same positions in both windows.

The GUI in NetFourByFour is changed from the FourByFour game: the message text field has been replaced by a message string which appears as an overlay at the top-left corner of the Canvas3D window. This functionality is achieved by subclassing the Canvas3D class to implement the mixed-mode rendering method postSwap( ), resulting in the OverlayCanvas class.

The class diagrams for the NetFourByFour client are given in Figure 31-12, and the server side classes appear in Figure 31-13. Only public methods are listed, and methods which are synchronized are prefixed with an S.

Figure 31-12. Class diagrams for the NetFourByFour client


Figure 31-13. Class diagrams for the NetFourByFour server


FBFServer is the top-level class on the server-side.

As with previous networked examples, the connections between the client and server are unclear since the links are in terms of messages passing rather than method calls.

All the code (i.e., the NetFourByFour client and the FBFServer server) can be found in the NetFourByFour/ directory.


The Top-Level Server

As indicated in Figure 31-6, the FBFServer class manages a small amount of shared data used by its two handlers: an array of PlayServerHandler references and the current number of players.

     private PlayerServerHandler[] handlers;     // handlers for players     private int numPlayers; 

The references allow a message to be sent to a handler by calling its sendMessage( ) method:

     synchronized public void tellOther(int playerID, String msg)     // send mesg to the other player     { int otherID = ((playerID == PLAYER1) ? PLAYER2 : PLAYER1 );       if (handlers[otherID-1] != null)     // index is ID-1         handlers[otherID-1].sendMessage(msg);     } 

tellOther( ) is called from the handler for one player to send a message to the other player.

The numPlayers global is modified as a side effect of adding and removing a player, and it is used to decide if enough players exist to start a game:

     synchronized public boolean enoughPlayers(  )     { return (numPlayers == MAX_PLAYERS); } 

The Player Handlers

The PlayServerHandler tHRead for a client deals with the various handler messages defined in the initialization, termination, and game play stages shown in Figures 31-8, 31-9, and 31-10.

When the thread is first created, it's passed the socket link to the client, and I/O streams are layered on top of it:

     // globals     private FBFServer server;     private Socket clientSock;     private BufferedReader in;     private PrintWriter out;         private int playerID;     // player id assigned by FBFServer             public PlayerServerHandler(Socket s, FBFServer serv)     {       clientSock = s;       server = serv;       System.out.println("Player connection request");       try {         in  = new BufferedReader( new InputStreamReader(                                      clientSock.getInputStream(  ) ) );         out = new PrintWriter( clientSock.getOutputStream(  ), true );       }       catch(Exception e)       {  System.out.println(e);  }     } 

run( ) starts by carrying out the messages specified in the initialization stage of the network communication. It calls addPlayer( ) in the server to add the new player. This may fail if there are two players and a full message is sent back to the client. If the joining is successful, then an ok message will be sent to the new player and an added message to the other player (if one exists):

     public void run(  )     {       playerID = server.addPlayer(this);       if (playerID != -1) {    // -1 means player was rejected         sendMessage("ok " + playerID); // tell player his/her ID         server.tellOther(playerID, "added " + playerID);             processPlayerInput(  );             server.removePlayer(playerID);   // goodbye         server.tellOther(playerID, "removed " +                                       playerID); // tell others       }       else    // game is full         sendMessage("full");           try {     // close socket from player         clientSock.close(  );         System.out.println("Player "+playerID+" connection closed\n");       }       catch(Exception e)       {  System.out.println(e);  }     } 

When processPlayer( ) returns, it means that the player has broken the network link, so the server must be updated and the other player notified with a removed message. Thus, run( ) finishes by carrying out the termination stage.

processPlayer( ) monitors the input stream for its closure of a disconnect message. Otherwise, messages are sent to doRequest( ), which deals with the game play stage of the communication:

     private void doRequest(String line)     {       if (line.startsWith("try")) {         try {           int posn = Integer.parseInt( line.substring(4).trim(  ) );               if (server.enoughPlayers(  ))             server.tellOther(playerID, "otherTurn " + playerID +                          " " + posn);  // pass turn to others           else             sendMessage("tooFewPlayers");         }         catch(NumberFormatException e)         { System.out.println(e); }       }     } 

A try message is sent to the other player as an otherTurn message.

sendMessage( ) writes a string onto the PrintWriter stream going to the player. However, the method must be synchronized since it's possible that the handler and top-level server may call it at the same time:

     synchronized public void sendMessage(String msg)     { try {         out.println(msg);       }       catch(Exception e)       {  System.out.println("Handler for player "+playerID+"\n"+e); }     } 



Killer Game Programming in Java
Killer Game Programming in Java
ISBN: 0596007302
EAN: 2147483647
Year: 2006
Pages: 340

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net