ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


O'Reilly Book Excerpts: Killer Game Programming in Java

Killer Game Programming in Java: A 3D Checkerboard, Part 2

Editor's note: In part one of this two-part excerpt from Killer Game Programming in Java, author Andrew Davison strode through some complex programming issues for developing Java 3D graphics, such as how to add shapes, lighting, and backgrounds to a Checkers3D application. Here in part two, Andrew continues the theme by demonstrating how to create a floating sphere for the Checkers3D app.

The Floor

The floor is made of tiles created with my ColouredTiles class, and axis labels made with the Java 3D Text2D utility class. Figure 15-5 shows the floor branch, previously hidden inside a "Floor Branch" box in Figure 15-3.

Figure 15-5
Figure 15-5. Floor branch of the scene graph

The floor subgraph is constructed with an instance of my CheckerFloor class and made available via the getBG( ) method:


    sceneBG.addChild( new CheckerFloor( ).getBG( ) );  // add the floor

The CheckerFloor( ) constructor uses nested for loops to initialize two ArrayLists. The blueCoords list contains all the coordinates for the blue tiles, and greenCoords holds the coordinates for the green tiles. Once the ArrayLists are filled, they are passed to ColouredTiles objects, along with the color that should be used to render the tiles. A ColouredTiles object is a subclass of Shape3D, so can be added directly to the floor's graph:


    floorBG.addChild( new ColouredTiles(blueCoords, blue) );
    floorBG.addChild( new ColouredTiles(greenCoords, green) );

The red square at the origin (visible in Figure 15-1) is made in a similar way:


    Point3f p1 = new Point3f(-0.25f, 0.01f, 0.25f);
    Point3f p2 = new Point3f(0.25f, 0.01f, 0.25f);
    Point3f p3 = new Point3f(0.25f, 0.01f, -0.25f);
    Point3f p4 = new Point3f(-0.25f, 0.01f, -0.25f);

    ArrayList oCoords = new ArrayList( );
    oCoords.add(p1); oCoords.add(p2);
    oCoords.add(p3); oCoords.add(p4);

    floorBG.addChild( new ColouredTiles(oCoords, medRed) );

The square is centered at (0, 0) on the XZ plane and raised a little above the y-axis (0.01 units) so it's visible above the tiles.

Each side of the square is the length of 0.5 units. The four Point3f points in the ArrayList are stored in a counterclockwise order. This is true for each group of four points in blueCoords and greenCoords. Figure 15-6 shows the ordering of the square's points.

Figure 15-6
Figure 15-6. OrigMarker, viewed from above

Killer Game Programming in Java

Related Reading

Killer Game Programming in Java
By Andrew Davison

The Colored Tiles

My ColouredTiles class extends Shape3D and defines the geometry and appearance of tiles with the same color. The geometry uses a Java 3D QuadArray to represent the tiles as a series of quadrilaterals (quads). The constructor is



    QuadArray(int vertexCount, int vertexFormat);

The vertex format is an ORed collection of static integers, which specify the different aspects of the quad to be initialized later, such as its coordinates, color, and normals. In ColouredTiles, the QuadArray plane is created using this line of code:


    plane = new QuadArray(coords.size( ),
                          GeometryArray.COORDINATES | GeometryArray.COLOR_3 );

The size( ) method returns the number of coordinates in the supplied ArrayList. The coordinate and color data is supplied in createGeometry( ):


    int numPoints = coords.size( );
    Point3f[] points = new Point3f[numPoints];
    coords.toArray( points );   // ArrayList->array
    plane.setCoordinates(0, points);

    Color3f cols[] = new Color3f[numPoints];
    for(int i=0; i < numPoints; i++)
      cols[i] = col;
    plane.setColors(0, cols);

The order in which a quad's coordinates are specified is significant. The front of a polygon is the face where the vertices form a counterclockwise loop. Knowing front from back is important for lighting and hidden face culling, and by default, only the front face of a polygon will be visible in a scene. In this application, the tiles are oriented so their fronts are facing upward along the y-axis.

It's necessary to ensure that the points of each quad from a convex, planar polygon, or rendering may be compromised. However, each quad in the coordinates array doesn't need to be connected or adjacent to the other quads, which is the case for these tiles.

