macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Building a Game Engine with Cocoa
Pages: 1, 2, 3

Getting into the Details

That concludes the project setup and the core logic for drawing the board and the pieces on it. Now, let's have a closer look at a few of the drawing details:



//the board should have a checkered appearance
-(NSColor*)squareColorForBoardCoord:(NSPoint)p {
    return ((int)p.x % 2 == (int)p.y % 2) ? boardColors[0] : boardColors[1]; 
}

//draw the background of the board
- (void)drawBoardBackgroundInRect:(NSRect)rect {
    int i; int j;
    for (i=0; i < DIMENSION; i++)
        for (j=0; j < DIMENSION; j++) {
            [[self squareColorForBoardCoord:NSMakePoint(i,j)] set];
            [self drawRectForBoardCoord:NSMakePoint(i,j)];            
        }    
}

//call though to drawRectForBoardCoord:andHighlight: and default highlighting to NO
- (void) drawRectForBoardCoord:(NSPoint)p {
    [self drawRectForBoardCoord:p andHighlight:NO];
}

//given a board coordinate such as (1,1), draw that board square and optionally highlight it
- (void)drawRectForBoardCoord:(NSPoint)p andHighlight:(BOOL)h {
    if (!h)
        [NSBezierPath fillRect:[self rectForBoardCoord:p]];
    else {
        NSLog(@"highlighting");
        NSRect r = [self rectForBoardCoord:p];
        [[[self squareColorForBoardCoord:p] blendedColorWithFraction:0.6 ofColor:[NSColor yellowColor]] set];
        [NSBezierPath fillRect:r];
    }
}

//find the drawing area on the view that corresponds to the coordinate 
//board is like quadrant 1 of cartesian plane with origin in lower left.
- (NSRect) rectForBoardCoord:(NSPoint)p {
    NSRect rect = [self bounds];
    
    NSRect r;
    r.origin = NSMakePoint(rect.origin.x+ p.x*squareDim, rect.origin.y+ p.y*squareDim);
    r.size = NSMakeSize(squareDim, squareDim);
    
    return r;
}

The method squareColorForBoardCoord: takes a coordinate in the form of an NSPoint as its argument and, based on that coordinate, returns which color the square should be. This information is used repetitively in drawBoardBackgroundInRect: for drawing the board, which is simply a nested loop that iterates over each square on the board.

The method drawRectForBoardCoord: is defined next; it simply calls through to drawRectForBoardCoord:andHighlight with highlighting declined and produces a square on the board. Again, note that the instance variable squareDim is used in these calculations.

Coming up next is the most complex drawing routine and another block of code that determines the board coordinates that correspond to a mouse click from inside the view.

//given a board coordinate such as (1,1), draw the piece, which may get highlighted
//if the mouse is hovering over it.
- (NSBezierPath*)drawPieceForCoord:(NSPoint)p andPlayer:(int)player {    
    NSRect r = [self rectForBoardCoord:p];
    
    //shrink the rectangle a bit so that pieces don't touch the edges
    r.origin.x += PIECE_MARGIN/2;
    r.origin.y += PIECE_MARGIN/2;
    r.size.width -= PIECE_MARGIN;
    r.size.height -= PIECE_MARGIN;
    
    NSBezierPath *path;
    
    path = [NSBezierPath bezierPathWithOvalInRect:r];
    
    if (hoveredCoord.x == p.x && hoveredCoord.y == p.y) {
        [[NSColor yellowColor] set];
        [path fill];
        
        NSRect highlightedRect = r;
        
        highlightedRect.origin.x +=HIGHLIGHT_MARGIN/2;
        highlightedRect.origin.y +=HIGHLIGHT_MARGIN/2;
        highlightedRect.size.width -= HIGHLIGHT_MARGIN;
        highlightedRect.size.height -= HIGHLIGHT_MARGIN;
        NSBezierPath *body = [NSBezierPath bezierPathWithOvalInRect:highlightedRect];
        
        [playerColors[player] set];
        [body fill];
    }
    else { 
        [playerColors[player] set];
        [path fill];
    }
    
    //Give your board pieces a nice texture with something like this...
    NSBezierPath *texture;
    while (r.size.width > 0) {
        r.origin.x += 2;
        r.origin.y += 2;
        r.size.width -= 4;
        r.size.height -= 4;
        texture = [NSBezierPath bezierPathWithOvalInRect:r];
        [[NSColor whiteColor] set];
        [texture stroke];
    }
    
    return path;
}

//based on a mouse click, determine the corresponding board coordinate
-(NSPoint)boardCoordForClickPoint:(NSPoint)p {
    int x=-1; int y=-1;
    
    while (p.x > 0) {  
        p.x -= squareDim;
        x++;
    }
    
    while (p.y > 0) {   
        p.y -= squareDim;
        y++;
    }
    
    return NSMakePoint(x,y);
}

