Comparing NetFourByFour and FourByFour


The Standalone Tic-Tac-Toe Game

Figure 31-2 gives the class diagrams for the standalone tic-tac-toe game, FourByFour. The class names and public methods are shown.

The FourByFour class is a Java 3D JFrame, which contains a GUI made up of a WrapFourByFour object for the 3D canvas and a text field for messages. The public showMessage( ) method allows objects to write into that field.

Figure 31-2. Class diagrams for FourByFour


WrapFourByFour constructs the scene: the lights, background, the parallel projection, and 16 poles, but leaves the initialization of the markers to a Positions object. WrapFourByFour creates a PickDragBehavior object to handle mouse picking and dragging.

The Board object contains the game logic, with data structures representing the current state of the board and methods for making a move and reporting a winner.

The FourByFour application can be found in the FourByFour/ directory.


The Origins of the Game

A tic-tac-toe game, FourByFour, has been part of the Java 3D distribution for many years. It has a more extensive GUI than the version described in this chapter, supporting repeated games, varying skill levels, and listing the high scores. A crucial difference is that the demo pits the machine against a single player, rather than player versus player. This requires a much more complicated Board class, weighing in at 2,300 lines (compared to 300 in my version of Board). It's possible to change the machine's skill level (from dumb to expert), which makes Board carry out increasingly comprehensive analyses of the player's moves and the current game state. The original Board renders the game to a 2D window in addition to a 3D canvas.

The original FourByFour demo utilizes its own versions of the Box and Cylinder utilities and builds its GUI with AWT. My version uses the Java 3D shape utility classes and Swing. The demo employs its own ID class to number the marker shapes; my code uses the userData field of the Java 3D Shape3D class.

Building the Game Scene

