macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Building a Game Engine with Cocoa, Part 2

by Matthew Russell
01/05/2007

Last time, we started the initial implementation for building a simple game engine with Cocoa. We finished with a working board that would draw itself and allow us to move pieces freely around on it. In this episode, we'll extend this work and add a little more pizzazz to the game board itself by introducing the logic for generating valid moves for Lines of Action, our sample game of choice. With a snazzy game board and move generation out of the way, we'll be poised nicely to dive right into the game tree search in the next installment.

If you want to follow along with the updated source code opened in another tab, here are the relevant files:

You can download the entire project for this piece here.

User Interface Updates

There are a number of updates to the game board that can drastically increase the overall usability of our board game. One helpful change is to keep a game piece highlighted once it has been clicked, if it belongs to the player who is moving. This mechanism alerts the user that a piece has indeed been selected, and presumably, the next click designates where the piece should move. Another related change is to highlight squares on the board that correspond to valid move locations once a piece has been selected.

These two approaches in combination provide a great deal of clarity as to what's going on and increase the overall fun factor in a strategy game like Lines of Action; they allow you to spend more time strategizing and less time trying to make sure you haven't overlooked an obvious possibility for moving or capturing a piece. Plus, if you've never played before, they definitely facilitate the learning process.

The changes to get these effects in place are primarily reflected in mouseDown:, mouseMoved:, and drawBoardBackgroundInRect:. Here are the new versions of these methods with some of the most interesting portions highlighted. Ignore the self-descriptive macros, but only for the moment. We'll discuss them, along with the summary of the changes, later.

- (void)mouseDown:(NSEvent*)event {
    //get the mouse position in view coordinates
    NSPoint mouse;
    mouse = [self convertPoint: [event locationInWindow]  fromView: nil];
    
    //if there was a previous click, save it
    NSPoint previousPoint = selectedCoord;
    
    //get the new board square that was clicked in.
    selectedCoord = [self boardCoordForClickPoint:mouse];
    
    //starting point for a move must be the player moving
    if (
        EQUAL_NSPOINTS(previousPoint, NIL_POINT) &&
        board[(int)selectedCoord.x][(int)selectedCoord.y] != playerMoving
        ) {
        selectedCoord = NIL_POINT;
        return;
    }
    //ending point for a move must not be on the player moving
    if (NOTEQUAL_NSPOINTS(previousPoint, NIL_POINT)) 
        if (board[(int)selectedCoord.x][(int)selectedCoord.y] == playerMoving)
            hoveredCoord = selectedCoord;
        else if (![currentPlayerMoves 
        containsObject:[NSValue valueWithPoint:selectedCoord]]) {
            //ignore it. it's not a valid move
            if (NOTEQUAL_NSPOINTS(previousPoint, NIL_POINT)) {
                selectedCoord = previousPoint;
                return;
            }
        }
            
    //generate moves here
    if (currentPlayerMoves)
        [currentPlayerMoves release];    
    currentPlayerMoves = [[self validMovesFromSpot:selectedCoord] retain];
    
    if (
        NOTEQUAL_NSPOINTS(previousPoint, NIL_POINT)
        && board[(int)selectedCoord.x][(int)selectedCoord.y] != playerMoving
        ) {
    

    [self movePieceFromCoord:previousPoint toCoord:selectedCoord];
    selectedCoord = NIL_POINT;
    [currentPlayerMoves release];
    currentPlayerMoves = nil;
    
    if (playerMoving == PLAYER_1)
        playerMoving = PLAYER_2;
    else
        playerMoving = PLAYER_1;
    }
    
    
    //set display now that currentPlayerMoves is populated
    [self setNeedsDisplay:YES];
    
}

