Reversi Game Display

From Gardenwiki

Jump to: navigation, search

This page is part of the Reversi Tutorial.

Contents

Step 3. Create the Game Display

One of the most important parts of a game is the user interface. Not only do we want it to be clear and intuitive, but we also want it to be exciting and dynamic. This is a game after all. To that end, we provide a media toolkit which contains a sprite and animation framework that can be used to easily create your game interface.

Sprites

A sprite is a graphical entity that is drawn at some position on the screen. The media toolkit provides an easy way to render a sprite and to move it around the display, and then handles all of the complicated figuring out of what to redraw and when, so that it all happens efficiently.

We're going to want to use a sprite to represent the pieces on our Reversi board, so let's create one now. Create a file called PieceSprite.java in the directory with all the rest of your code:

PieceSprite.java

package com.samskivert.reversi;

import java.awt.Color;
import java.awt.Graphics2D;

import com.threerings.media.sprite.Sprite;

/**
 * Displays a piece on the board view.
 */
public class PieceSprite extends Sprite
{
    /** The dimensions of our sprite in pixels. */
    public static final int SIZE = 64;

    /**
     * Creates a piece sprite to display the supplied game piece.
     */
    public PieceSprite (ReversiObject.Piece piece)
    {
        super(SIZE, SIZE);
        updatePiece(piece);
    }

    /**
     * Called when the piece we are displaying has been updated.
     */
    public void updatePiece (ReversiObject.Piece piece)
    {
        // keep track of our piece
        _piece = piece;

        // set our location based on the location of the piece
        setLocation(_piece.x * SIZE, _piece.y * SIZE);

        // force a redraw in case our color changed but not our location
        invalidate();
    }

    @Override // from Sprite
    public void paint (Graphics2D gfx)
    {
        // set our color depending on the player that owns this piece
        gfx.setColor(_piece.owner == ReversiObject.BLACK ?
                     Color.darkGray : Color.white);

        // draw a filled in circle in our piece color
        gfx.fillOval(_bounds.x, _bounds.y, _bounds.width, _bounds.height);

        // then outline that oval in black; note: fill... and draw... need to
        // be called with different width and height (see javadoc of Graphics)
        gfx.setColor(Color.black);
        gfx.drawOval(_bounds.x, _bounds.y, _bounds.width-1, _bounds.height-1);
    }

    protected ReversiObject.Piece _piece;
}

In the constructor, we tell our parent class (Sprite) how big we're going to be (in pixels). We don't have to fill in the whole rectangle, but the toolkit uses the rectangular bounds that we provide to determine when our sprite needs to be repainted. Once we've done that, we set up our initial piece information. The main thing this does is to set the location of our sprite and to keep the piece around so that we know what color to render our piece.

Later, if our piece changes, we'll call updatePiece() with the updated piece, and the sprite will change position if necessary (that will actually never happen because pieces don't move in Reversi) and it will change color if necessary (that will happen).

Then we have the paint() method which is what does the actual job of drawing the sprite to the screen. Notice in paint we use the _bounds rectangle to do our rendering. A sprite should always render relative to the current value of its _bounds rectangle because that will always be configured with the correct location of the sprite on the screen.

ReversiBoardView

Now that we have our piece sprite, we need something to use it in. That's where the board view comes into play. The board view is responsible for rendering the main game display and will use our PieceSprite class to do so.

By default, the skeleton game uses a JComponent for its board view, but we want to use the sprite and animation framework to render our board, so we're going to change that. Open up RerversiBoardView.java and you should see the following:

package com.samskivert.reversi;

import java.awt.Graphics;
import javax.swing.JComponent;

import com.threerings.toybox.util.ToyBoxContext;

import com.threerings.crowd.client.PlaceView;
import com.threerings.crowd.data.PlaceObject;

/**
 * Displays the main game interface (the board).
 */
public class ReversiBoardView extends JComponent
    implements PlaceView
{
    /**
     * Constructs a view which will initialize itself and prepare to display
     * the game board.
     */
    public ReversiBoardView (ToyBoxContext ctx)
    {
        _ctx = ctx;
    }

    // from interface PlaceView
    public void willEnterPlace (PlaceObject plobj)
    {
        _gameobj = (ReversiObject)plobj;
    }

    // from interface PlaceView
    public void didLeavePlace (PlaceObject plobj)
    {
    }

    @Override // from JComponent
    public void paintComponent (Graphics g)
    {
        super.paintComponent(g);

        // here we would render things, like our board and perhaps some
        // pieces or whatever is appropriate for this game
    }

    /** Provides access to client services. */
    protected ToyBoxContext _ctx;

    /** A reference to our game object. */
    protected ReversiObject _gameobj;
}

