.
Believe it or not, all of the remaining work will be done right here in the GameBoard class. A sample project will be available at the end of this article, so instead of trying to actually develop and refactor the entire project file, let's step through the finished product at a high level, discussing the more interesting parts of the project. You can always work though some of the more specific details afterward and use the TalkBacks at the end of this article for specific questions or ideas you want to share. Of course, the full header and implementation files for GameBoard might be useful to have open in another tab while you follow along.
The first few methods we'll develop are fairly routine and get us through some of the typical initialization and setup.
//designated initializer for superclass - (id) initWithFrame:(NSRect) frame { if (self = [super initWithFrame: frame]) { //set up player colors playerColors[PLAYER_1] = [NSColor redColor]; playerColors[PLAYER_2] = [NSColor blackColor]; //set up board colors boardColors[0] = [NSColor lightGrayColor]; boardColors[1] = [NSColor darkGrayColor]; //initialize board int i; int j; for (i=0; i< DIMENSION; i++) for (j=0; j <DIMENSION; j++) { board[i][j] = EMPTY; piecesPaths[i][j] = nil; } //set up initial layout for a game of LOA for (i=0; i <DIMENSION; i += DIMENSION-1) for (j=1; j <DIMENSION-1; j++) { board[i][j] = PLAYER_1; board[j][i] = PLAYER_2; } //nothing is hovered or selected yet hoveredCoord = NIL_POINT; selectedCoord = NIL_POINT; //current dimension of a square on the board squareDim = frame.size.width/DIMENSION; } return self; } //outlets, etc. have been setup by here... - (void)awakeFromNib { //keep the window a square if the user resizes //(also remember to set the springs in interface builder) [[self window] setContentAspectRatio:NSMakeSize(1.0,1.0)]; //register for mouse events [[self window] setAcceptsMouseMovedEvents: YES]; } //for receiving mouse events - (BOOL) acceptsFirstResponder { return YES; } //recalculate the dimension for a board square when the view is resized - (void) setFrame: (NSRect) frame { [super setFrame: frame]; squareDim = frame.size.width/DIMENSION; } //draws the view, although never called explicitly - (void)drawRect:(NSRect)rect { [self drawBoardBackgroundInRect:rect]; //draw all pieces on the board by using the "board" //also store the bezier paths for detecting mouseovers int i; int j; for (i=0; i < DIMENSION; i++) for (j=0; j < DIMENSION; j++) if (board[i][j] == PLAYER_1) { if (piecesPaths[i][j] != nil) [piecesPaths[i][j] release]; piecesPaths[i][j] = [[self drawPieceForCoord:NSMakePoint(i,j) andPlayer:PLAYER_1] retain]; } else if (board[i][j] == PLAYER_2) { if (piecesPaths[i][j] != nil) [piecesPaths[i][j] release]; piecesPaths[i][j] = [[self drawPieceForCoord:NSMakePoint(i,j) andPlayer:PLAYER_2] retain]; } } //clean up piecesPaths - (void)dealloc { int i; int j; for (i=0; i< DIMENSION; i++) for (j=0; j< DIMENSION; j++) if (piecesPaths[i][j] != nil) [piecesPaths[i][j] release]; [super dealloc]; }
The
initWithFrame: method is the designated initializer for
NSView and is the area in which you can set up initial values for the gameboard, so it is natural that we assign colors for the board squares and each player here. One thing particularly worth pointing out is that we're calculating and storing an instance variable called
squareDim that designates the size of a square on our board. Even though we could recalculate this value on the fly every single time we need it and probably never even begin to notice the impact, it's good form to go ahead and store this value away. As we'll see in
setFrame:, we'll only recalculate this value whenever the window is resized, and the board needs to be redrawn proportional to that new size.
The method
awakeFromNib is an ideal place to register for mouse events since it's safe to reference the window by this point. Once the GameBoard registers for mouse events via
setAcceptsMouseMovedEvents, it'll receive them since it was set in Interface Builder to be the window's first responder. The actual way our board handles the mouse events is by implementing
NSResponder's methods
mouseMoved: and
mouseDown: (more on these toward the end of the article.) In case you were wondering,
NSResponder is an abstract class that the core event handling classes such as
NSWindow and
NSview inherit from.
In
awakeFromNib, we also set the aspect ratio of the window to be 1:1 so that the window will remain square if it is resized. This property keeps the board drawing simple, since boards for these types of games are usually square.
In addition to registering for mouse events in
awakeFromNib, the view must explicitly declare itself capable of being the first responder, so we take care of this in
acceptsFirstResponder:. Setting the first responder status is the mechanism that allows the view to actually receive events such as mouse drags and mouse clicks. Setting
setAcceptsMouseMovedEvents specifies that the view has methods in place for handling events such as the mouse moving.
Whenever a window resize occurs,
setFrame: is called, so we'll use it to recalculate the size of squares on the board as previously mentioned, since we'd like for the board to be able to size along with the window. As you'll see, calculating the proper dimensions for board squares is the only remotely tricky thing that's necessary to make this happen.
Inside
drawRect is where the drawing action happens, although it does use the helper functions
drawBoardBackgroundInRect: and
drawPieceForCoord: for a lot of the labor. Assuming the helper functions do what it sounds like they do, we can read through
drawRect: and see that it first draws the empty board rectangle that defines the view, and then proceeds to iterate through each of the squares on the board using a simple matrix that keeps track of the pieces.
If a particular board spot is occupied, a routine is called that draws the appropriate piece in that spot using an
NSBezierPath, and that path is retained so that we'll have access to it later. The reason for keeping these paths around is because
NSBezierPath has a method that makes it rather trivial to determine whenever the mouse hovers it; we'll use this ability to provide a rollover effect. It is noteworthy that the retains you see in this method are the only ones that appear in the entire class, and scanning on down to
dealloc shows that we clean them up when the time comes. All other drawing is accomplished with bezier paths that are autoreleased and consequently don't need to be retained.
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 && ]) { 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.
Figure 4. Our board in action!
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.