Comparing NetFourByFour and FourByFourMany classes in NetFourByFour are similar to those in FourByFour; this is a consequence of keeping the game logic on the client side. The Positions class, which manages the on-screen markers is unchanged from FourByFour. PickDragBehavior still handles user picking and dragging, but reports a selected position to NetFourByFour rather than to Board. The game data structures in Board are as before, but the tryMove( ) method for processing a move and the reportWinner( ) are different. WrapNetFourByFour is similar to WrapFourByFour but utilizes the OverlayCanvas class rather than Canvas3D. The NetFourByFour class is changed since the networking code for the client side is located there. FBFWatcher is used to monitor messages coming from the server, so it is new. Game Initialization in the ClientThe network initialization done in NetFourByFour consists of opening a connection to the server and creating a FBFWatcher thread to wait for a response: // globals in NetFourByFour private Socket sock; private PrintWriter out; private void makeContact( ) // in NetFourByFour { try { sock = new Socket(HOST, PORT); BufferedReader in = new BufferedReader( new InputStreamReader( sock.getInputStream( ) )); out = new PrintWriter( sock.getOutputStream( ), true ); new FBFWatcher(this, in).start( ); // start watching server } catch(Exception e) { System.out.println("Cannot contact the NetFourByFour Server"); System.exit(0); } } // end of makeContact( ) A consideration of Figure 31-8 shows that an ok or full message may be delivered from the server. These responses, and the other possible client-directed messages, are caught by FBFWatcher in its run( ) method: public class FBFWatcher extends Thread { private NetFourByFour fbf; // ref back to client private BufferedReader in; public FBFWatcher(NetFourByFour fbf, BufferedReader i) { this.fbf = fbf; in = i; } public void run( ) { String line; try { while ((line = in.readLine( )) != null) { if (line.startsWith("ok")) extractID(line.substring(3)); else if (line.startsWith("full")) fbf.disable("full game"); else if (line.startsWith("tooFewPlayers")) fbf.disable("other player has left"); else if (line.startsWith("otherTurn")) extractOther(line.substring(10)); else if (line.startsWith("added")) // don't use ID fbf.addPlayer( ); // client adds other player else if (line.startsWith("removed")) // don't use ID fbf.removePlayer( ); // client removes other player else // anything else System.out.println("ERR: " + line + "\n"); } } catch(Exception e) // socket closure will end while { fbf.disable("server link lost"); } // end game as well } // end of run( ) // other methods... } // end of FBFWatcher class The messages considered inside run( ) match the communications that a player may receive, as given in Figures 31-8, 31-9, and 31-10. An ok message causes exTRactID( ) to extract the player ID and call NetFourByFour's setPlayerID( ) method. This binds the playerID value used throughout the client's execution. A full message triggers a call to NetFourByFour's disable( ) method. This is called from various places to initiate the client's departure from the game. The handler for the player sends an added message to the FBFWatcher of the other player, leading to a call of it's NetFourByFour addPlayer( ) method. This increments the client's numPlayers counter, which permits game play to commence when equal to 2. Game Termination in the ClientFigure 31-9 lists five ways in which game play may stop:
Each case is considered in the following sections. The player has wonThe Board object detects whether a game has been won in the same way as the FourByFour version and then calls reportWinner( ): private void reportWinner(int playerID) // in Board { long end_time = System.currentTimeMillis( ); long time = (end_time - startTime)/1000; int score = (NUM_SPOTS + 2 - nmoves)*111 - int) Math.min(time*1000, 5000); fbf.gameWon(playerID, score); } reportWinner( ) has two changes: it's passed the player ID of the winner, and it calls gameWon( ) in NetFourByFour rather than write to a text field. gameWon( ) checks the player ID against the client's own ID and passes a suitable string to disable( ): public void gameWon(int pid, int score) // in NetFourByFour { if (pid == playerID) // this client has won disable("You've won with score " + score); else disable("Player " + pid + " has won with score " + score); } disable( ) is the core method for terminating game play for the client. It sends a disconnect message to the server (see Figure 31-9), sets a global Boolean isDisabled to true, and updates the status string: synchronized public void disable(String msg) // in NetFourByFour { if (!isDisabled) { // client can only be disabled once try { isDisabled = true; out.println("disconnect"); // tell server sock.close( ); setStatus("Game Over: " + msg); // System.out.println("Disabled: " + msg); } catch(Exception e) { System.out.println( e ); } } } disable( ) may be called from the client's close box, FBFWatcher, or from Board (via gameWon( )), so it must be synchronized. The isDisabled flag means the client can only be disabled once. Disabling breaks the network connection and makes it so further selections have no effect on the board. However, the application is left running, and the player can rotate the game board. The close box was clickedThe constructor for NetFourByFour sets up a call to disable( ) and exit( ) in a window listener: addWindowListener( new WindowAdapter( ) { public void windowClosing(WindowEvent e) { disable("exiting"); System.exit( 0 ); } }); Too few players, and the game is fullIf FBFWatcher receives a tooFewPlayers or a full message from its handler, it will call disable( ): public void run( ) // in FBFWatcher { String line; try { while ((line = in.readLine( )) != null) { if (line.startsWith("ok")) extractID(line.substring(3)); else if (line.startsWith("full")) fbf.disable("full game"); else if (line.startsWith("tooFewPlayers")) fbf.disable("other player has left"); : // other message else-if-tests } } catch(Exception e) {// exception handling... } } // end of run( ) The handler or other player has diedIf the other player's client suddenly terminates, then its server-side handler will detect the closure of its socket and will send a removed message to the other player: public void run( ) // in FBFWatcher { String line; try { while ((line = in.readLine( )) != null) { if (line.startsWith("ok")) extractID(line.substring(3)); // other message else-if-tests, and then... else if (line.startsWith("removed")) // don't use ID fbf.removePlayer( ); // client removes other player // other message else-if-tests } } catch(Exception e) // socket closure will end while { fbf.disable("server link lost"); } // end game as well } // end of run( ) FBFWatcher will see the removed message and call removePlayer( ) in NetFourByFour. This will decrement its number of players counter which prevents any further selected moves from being carried out. If the server dies then FBFWatcher will raise an exception when it reads from the socket. This triggers a call to disable( ) which ends the game. Game Play in the ClientFigure 31-7 presents an overview of typical game play using an activity diagram. Player 1 selects a move that is processed locally while being sent via the server to the other player to be processed. A closer examination of this turn-taking operation is complex because it involves the clients and the server. I'll break it into three parts, corresponding to the swimlanes in the activity diagram. Each part will be expanded into its own UML sequence diagram, which allows more detail to be exposed. Perhaps the most important point in this section is the usefulness of UML activity diagrams and sequence diagrams for designing and documenting network code. A networked application utilizes data and methods distributed across many distinct pieces of software, linked by complex, low-level message passing mechanisms. Abstraction tools are essential. Player 1's clientThe sequence diagram for the left side of Figure 31-7 (player 1's client) is shown in Figure 31-14. Figure 31-14. Sequence diagram for player 1's clientThe mouse press is dealt with by PickDragBehavior in a similar way to FourByFour, except that tryMove( ) is called in NetFourByFour and passed the selected position index. tryMove( ) carries out simple tests before sending a message off to the server and calling doMove( ) to execute the game logic: public void tryMove(int posn) // in NetFourByFour { if (!isDisabled) { if (numPlayers < MAX_PLAYERS) setStatus("Waiting for player " + otherPlayer(playerID) ); else if (playerID != currPlayer) setStatus("Sorry, it is Player " + currPlayer + "'s turn"); else if (numPlayers == MAX_PLAYERS) { out.println( "try " + posn ); // tell the server doMove(posn, playerID); // do it, don't wait for response } else System.out.println("Error on processing position"); } } // end of tryMove( ) TRyPosn( ) in Board is simpler than the version in FourByFour: public void tryPosn(int pos, int playerID) // in Board { positions.set(pos, playerID); // change 3D marker shown at pos playMove(pos, playerID); // play the move on the board } A gameOver Boolean is no longer utilized, the isDisabled Boolean has taken its place back in NetFourByFour. tryPosn( ) no longer changes the player ID since a client is dedicated to a single player. The playerID input argument of tryPosn( ) is a new requirement since this code may be called to process moves by either of the two players. set( ) in Positions is unchanged from FourByFour, and playMove( ) utilizes the same game logic to update the game and test for a winning move. reportWinner( ) is a little altered, as explained when considering the termination cases. Server-side processingThe sequence diagram on the server side (Figure 31-15) shows how a try message from player 1 is passed to player 2 as an otherTurn message. Figure 31-15. Sequence diagram for the serverI considered the coding behind these diagrams when I looked at the server-side classes. The diagram shows that the otherTurn message is received by the FBFWatcher of player 2: public void run( ) // in FBFWatcher { String line; try { while ((line = in.readLine( )) != null) { if (line.startsWith("ok")) extractID(line.substring(3)); : // other message else-if-tests, and then... else if (line.startsWith("otherTurn")) extractOther(line.substring(10)); : // other message else-if-tests } } catch(Exception e) { // exception handling... } } // end of run( ) Player 2's clientThe sequence diagram for the righthand side of Figure 31-7 (player 2's client) is shown in Figure 31-16. The call to exTRactOther( ) in FBFWatcher extracts the player's ID and the position index from the otherTurn message. It then calls doMove( ) in NetFourByFour: synchronized public void doMove(int posn,int pid) //in NetFourByFour { wrapFBF.tryPosn(posn, pid); // and so to Board if (!isDisabled) { currPlayer = otherPlayer( currPlayer ); // player's turn over if (currPlayer == playerID) // this player's turn now setStatus("It's your turn now"); else // the other player's turn setStatus("Player " + currPlayer + "'s turn"); } } doMove( ) and the methods it calls (e.g., tryPosn( ) in WrapNetFBF and Board) are used by the client to execute its moves and to execute the moves of the other player. The methods all take the player ID as an argument, so the owner of the move is clear. As mentioned before, the client could still be processing its move when a request to process the opponent's move comes in. This situation is handled by the use of the synchronized keyword with doMove( ): a new call to doMove( ) must wait until the current call has finished. Figure 31-16. Sequence diagram for player 2's clientWriting on the CanvasOverlayCanvas is a subclass of Canvas3D which draws a status string onto the canvas in its top-left corner (see Figure 31-11). The display is implemented as an overlay, meaning that the string is not part of the 3D scene; instead, it's resting on "top" of the canvas. This technique utilizes Java 3D's mixed mode rendering, which gives the programmer access to Java 3D's rendering loop at different stages in its execution. The client makes regular calls to setStatus( ) in NetFourByFour to update a global status string: synchronized public void setStatus(String msg) //in NetFourByFour { status = msg; } This string is periodically accessed from the OverlayCanvas object by calling getStatus( ): synchronized public String getStatus( ) // in NetFourByFour { return status; } The get and set methods are synchronizedthe calls from OverlayCanvas may come at any time and should not access the string while it's being updated. The OverlayCanvas object is created in the constructor of WrapNetFBF, in the usual way that a Canvas3D object is created: GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration( ); OverlayCanvas canvas3D = new OverlayCanvas(config, fbf); add("Center", canvas3D); canvas3D.setFocusable(true); // give focus to the canvas canvas3D.requestFocus( ); The only visible difference is the passing of a reference to the NetFourByFour object into the OverlayCanvas constructor (the fbf variable). This is used by OverlayCanvas to call the getStatus( ) method in NetFourByFour. Mixed mode renderingCanvas3D provides four methods for accessing Java 3D's rendering loop: preRender( ), postRender( ), postSwap( ), and renderField( ). By default, these methods have empty implementations and are called automatically at various stages in each cycle of the rendering loop. I can utilize them by subclassing Canvas3D and by providing implementations for the required methods. Here are the four methods in more detail:
Drawing on the overlay canvaspostSwap( ) is used to draw the updated status string on screen: // globals private final static int XPOS = 5; private final static int YPOS = 15; private final static Font MSGFONT = new Font( "SansSerif", Font.BOLD, 12); private NetFourByFour fbf; private String status; public void postSwap( ) { Graphics2D g = (Graphics2D) getGraphics( ); g.setColor(Color.red); g.setFont( MSGFONT ); if ((status = fbf.getStatus( )) != null) // it has a value g.drawString(status, XPOS, YPOS); // this call is made to compensate for the javaw repaint bug, Toolkit.getDefaultToolkit( ).sync( ); } // end of postSwap( ) The call to getStatus( ) in NetFourByFour may return null at the start of the client's execution if the canvas is rendered before status gets a value. The repaint( ) and paint( ) methods are overridden: public void repaint( ) // Overriding repaint( ) makes the worst flickering disappear { Graphics2D g = (Graphics2D) getGraphics( ); paint(g); } public void paint(Graphics g) // paint( ) is overridden to compensate for the javaw repaint bug { super.paint(g); Toolkit.getDefaultToolkit( ).sync( ); } repaint( ) is overridden to stop the canvas from being cleared before being repainted, which otherwise causes a nasty flicker. The calls to sync( ) in postSwap( ) and paint( ) are bug fixes to avoid painting problems when using javaw to execute Java 3D mixed-mode applications. |