Since a quad's geometry doesn't include normals information, a Material node component can't be used to specify the quad's color when lit. I could use a ColoringAttributes, but a third alternative is to set the color in the geometry, as done here (plane.setColors(0, cols);). This color will be constant, unaffected by the scene lighting.

Once finalized, the Shape3D's geometry is set with:


    setGeometry(plane);

The shape's appearance is handled by createAppearance( ), which uses a Java 3D PolygonAttribute component to switch off the culling of the back face. PolygonAttribute can be employed to render polygons in point or line form (i.e., as wire frames), and to flip the normals of back facing shapes:


    Appearance app = new Appearance( );
    PolygonAttributes pa = new PolygonAttributes( );
    pa.setCullFace(PolygonAttributes.CULL_NONE);
    app.setPolygonAttributes(pa);

Once the appearance has been fully specified, it's fixed in the shape with


    setAppearance(app);

The Floor's Axis Labels

The floor's axis labels are generated with the labelAxes( ) and makeText( ) methods in CheckerFloor( ). labelAxes( ) uses two loops to create labels along the x and z. Each label is constructed by makeText( ) and then added to the floor's BranchGroup (see Figure 15-5):


    floorBG.addChild( makeText(pt,""+i) );

makeText( ) uses the Text2D utility class to create a 2D string of a specified color, font, point size, and font style:


    Text2D message = new Text2D(text, white, "SansSerif", 36, Font.BOLD);
                            // 36 point bold Sans Serif

A Text2D object is a Shape3D object with a quad geometry (a rectangle), and appearance given by a texture map (image) of the string, placed on the front face. By default, the back face is culled; if the user moves behind an axis label, the object becomes invisible.

The point size is converted to virtual-world units by dividing by 256. Generally, it's a bad idea to use too large a point size in the Text2D( ) constructor since the text may be rendered incorrectly. Instead, a TransformGroup should be placed above the shape and used to scale it to the necessary size.

The positioning of each label is done by a TransformGroup above the shape:


    TransformGroup tg = new TransformGroup( );
    Transform3D t3d = new Transform3D( );
    t3d.setTranslation(vertex);   // the position for the label
    tg.setTransform(t3d);
    tg.addChild(message);

setTranslation( ) only affects the position of the shape. The tg TransformGroup is added to the floor scene graph.

Viewer Positioning

The scene graph in Figure 15-3 doesn't include the view branch graph; that branch is shown in Figure 15-7.

Figure 15-7
Figure 15-7. The view branch graph

The branch is created by a call to the SimpleUniverse constructor in the WrapCheckers3D( ) constructor:


    su = new SimpleUniverse(canvas3D);

SimpleUniverse offers simplified access to the view branch graph via the ViewingPlatform and Viewer classes, which are mapped to the graph (shown as dotted rectangles in Figure 15-7).

ViewingPlatform is used in initUserPosition( ) to access the TransformGroup above the ViewPlatform node:


    ViewingPlatform vp = su.getViewingPlatform( );
    TransformGroup steerTG = vp.getViewPlatformTransform( );

steerTG corresponds to the TG node in Figure 15-7. Its Transform3D component is extracted and changed with the lookAt( ) and invert( ) methods:


    Transform3D t3d = new Transform3D( );
    steerTG.getTransform(t3d);


    t3d.lookAt( USERPOSN, new Point3d(0,0,0), new Vector3d(0,1,0));
    t3d.invert( );

    steerTG.setTransform(t3d);

lookAt( ) is a convenient way to set the viewer's position in the virtual world. The method requires the viewer's intended position, the point that she is looking at, and a vector specifying the upward direction. In this application, the viewer's position is USERPOSN (the (0, 5, 20) coordinate); she is looking toward the origin (0, 0, 0), and "up" is along the positive y-axis. This is illustrated by Figure 15-8.

Figure 15-8
Figure 15-8. lookAt( ) depicted graphically

invert( ) is required since the position is relative to the viewer rather than an object in the scene.

Viewer Movement

The user is able to move through the scene by utilizing the Java 3D OrbitBehavior utility class in the view graph. A combination of control keys and mouse button presses move and rotate (or orbits) the viewer's position.