//used for moving
- (void)movePieceFromCoord:(NSPoint)p1 toCoord:(NSPoint)p2 {
    NSLog(@"Attempting to move piece from (%f,%f) to (%f,%f)", p1.x,p1.y,p2.x,p2.y);
    
    //make sure move isn't in place
    if (p1.x == p2.x && p1.y == p2.y)
        return;
    
    //make sure there is a piece at p1
    if (board[(int)p1.x][(int)p1.y] == EMPTY)
        return;
    
    //update the board
    int player = board[(int)p1.x][(int)p1.y];
    board[(int)p1.x][(int)p1.y] = EMPTY;
    board[(int)p2.x][(int)p2.y] = player;
    
    [self setNeedsDisplay:YES];
}

In reality, drawPieceForCoord:andPlayer: looks more complex than it is. When everything is said and done, this method simply draws a fancy circle on the board. In all cases, it uses a small margin that sizes a piece so that it fits neatly inside of a square, and uses some cheap parlor tricks with NSBezierPaths to texturize the board pieces so that they don't look so dull. The most interesting work, however, occurs if the mouse is over the piece. If the mouse is hovering over a piece, the piece is drawn in a highlighted fashion to provide a visual cue to the user that the piece can be selected. The actual mechanics of determining if the mouse is over the piece (not just the square, but the piece in the square) relies on the matrix of bezier paths (piecesPaths) that are stored outside this method. This method simply references the instance variable hoveredCoord, which is updated in mouseMoved (coming up shortly).

While rectForBoardCoord: takes a coordinate and returns a rectangle for drawing on the view, the method boardCoordForClickPoint: basically does the reverse. Given a click on the view (which takes place inside of a board square), it returns the coordinate for this board square. This method comes in handy when it's time to move pieces on the board, since it allows us to know the source and destination for a piece that we can use to redraw. In fact, this is exactly what movePieceFromCoord: does to create the effect of moving a piece. It first verifies that an actual piece is being moved to a different square, and then performs the swap in the board matrix and flags the display as stale. The display updates when drawRect: kicks in and handles the redraw.

The final two routines are methods that NSView inherits from NSResponder and involve mouse events.

- (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 (hoveredCoord.x != NIL_POINT.x && hoveredCoord.y != NIL_POINT.y && 
        [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]) {
                    hoveredCoord = NSMakePoint(i,j);
                    [self setNeedsDisplay:YES];
                    return;
                }
                    
                    //no pieces were being moused over. no need to redraw unless we need
                    //to un-highlight a piece
                    if (hoveredCoord.x != NIL_POINT.x && hoveredCoord.y != NIL_POINT.y) {
                        [self setNeedsDisplay:YES];
                        hoveredCoord = NIL_POINT;
                    }
} 

- (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 = NIL_POINT;
    if (selectedCoord.x != NIL_POINT.x && selectedCoord.y != NIL_POINT.y)
        previousPoint = selectedCoord;
    
    //get the new board square that was clicked in.
    selectedCoord = [self boardCoordForClickPoint:mouse];
    
    if (previousPoint.x != NIL_POINT.x && previousPoint.y != NIL_POINT.y) {
        [self movePieceFromCoord:previousPoint toCoord:selectedCoord];
        selectedCoord = NIL_POINT;
    }
}

The mouseMoved: routine is the more complex of the two, because it has some explicit checks in place to make sure that the view is redrawn only when necessary. Taking a moment to tinker with some of these checks is worthwhile, and you can insert an NSLog statement in drawRect: to see the impact. Though there might not be a noticeable change on the actual display, you'll find that it's very easy to redraw the display far more often than is really necessary.

Finally, mouseDown: simply keeps track of mouse clicks and maps them to board coordinates using boardCoordForClickPoint:. If a piece is clicked, then the next click results in movePiecefromCoord:toCoord trying to move the piece to a new location. Figure 4 shows the board in action.

Our board in action
Figure 4. Our board in action!

Next Time

Next time, we'll write some custom logic for Lines of Action that enforces valid (instead of arbitrary) moves to be made, and we'll give the user some better visual cues. We'll also implement a game tree search and develop a computer opponent that we can play against.

But until then, we're off to a fine start. If you're really liking this project, consider taking some time to dig into the Cocoa Drawing Guide and try to improve upon the existing board. Or go out and read about game trees.

You can download the complete project file for this article here.

Matthew Russell is a computer scientist from middle Tennessee; and serves Digital Reasoning Systems as the Director of Advanced Technology. Hacking and writing are two activities essential to his renaissance man regimen.


Return to the Mac DevCenter.