- (void)mouseMoved:(NSEvent *)event {
    // get the mouse position in view coordinates
    NSPoint mouse;
    mouse = [self convertPoint: [event locationInWindow]  fromView: nil];
    
    //no need to redraw anything in this case, the piece is still highlighted
    if (NOTEQUAL_NSPOINTS(hoveredCoord, NIL_POINT) &&
        [piecesPaths[(int)hoveredCoord.x][(int)hoveredCoord.y] containsPoint: mouse])
        return;
    
    //otherwise, check to see if a piece is being moused over
    int i; int j;
    for (i = 0; i < DIMENSION; i++) 
        for (j = 0; j < DIMENSION; j++) 
            if (board[i][j] != EMPTY)
                if ([piecesPaths[i][j] containsPoint: mouse] &&
                EQUAL_NSPOINTS(selectedCoord, NIL_POINT)) {
                    hoveredCoord = NSMakePoint(i,j);
                    [self setNeedsDisplay:YES];
                    return;
                }
                    
    //no pieces are being moused over if we've made it to here, so there's
    //no need to redraw unless we need to un-highlight a piece, but only 
    //unhighlight if the piece is not unselected
    if (NOTEQUAL_NSPOINTS(hoveredCoord, NIL_POINT) &&
        EQUAL_NSPOINTS(selectedCoord, NIL_POINT)) {
        [self setNeedsDisplay:YES];
        hoveredCoord = NIL_POINT;
    }
} 

- (void)drawBoardBackgroundInRect:(NSRect)rect {
    int i; int j;
    for (i=0; i < DIMENSION; i++)
        for (j=0; j < DIMENSION; j++) {
            if (currentPlayerMoves && [currentPlayerMoves containsObject:
            POINT_OBJECT(i,j)])
                [self drawRectForBoardCoord:NSMakePoint(i,j) andHighlight:YES];
            else
                [self drawRectForBoardCoord:NSMakePoint(i,j) andHighlight:NO];
        }    
}

All of the macros you see are defined at the top of GameBoard.m and are intended to be more self-descriptive and readable than their encompassing logic appears. A motivator for defining these macros was the repetitiveness involved in much of the conditional logic that was introduced with move generation, as well as the repetitive casting of NSPoint's .x and .y components to int values. (A useful exercise in refactoring the code base before next time would be to replace all of the NSPoint references with a custom struct that holds two integer values.) A summary of the macros introduced follows, along with introductory comments.

//a way of designating an undefined point on the board
#define NIL_POINT NSMakePoint(-1,-1)

//ways for checking if two NSPoints are equal or not
#define EQUAL_NSPOINTS(p1,p2) (p1.x == p2.x && p1.y == p2.y)
#define NOTEQUAL_NSPOINTS(p1,p2) (p1.x != p2.x || p1.y != p2.y)

//descriptive shortcuts for checking board conditions
#define PLAYER_MOVING_NOT_AT(x,y) (board[(int)(x)][(int)(y)] != playerMoving)
#define OPPONENT_NOT_BLOCKING(c1,p1,p2) (![self opponentInLineBetweenCoord:(c1) \
andCoord:NSMakePoint((p1),(p2))])
#define OPPONENT_AT(x,y) ( (board[(int)(x)][(int)(y)] != EMPTY && \
board[(int)(x)][(int)(y)] != playerMoving) )

//NSPoints (structs) can't be loaded into arrays, so wrap/retrieve them via NSValue
#define POINT_OBJECT(x,y) ([NSValue valueWithPoint:NSMakePoint((x),(y))])

Getting back to the actual changes that are related to the highlighted pieces, you see in mouseDown: that we're still tracking where the user clicks on the board. However, we're also keeping track of whose turn it is to move, via a new instance variable called playerMoving, so that we can always highlight the most recent piece the player moving has clicked and force him to make a valid move based upon the array of results returned by a method called validMovesFromSpot: (more on this shortly). A small change in mouseMoved: now results in the selected piece remaining highlighted if it was clicked and belongs to the player moving.

Knowing which piece is currently selected (as you're no doubt thinking by now) also allows us to highlight spaces on the board, which is the other change we'd like to make to the user interface. The method drawBoardBackgroundInRect: handles this change by inspecting the contents of another instance variable, currentPlayerMoves, whenever it is asked to draw the board. But when is drawBoardBackgroundInRect: called? It is called first thing, back in drawRect:, which we trigger at the very end of mouseDown: with the [self setNeedsDisplay:YES] call.

Overall, there's not a ton of work involved with these changes, and yet they pack a significant punch with respect to overall usability and enjoyment. We'll get to the details of the move generation next, but go ahead and take a look at Figures 1 and 2 to see how our new game board shows you which moves you have available when you're trying to move.

New Game Board - Select a piece to move
Figure 1. The new game board makes it simple to see which moves are available for each piece on the board.

New Game Board - Board after several moves
Figure 2. The board after several valid moves from both sides.

Pages: 1, 2

Next Pagearrow