My WrapFourByFour class uses the same coding style as earlier 3D examples in that it creates the 3D canvas and adds the scene in createSceneGraph( ):

     private void createSceneGraph(Canvas3D canvas3D, FourByFour fbf)     {       sceneBG = new BranchGroup(  );       bounds = new BoundingSphere(new Point3d(0,0,0), BOUNDSIZE);             // Create the transform group which moves the game       TransformGroup gameTG = new TransformGroup(  );       gameTG.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);       gameTG.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);       sceneBG.addChild(gameTG);           lightScene(  );         // add the lights       addBackground(  );      // add the background           gameTG.addChild( makePoles(  ) );       // add poles           // posns holds the spheres/boxes which mark a player's turn.       // Initially posns displays a series of small white spheres.       Positions posns = new Positions(  );           // board tracks the players' moves on the game board       Board board = new Board(posns, fbf);           gameTG.addChild( posns.getChild(  ) );  // add markers           mouseControls(canvas3D, board, gameTG);           sceneBG.compile(  );   // fix the scene     } 

A transformGroup, gameTG, is added below the scene's BranchGroup; it's used by the PickDragBehavior object to rotate the game when the mouse is dragged. Consequently, all the visible game objects (e.g., poles, markers) are linked to gameTG; static entities (e.g., the lights, background) are connected to sceneBG.

makePoles( ) creates the 16 poles where the game markers appear. The initial positions of the poles are shown in Figure 31-3.

The first pole (the leftmost pole of the first row) is centered at (-30,0,30) and has length 60 units. The other three poles in the row are spaced at 20-unit intervals to the right, and the next row begins, 20 units along the z-axis. There's no particular significance to these dimensions, aside from making the various parts of the game easier to see:

     private BranchGroup makePoles(  )     {       Color3f grey = new Color3f(0.25f, 0.25f, 0.25f);       Color3f black = new Color3f(0.0f, 0.0f, 0.0f); 

Figure 31-3. The game poles


       Color3f diffuseWhite = new Color3f(0.7f, 0.7f, 0.7f);       Color3f specularWhite = new Color3f(0.9f, 0.9f, 0.9f);           // Create the pole appearance       Material poleMaterial =           new Material(grey, black, diffuseWhite,specularWhite,110.f);       poleMaterial.setLightingEnable(true);       Appearance poleApp = new Appearance(  );       poleApp.setMaterial(poleMaterial);            BranchGroup bg = new BranchGroup(  );       float x = -30.0f;       float z = -30.0f;           for(int i=0; i<4; i++) {         for(int j=0; j<4; j++) {           Transform3D t3d = new Transform3D(  );           t3d.set( new Vector3f(x, 0.0f, z) );           TransformGroup tg = new TransformGroup(t3d);           Cylinder cyl = new Cylinder(1.0f, 60.0f, poleApp);           cyl.setPickable(false);  // user cannot select the poles           tg.addChild( cyl );           bg.addChild(tg);           x += 20.0f;         }         x = -30.0f;         z += 20.0f;       }       return bg;     }  // end of makePoles(  ) 

A pole is represented by a Cylinder, below a transformGroup which positions it. The poles (and transforms) are grouped under a BranchGroup. The cylinders are made unpickable, which simplifies the picking task in PickDragBehavior.

mouseControls( ) creates a PickDragBehavior object, attaching it to the scene:

     private void mouseControls(Canvas3D c,Board board, TransformGroup gameTG)     { PickDragBehavior mouseBeh =                           new PickDragBehavior(c, board, sceneBG, gameTG);       mouseBeh.setSchedulingBounds(bounds);       sceneBG.addChild(mouseBeh);     } 

initUserPosition( ) modifies the view to use parallel projection and moves the viewpoint along the +z axis so the entire game board is visible and centered in the canvas:

     private void initUserPosition(  )     {       View view = su.getViewer(  ).getView(  );       view.setProjectionPolicy(View.PARALLEL_PROJECTION);           TransformGroup steerTG = su.getViewingPlatform(  ).getViewPlatformTransform(  );       Transform3D t3d = new Transform3D(  );       t3d.set(65.0f, new Vector3f(0.0f, 0.0f, 400.0f));       steerTG.setTransform(t3d);     } 

Building the Game Markers

The Positions object creates three sets of markers: 64 small white balls, 64 larger red balls, and 64 blue cubes. The white balls are visible, the other shapes are invisible when the game starts. When a player makes a move, the selected white ball is replaced by a red one (if it was player 1's turn) or a blue cube (for player 2).

This functionality is achieved by creating three Java 3D Switch nodes, one for each set of markers, linked to the scene with a Group node, as shown in Figure 31-4.

Figure 31-4. Scene graph branch for the game markers


Each Shape3D is positioned with a transformGroup and allocated a bit in a Java 3D BitSet object corresponding to its position in the game (positions are numbered 0 to 63). The BitSet is used as a mask in the Switch node to specify what shapes are visible or invisible.

The three Switch branches are created with calls to makeWhiteSpheres( ), makeRedSpheres( ), and makeBlueCubes( ), which are functionally similar. The code for makeWhiteSpheres( ) is:

     private void makeWhiteSpheres(  )     {       // Create the switch nodes       posSwitch = new Switch(Switch.CHILD_MASK);       // Set the capability bits       posSwitch.setCapability(Switch.ALLOW_SWITCH_READ);       posSwitch.setCapability(Switch.ALLOW_SWITCH_WRITE);       posMask = new BitSet(  );   // create the bit mask           Sphere posSphere;       for (int i=0; i<NUM_SPOTS; i++) {          Transform3D t3d = new Transform3D(  );          t3d.set( points[i] );    // set position          TransformGroup tg = new TransformGroup(t3d);          posSphere = new Sphere(2.0f, whiteApp);          Shape3D shape = posSphere.getShape(  );          shape.setUserData( new Integer(i) );                    // add board position ID to each shape          tg.addChild( posSphere );          posSwitch.addChild(tg);          posMask.set(i);    // make visible       }       // Set the positions mask       posSwitch.setChildMask(posMask);           group.addChild( posSwitch );      } // end of makeWhiteSpheres(  ) 

All the game marker Shape3D objects are pickable by default, which means that the PickDragBehavior object can select them.

An important feature of makeWhiteSpheres( ) is that each ball is assigned user data (i.e., an Integer object holding its position index). makeRedSpheres( ) and makeBlueCubes( ) don't set user data for their markers.

This difference is denoted by little numbered boxes in Figure 31-4.


The integer field for a selected white ball is read by PickDragBehavior to determine its position. There's no need for integer fields in the red balls or blue cubes since they occupy positions on the board.

Another difference between makeWhiteSpheres( ) and the other two methods is that the white balls are all set to be visible initially, and the red balls and blue cubes are invisible. This is changed during the course of the game by calls to set( ):

     public void set(int pos, int player)     // called by Board to update the 3D scene     {       // turn off the white marker for the given position       posMask.clear(pos);       posSwitch.setChildMask(posMask);           // turn on one of the player markers       if (player == PLAYER1) {          player1Mask.set(pos);          player1Switch.setChildMask(player1Mask);   // red for p1       }       else if (player == PLAYER2) {            player2Mask.set(pos);          player2Switch.setChildMask(player2Mask);   // blue for p2       }       else  // should not happen          System.out.println("Illegal player value: " + player);     } 

The pos argument is the position index (a number between 0 and 63), extracted from the user data field of the selected white marker. The player value represents the first or second player.

The main design choice in the Positions class is to create all the possible markers at scene creation time. This makes the scene's initialization a little slower, but the rendering speed for displaying a player's marker is improved because Shape3D nodes don't need to be attached or detached from the scene graph at runtime.

The position indexes for the markers (0-63) are tied to locations in space by the initLocations( ) method, which creates a points[] array of markers' coordinates. The positions correspond to the indexes of points[]:

     private void initLocations(  )     { points = new Vector3f[NUM_SPOTS];       int count = 0;       for (int z=-30; z<40; z+=20)         for (int y=-30; y<40; y+=20)           for (int x=-30; x<40; x+=20) {             points[count] = new Vector3f((float)x, (float)y, (float)z);             count++;           }     } 

points[] is used to initialize the transformGroups for the markers, positioning them in space.

The coordinates were chosen so the markers appear embedded in the poles. Figure 31-5 shows the first row of poles (the back row in Figure 31-3) and its 16 markers, which have position indexes 0-15.

Figure 31-5. Marker positions in the first row of poles


Picking and Dragging

The PickDragBehavior object allows the user to click on a white marker to signal that the next move should be in that position, and to utilize mouse dragging to rotate the scene. The object monitors mouse drags, presses, and releases. A mouse release is employed as a way of detecting that a new mouse drag may start during the next user interaction.

processStimulus( ) responds to the three mouse operations:

     public void processStimulus(Enumeration criteria)     {       WakeupCriterion wakeup;       AWTEvent[] event;       int id;       int xPos, yPos;       while (criteria.hasMoreElements(  )) {         wakeup = (WakeupCriterion) criteria.nextElement(  );         if (wakeup instanceof WakeupOnAWTEvent) {           event = ((WakeupOnAWTEvent)wakeup).getAWTEvent(  );           for (int i=0; i<event.length; i++) {             xPos = ((MouseEvent)event[i]).getX(  );             yPos = ((MouseEvent)event[i]).getY(  );             id = event[i].getID(  );             if (id == MouseEvent.MOUSE_DRAGGED)               processDrag(xPos, yPos);             else if (id == MouseEvent.MOUSE_PRESSED)               processPress(xPos, yPos);             else if (id == MouseEvent.MOUSE_RELEASED)               isStartDrag = true;   // a new drag may start next time            }         }       }       wakeupOn (mouseCriterion);     } // end of processStimulus(  ) 

processDrag( ) handles mouse dragging and is passed the current (x, y) position of the cursor on screen. processPress( ) deals with a mouse press, and the global Boolean isStartDrag is set to true when the mouse is released.

Dragging the board

When the user drags the mouse, a sequence of MOUSE_DRAGGED events are generated, each one including the current (x, y) position of the cursor. processDrag( ) obtains the movement covered by a single MOUSE_DRAGGED event by calculating the offset relative to the (x, y) coordinate from the previous drag event (stored in xPrev and yPrev). The x and y components of the move are converted into x- and y-axis rotations and are applied to the TRansformGroup for the board.

However, this approach only works after the first event, so the second event (and subsequent ones) have a previous coordinate to consider. The first event in a drag sequence is distinguished by the isStartDrag Boolean:

     private void processDrag(int xPos, int yPos)     {       if (isStartDrag)         isStartDrag = false;       else {  // not the start of a drag, so can calculate offset         int dx = xPos - xPrev;              // get dists dragged         int dy = yPos - yPrev;         transformX.rotX( dy * YFACTOR );    // convert to rotations         transformY.rotY( dx * XFACTOR );         modelTrans.mul(transformX, modelTrans);          modelTrans.mul(transformY, modelTrans);                         // add to existing x- and y- rotations         boardTG.setTransform(modelTrans);       }       xPrev = xPos;     // save locs so can work out drag next time       yPrev = yPos;     } 

modelTrans is a global transform3D object that stores the ongoing, total rotational effect on the board. TRansformX and TRansformY are globals. boardTG is the TRansformGroup for the board, passed in from WrapFourByFour when the PickDragBehavior object is created.

Picking a marker

processPress( ) sends a pick ray along the z-axis into the world, starting from the current mouse press position. The closest intersecting node is retrieved and if it's a Shape3D containing a position index, then that position will be used as the player's desired move.

One problem is translating the (x, y) position supplied by the MOUSE_PRESSED event into world coordinates. This is done in two stages: The screen coordinate is mapped to the canvas' image plate and then to world coordinates.

The picking code is simplified by the judicious use of setPickable(false) when the scene is set up. The poles are made unpickable when created in makePoles( ) in WrapFourByFour, which means that only the markers can be selected:

     // global     private final static Vector3d IN_VEC = new Vector3d(0.f,0.f,-1.f);             // direction for picking -- into the scene         private Point3d mousePos;     private Transform3D imWorldT3d;     private PickRay pickRay = new PickRay(  );     private SceneGraphPath nodePath;             private void processPress(int xPos, int yPos)     {       canvas3D.getPixelLocationInImagePlate(xPos, yPos, mousePos);                     // get the mouse position on the image plate       canvas3D.getImagePlateToVworld(imWorldT3d);                     // get image plate --> world transform       imWorldT3d.transform(mousePos);   // convert to world coords           pickRay.set(mousePos, IN_VEC);                      // ray starts at mouse pos, and goes straight in           nodePath = bg.pickClosest(pickRay);               // get first node along pickray (and its path)       if (nodePath != null)         selectedPosn(nodePath);     } 

The image plate to world coordinates transform is obtained from the canvas and applied to mousePos, which changes it in place. A ray is sent into the scene starting from that position, and the closest SceneGraphPath object is retrieved. This should be a branch ending in a game marker or null (i.e., the user clicked on a pole or the background).

selectedPosn( ) gets the terminal node of the path and checks that it's a Shape3D containing user data (only the white markers hold data, which is their position index):

     private void selectedPosn(SceneGraphPath np)     { Node node = np.getObject(  );       // get terminal node of path       if (node instanceof Shape3D) {    // check for shape3D         Integer posID = (Integer) node.getUserData(  );  //get posn index         if (posID != null)           board.tryPosn( posID.intValue(  ) );       }     } 

The position index (as an int) is passed to the Board object where the game logic is located.

If a red or blue marker was selected, the lack of user data will stop any further processing since it's not possible for a player to make a move in a spot which has been used.

Picking comparisons

This is the third example of Java 3D picking in this book, and it's worth comparing the three approaches:

  • In Chapter 23, picking was used to select a point in the scene, and a gun rotated and shot at it. The picking was coded using a subclass of PickMouseBehavior, and details about the intersection coordinate were required.

  • In Chapter 26, a ray was shot straight down from the users' position in a landscape to get the floor height of the spot where they were standing. The picking was implemented with PickTool, and an intersection coordinate was necessary.

  • The picking employed here in processPress( ) doesn't use any of the picking utilities (i.e., PickMouseBehavior, PickTool) and only requires the shape that is first touched by the ray. Consequently, the task is simple enough to code directly, though the conversion from screen to world coordinates is somewhat tricky.

The Game Representation

The Board object initializes two arrays when it's first created: winLines[][] and posToLines[][].

winLines[][] lists all the possible winning lines in the game, in terms of the four positions that make up a line. For example, referring to Figure 31-5, {0,1,2,3}, {3,6,9,12}, and {0,4,8,12} are winning lines; the game has a total of 76 winning lines. For each line, winLines[][] records the number of positions occupied by a player. If the total reaches four for a particular line, then the player has completed the line and won.

posToLines[] specifies all the lines that utilize a given position. Thus, when a player selects a given position, all of those lines can be updated simultaneously.

Processing a selected position

The main entry point into Board is TRyPosn( ), called by PickDragBehavior to pass the player's selected position into the Board object for processing:

     public void tryPosn(int pos)     {       if (gameOver)   // don't process position when game is over         return;           positions.set(pos, player);  // change the 3D marker shown at pos       playMove(pos);               // play the move on the board           // switch players, if the game isn't over       if (!gameOver) {         player = ((player == PLAYER1) ? PLAYER2 : PLAYER1 );         if (player == PLAYER1)           fbf.showMessage("Player 1's turn (red spheres)");         else           fbf.showMessage("Player 2's turn (blue cubes)");       }     }  // end of tryPosn(  ) 

Board uses a global Boolean, gameOver, to record when the game has ended. The test of gameOver at the start of tryPosn( ) means that selecting a marker will have no effect once the game is finished.

The player's marker is made visible by a call to set( ) in the Positions object, and playMove( ) updates winLines[][]. After the move, the current player is switched; the player variable holds the current player's ID. However, the move may have been a winning one, so gameOver is checked before the switch. The calls to showMessage( ) cause the text field in the GUI to be updated.

Storing the selected position

playMove( ) uses the supplied position index to modify the various lines in which it appears. If the number of used positions in any of those lines reaches four, then the player will have won, and reportWinner( ) will be called:

     private void playMove(int pos)     {       nmoves++;                     // update the number of moves           // get number of lines that this position is involved in       int numWinLines = posToLines[pos][0];           /* Go through each line associated with this position          and update its status. If I have a winner, stop game. */           int line;       for (int j=0; j<numWinLines; j++) {         line = posToLines[pos][j+1];         if (winLines[line][1] != player &&             winLines[line][1] != UNOCCUPIED)           winLines[line][0] = -1;              /* The other player has already made a move in this line              so this line is now useless to both players. */         else {           winLines[line][1] = player;  //this line belongs to player           winLines[line][0]++;         // one more posn used in line           if (winLines[line][0] == 4) {  // all positions used,             gameOver = true;             // so this player has won             reportWinner(  );           }         }            }     } // end of playMove(  ) 

The winLines[x][1] field for line x states whether a player has made a move in that line. If a player selects a position in a line being used by another player, then the line becomes useless, which is signaled by setting winLines[x][0] == -1.

Reporting a winner

reportWinner( ) does some numerical "hand-waving" to obtain a score, based on the running time of the game and the number of moves made. The score is reported in the text field of the GUI:

     private void reportWinner(  )     {       long end_time = System.currentTimeMillis(  );       long time = (end_time - startTime)/1000;           int score = (NUM_SPOTS + 2 - nmoves)*111 - (int) Math.min(time*1000, 5000);           if (player == PLAYER1)         fbf.showMessage("Game over, player 1 wins with score "+score);       else   // PLAYER2         fbf.showMessage("Game over, player 2 wins with score "+score);     }  // end of reportWinner(  ) 



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