The behavior is set up in orbitControls( ) in WrapCheckers3D:


    OrbitBehavior orbit = new OrbitBehavior(c, OrbitBehavior.REVERSE_ALL);
    orbit.setSchedulingBounds(bounds);
    ViewingPlatform vp = su.getViewingPlatform( );
    vp.setViewPlatformBehavior(orbit);

The REVERSE_ALL flag ensures that the viewpoint moves in the same direction as the mouse.

TIP: Numerous other flags and methods affect the rotation, translation, and zooming characteristics, explained in the OrbitBehavior class documentation.

MouseRotate, MouseTranslate, and MouseZoom are similar behavior classes that appear in many Java 3D examples; their principal difference from OrbitBehavior is that they affect the objects in the scene rather than the viewer.

TIP: Most games, such as first-person shooters (FPS), require greater control over the viewer's movements than these utility behaviors can offer, so I'll be implementing my own behaviors in later chapters.

Viewing the Scene Graph

This chapter has used scene graphs to illustrate the discussed coding techniques, and scene graphs are a useful way of understanding (and checking) code.

I received help with my drawings by using Daniel Selman's Java3dTree package. It creates a JFrame holding a textual tree representation of the scene graph (Figure 15-9).

Figure 15-9
Figure 15-9. Java3dTree representation of the Checkers3D scene graph

The tree (a JTree object) is initially minimized, and branches can be examined by clicking on the subfolder icons. Information about the currently selected node appears in the bottom window. The package is available in j3dtree.jar as part of the source code downloadable from http://www.manning.com/selman/ for Selman's Java 3D Programming text.

Augmenting code to generate the JTree is simple. WrapCheckers3D must import the j3dtree package and declare a global variable for the JFrame tree display:



    import com.sun.j3d.utils.behaviors.vp.*;

    private Java3dTree j3dTree;

The WrapCheckers3D( ) constructor creates the j3dTree object:


    public WrapCheckers3D( )
    {
      // other code
      su = new SimpleUniverse(canvas3D);

      j3dTree = new Java3dTree( );   // create a display tree for the SG

      createSceneGraph( );
      initUserPosition( );
      orbitControls(canvas3D);
      su.addBranchGraph( sceneBG );

      j3dTree.updateNodes( su );    // build the tree display window
    }

After the scene graph has been completed, (i.e., at the end of the constructor), the tree display is built with a single line:


    j3dTree.updateNodes( su );

However, prior to this, the capabilities of the scene graph nodes must be adjusted with:


    j3dTree.recursiveApplyCapability( sceneBG );

This operation should be carried out after the content branch group (sceneBG) has been completed, but before it is compiled or made live. In my code, this means adding the line to createSceneGraph( ):


    private void createSceneGraph( )
    {
      sceneBG = new BranchGroup( );
      // other code to create the scene

    
      j3dTree.recursiveApplyCapability( sceneBG );

      sceneBG.compile( );
    }

Unfortunately, you can't just call:


    j3dTree.recursiveApplyCapability( su );

without generating errors because the SimpleUniverse( ) constructor has made the ViewingPlatform live, which prevents further changes to its capabilities.

Since only the capabilities in the content branch have been adjusted, the call to updateNodes( ) will generate some warning messages when the view branch below the Locale node is encountered.

Compilation and execution must include j3dtree.jar in the classpath. My preferred approach is to do this via command line arguments:



    javac -classpath "%CLASSPATH%;j3dtree.jar" *.java

    java -cp "%CLASSPATH%;j3dtree.jar" Checkers3D

TIP: If typing the classpath repeatedly isn't to your taste, command lines like these can be hidden inside batch files or shell scripts.

The Java3dTree object is a textual representation of the scene, which means that I had to draw the scene graph myself. But the plus side is that tree generation has negligible impact on the rest of the program.

Another solution is to use the Java 3D Scene Graph Editor (http://java3d.netbeans.org/j3deditor_intro.html). This displays a graphical version of the scene graph but has the downside that its installation and usage are complicated and the memory requirements may be severe on some machines.


View catalog information for Killer Game Programming in Java

Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.