We want to make some changes. Delete the Graphics and JComponent imports and add a new one:

 import com.threerings.media.VirtualMediaPanel;
 import com.threerings.toybox.util.ToyBoxContext;
 
 import com.threerings.crowd.client.PlaceView;
 import com.threerings.crowd.data.PlaceObject;

Change the class to extend VirtualMediaPanel and modify the constructor like so:

 public class ReversiBoardView extends VirtualMediaPanel
     implements PlaceView
 {
     /**
      * Constructs a view which will initialize itself and prepare to display
      * the game board.
      */
     public ReversiBoardView (ToyBoxContext ctx)
     {
         super(ctx.getFrameManager());
         _ctx = ctx;
     }

Then delete the paintComponent() method because that's no longer needed.

Next we want to define a preferred size for our board because that will make it easier to test things. For now we'll default to an 8x8 board and we'll see how we can make that a customizable parameter later. Add the following protected field at the bottom:

   /** The size of the Reversi board. */
   protected Dimension _size = new Dimension(8, 8);

Don't forget to add the import statement for java.awt.Dimension. Then add a getPreferredSize() method:

   @Override // from JComponent
   public Dimension getPreferredSize ()
   {
       return new Dimension(_size.width * PieceSprite.SIZE + 1,
                            _size.height * PieceSprite.SIZE + 1);
   }

We want each grid square to be big enough to hold a PieceSprite and then we want one more pixel on the edge to draw our right and bottommost grid line.

Let's take a small detour and talk a moment about the virtual media panel and what it, and the media toolkit, do for us.

MediaPanel and VirtualMediaPanel

Swing normally uses what is called "passive rendering" which means that when a user interface component changes, it triggers an event requesting that it be repainted as soon as possible and the UI toolkit eventually calls repaint() on the component that made the request. Unless a component requests a repaint, no rendering is done. Most video games on the other hand, use what is called "active rendering".

Active rendering generally involves two phases, in the first phase the game simulation is run which updates the state of various things in the game. In the second phase, the whole screen is redrawn to reflect the updated state of all of the game elements. This two phase process is done as fast as possible, or more commonly as fast as possible up to the refresh speed of a user's monitor. So if their monitor can draw a new screen 60 times a second, the game will try to update itself and redraw 60 times a second, but no faster. One trip through this loop (performing phase one and phase two) is generally called a frame and the speed at which the game is running is called the frame rate.

The media toolkit provides an active rendering framework that works with Swing and Java. However, instead of repainting everything every time through the loop, it keeps track of what became dirty on every frame and then only repaints those elements. This helps with performance on slower machines which don't quite have the graphics horsepower to just blaze through and redraw everything 60 times per second.

Now back to the MediaPanel. You can see in the constructor, we are passing the FrameManager to our VirtualMediaPanel parent class's constructor. The FrameManager takes care of running the frame loop and doing everything that is needed for the active rendering process. In the media toolkit, we call the two phases of the frame loop tick and paint. Every time through the loop, tick() is called to update the internal state of all of the media. Those calls to tick() result in a bunch of dirty rectangles being created. Then the MediaPanel determines which of the media intersect the dirty rectangles and those media have paint() called so that they can update the display.

The main type of media managed by the MediaPanel are sprites. Sprite has two methods that allow it to participate in the active rendering process: tick() and paint(). tick() is called on every frame to update the state of the sprite and paint() is called if the sprite needs to be repainted as a result of something that happened during tick().

VirtualMediaPanel extends MediaPanel and adds support for a virtual coordinate system. A normal MediaPanel simply renders all media assuming (0, 0) is at the upper left corner of the MediaPanel component on the screen. The VirtualMediaPanel can move that (0, 0) origin around wherever it wants and contains code to efficiently redraw media as they go into and out of view. For our simple Rerversi game we don't really need the fancy virtual coordinate system, but we use it anyway because it allows us to draw all of our sprites relative to (0, 0) and then to easily move (0, 0) to a point where the board is nicely centered on the screen without complicating any of the rendering logic.

We'll end our detour here, but bear in mind that the media toolkit makes it easy to do a lot of fancy graphical effects which we'll touch on later in the tutorial.

Test Your Board View

Before we try testing the board view, let's make one more change and have the board view draw a grid. This will involve overriding the paintBehind() method. MediaPanel defines paintBehind(), paintBetween() and paintInFront() which are used to paint behind all sprites and animations, between the "front" and "back layers of sprites and animations and in front of all sprites and animations respectively. The details aren't too important, but since we want our piece sprites to show up on top of our board, we draw the board in paintBehind().

   @Override // from MediaPanel
   protected void paintBehind (Graphics2D gfx, Rectangle dirtyRect)
   {
       super.paintBehind(gfx, dirtyRect);
 
       // fill in our background color
       gfx.setColor(Color.lightGray);
       gfx.fill(dirtyRect);
 
       // draw our grid
       gfx.setColor(Color.black);
       for (int yy = 0; yy <= _size.height; yy++) {
           int ypos = yy * PieceSprite.SIZE;
           gfx.drawLine(0, ypos, PieceSprite.SIZE * _size.width, ypos);
       }
       for (int xx = 0; xx <= _size.width; xx++) {
           int xpos = xx * PieceSprite.SIZE;
           gfx.drawLine(xpos, 0, xpos, PieceSprite.SIZE * _size.height);
       }
   }

You'll need to add a few import statements to make everything compile.

Now, we could start up a server and two clients like we did at the beginning of the tutorial and we would see our board view, but that takes an awfully long time. Because the board view is usually a complex part of the game that inevitably needs a lot of tweaking, we have a separate test harness that allows us to easily display our board view to make sure things are working. You can run this test harness and see your board like so:

 % ant viewtest

Note that when you run that ant command, it first compiles everything to make sure it's running the latest code and then runs the view test harness.

That's a fine grid we've got there. However, it would be nice to see our sprites in action as well. Fortunately, we can manipulate the board view in the test harness and add a few fake pieces just to see them properly rendered.

Open up ReversiBoardViewTest.java and add the following code to the initInterface() method:

 protected void initInterface ()
 {
     // add a couple of pieces to the view
     ReversiObject.Piece piece = new ReversiObject.Piece();
     piece.owner = ReversiObject.BLACK;
     piece.x = 1;
     piece.y = 2;
     _view.addSprite(new PieceSprite(piece));
 
     piece = new ReversiObject.Piece();
     piece.owner = ReversiObject.WHITE;
     piece.x = 2;
     piece.y = 2;
     _view.addSprite(new PieceSprite(piece));
 }

Now you can run the board view test harness again and see the two piece sprites properly rendered:

Image:Reversi_Board_View.png

It's a thing of beauty. Just for fun, change that last addSprite() line to the following:

       PieceSprite sprite = new PieceSprite(piece);
       _view.addSprite(sprite);
       int dx = piece.x * PieceSprite.SIZE;
       int dy = (piece.y+1) * PieceSprite.SIZE;
       sprite.move(new LinePath(new Point(dx, dy), 1000L));

and add an import for java.awt.Point and com.threerings.media.util.LinePath. Now run the view test harness again and watch the piece slide from one tile to the next.

While we're in here, those pieces seem really jammed up against the edges of the grid. Let's change the rendering of the PieceSprite to give them a little breathing room. Open up PieceSprite.java and change the paint() method:

   @Override // from Sprite
   public void paint (Graphics2D gfx)
   {
       // set our color depending on the player that owns this piece
       gfx.setColor(_piece.owner == ReversiObject.BLACK ?
                    Color.darkGray : Color.white);
 
       // draw a filled in circle in our piece color
       int px = _bounds.x + 3, py = _bounds.y + 3;
       int pwid = _bounds.width - 6, phei = _bounds.height - 6;
       gfx.fillOval(px, py, pwid + 1, phei + 1);
 
       // then outline that oval in black; note: fill... and draw... need to
       // be called with different width and height (see javadoc of Graphics)
       gfx.setColor(Color.black);
       gfx.drawOval(px, py, pwid, phei);
   }

Now run the view test harness. Ahh, much nicer.

Displaying the Game State

That testing is all well and good, but now we need to get to the task of displaying the actual game state. Let's go back to ReversiBoardView and look at a few important things.

 public class ReversiBoardView extends VirtualMediaPanel
     implements PlaceView
 {
     // from interface PlaceView
     public void willEnterPlace (PlaceObject plobj)
     {
         _gameobj = (ReversiObject)plobj;
     }
 
     // from interface PlaceView
     public void didLeavePlace (PlaceObject plobj)
     {
     }
 }

ReversiBoardView implements an interface called PlaceView which defines two methods: willEnterPlace() and didLeavePlace(). Game Gardens is built on top of a more generic framework and for our purposes a "place" is our game room and a PlaceObject is our GameObject (in this case ReversiObject). We can see that because we cast the PlaceObject provided to willEnterPlace() to our ReversiObject.

The PlaceView interface is very useful for creating views of the game state without having to do any special initialization. By implementing PlaceView and being in the user interface heirarchy, the toolkit will automatically call willEnterPlace() on every user interface component that implements PlaceView when the client enters the game room and didLeavePlace() when it leaves the game room.

This allows a user interface component that is displaying some aspect of the game state (in this case the board, but you can imagine a separate component that displayed scores, for example) to initialize itself and add any listeners that it needs to react to changes in the game object. In this case, we want to display the pieces on the board and react to pieces being added to the board and existing pieces being updated.

If you recall, we are keeping our pieces in a distributed set, so we'll want to use a SetListener to listen for additions and updates to that set. Let's go ahead and throw all the code at you and explain it bit by bit (note that only newly added code is shown):

 import java.util.HashMap;
 
 import com.threerings.presents.dobj.EntryAddedEvent;
 import com.threerings.presents.dobj.EntryUpdatedEvent;
 import com.threerings.presents.dobj.EntryRemovedEvent;
 import com.threerings.presents.dobj.SetListener;
 
 public class ReversiBoardView extends VirtualMediaPanel
     implements PlaceView, SetListener
 {
     // from interface PlaceView
     public void willEnterPlace (PlaceObject plobj)
     {
         _gameobj = (ReversiObject)plobj;
         _gameobj.addListener(this);
 
         // create sprites for all pieces currently on the board
         for (ReversiObject.Piece piece : _gameobj.pieces) {
             addPieceSprite(piece);
         }
     }
 
     // from interface PlaceView
     public void didLeavePlace (PlaceObject plobj)
     {
         _gameobj.removeListener(this);
         _gameobj = null;
     }
 
     // from interface SetListener
     public void entryAdded (EntryAddedEvent event)
     {
         if (event.getName().equals(ReversiObject.PIECES)) {
             // add a sprite for the newly created piece
             addPieceSprite((ReversiObject.Piece)event.getEntry());
         }
     }
 
     // from interface SetListener
     public void entryUpdated (EntryUpdatedEvent event)
     {
          if (event.getName().equals(ReversiObject.PIECES)) {
             // update the sprite that is displaying the updated piece
             ReversiObject.Piece piece = (ReversiObject.Piece)event.getEntry();
             _sprites.get(piece.getKey()).updatePiece(piece);
         }
    }
 
     // from interface SetListener
     public void entryRemoved (EntryRemovedEvent event)
     {
         // nothing to do here
     }
 
     /**
      * Adds a sprite to the board for the supplied piece.
      */
     protected void addPieceSprite (ReversiObject.Piece piece)
     {
         PieceSprite sprite = new PieceSprite(piece);
         _sprites.put(piece.getKey(), sprite);
         addSprite(sprite);
     }
 
     /** Contains a mapping from piece id to the sprite for that piece. */
     protected HashMap<Comparable,PieceSprite> _sprites =
         new HashMap<Comparable,PieceSprite>();
}

So we can see that in willEnterPlace() we create sprites for any pieces already on the board (we may have entered an in-progress game, so it's usually useful to assume that the game state might already contain something meaningful). We also add ourselves as a listener so that we will hear about any new pieces that are added to the ReversiObject.pieces distributed set.

In addPieceSprite() we keep a mapping from pieceId (remember that ReversiObject.Piece.getKey() returns the pieceId for that piece) to the sprite that displays that pieceId. That way we can easily look up a sprite when we receive an EntryUpdatedEvent with the ReversiObject.Piece that was updated.

We don't do anything special to remove the sprites when we leave as the MediaPanel and everything else will be disposed of and it will dispose of all of its sprites as well.

That was pretty easy and we now have a view that will automatically display the state of the game as pieces are played. In order to usefully test this out, we're going to need to move on to the next phase which is implementing user interaction: allowing users to take turns placing pieces on the board and updating the game state appropriately in response.

Next step: Implement User Interaction

Up to the Reversi Tutorial.

Personal tools