This is PacManView.m in view mode; [Download] [Up]
#import "Text.h" #import "PacManView.h" #import "PacManGameBrain.h" #import "PacManInfoController.h" #import <libc.h> // event stuff, misc. #import <daymisckit/daymisckit.h> #import "FruitView.h" #import "Maze.h" #import "Monster.h" #import "Player.h" // comment out the next line to disable the cheat modes. Cheaters can't // get net high scores, but they can get local high scores. #define CHEATMODES YES // sound definitions #define DOTEATSOUND 0 // any normal dot #define POWERDOTEATSOUND 2 // power dot #define MONSTEREATSOUND 1 // monsters #define FRUITEATSOUND 4 // fruit #define DEADSOUND 3 // player nabbed by a ghost #define STARTLEVELSOUND 5 // music for start of a game/level #define POINTS (12.0 * scale) // the size (in points) we draw the text at #define READY_X ((x - 2 * GHOST_SIZE) * scale + mazePos.x) #define READY_Y (y * scale + mazePos.y) #define OFFSET_X (scale * 6) #define OFFSET_Y (scale * 3) // decides which screen (1-6) to load for a given level. Allows us to // put them in an arbitrary order. static int screens[NUMSCREENS] = { // which maze image (1-6) to use 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 1, 2, 0, 0, 3, 4, 0, 5 }; static windowX[3] = { 0.0, 362.0, 698.0 }; static windowY[3] = { 0.0, 280.0, 536.0 }; @implementation PacManView - initFrame:(const NXRect *)frm // designated initializer for a view { [super initFrame:frm]; backIsColor = NO; // override default; we want image by default begin = 128; // initialize game variables // where the maze is located within our view. mazePos.x = 13; mazePos.y = 12; eraseReady = NO; myorigin.x = 0; myorigin.y = 0; scale = 0; // a convenient rect; this way I don't have to keep creating a bunch of // rects of dimensions GHOST_SIZE by GHOST_SIZE; I re-use this one... NXSetRect(&eraseRect, 0, 0, GHOST_SIZE, GHOST_SIZE); NXSetRect(&textEraseRect, 0, 0, 3 * GHOST_SIZE, 2 * GHOST_SIZE); NXSetRect(&readyRect, 0, 0, GHOST_SIZE * scale * 5, GHOST_SIZE * scale); // make sure power dots get drawn erasePwr = YES; return self; } - setGhostTracker:(GKTrackerId)tracker { ghostId = tracker; return self; } - setFruitTracker:(GKTrackerId)tracker { fruitId = tracker; return self; } // Called by appDidInit to load up images, etc. that we need. - loadPix { int i; id dotSound = [customSound soundNum:DOTEATSOUND type:0]; const int *gh; [super loadPix]; // the dots play through teir own private stream with our special params [dotSound setPlayStream:[[GKSoundStream alloc] initStreams:1]]; [dotSound setPercentToPlay:0.01]; // this number works well... :-) backGround2 = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size]; fruitPointCount = 0; // build the ghost objects gh = [maze ghosts]; for (i=0; i<=3; i++) { ghost[i] = [[Monster alloc] initGhost:i player:player maze:maze at:gh[i*2] :gh[i*2+1]]; ghostPointCount[i] = 0; } // get the images fruit[1] = [NXImage findImageNamed:"Fruit.tiff"]; fruit[2] = [NXImage findImageNamed:"FruitBig.tiff"]; gameOver[1] = [NXImage findImageNamed:"GameOver.tiff"]; gameOver[2] = [NXImage findImageNamed:"GameOverBig.tiff"]; [gameOver[1] getSize:&(gameOverSize[1])]; [gameOver[2] getSize:&(gameOverSize[2])]; [self setScale:[preferences scale]]; return self; } - ghost:(int)i // return ghost #i { return ghost[i]; } // State machine...called by the timed entry. This state machine basically // controls all the aspects of the game; depending upon it's state, different // things can/will happen. The best way to figure this one out is to sit // down and draw a diagram show how the states transition from one to the // next. Note that there are several counters, etc. that function as smaller // independent state machines that operate within the context of this larger // state machine. (Dots blinking, monster states, etc. all run independently // from main state machine, although the main machine occasionally interrupts // things in the sub-machines.) I've not yet had time to fully document the // logic involved in this state machine...if I ever have the time, I may do so, // but I don't know that anyone would care if I did anyway. Minor changes in // here could render the entire game non-functional if you aren't careful! // UNLESS YOU KNOW WHAT YOU'RE DOING, DON'T MESS WITH THIS CODE! Take time // to understand it fully before playing with it! // This is the most ridiculously long method you'll ever see, but there's // no really efficient way to break it up. (Each section deals with what // happens in a specific state, and breaking into smaller methods isn't worth // while, since things are repeated...and it's silly to proliferate subroutines // that only get called once and from one place.) - autoUpdate:sender { // ALL animation is controlled from here!!! register int i, lcnt; register BOOL dotEat = NO; int x, y; BOOL flag = NO; BOOL updateFlag = NO; float gx, gy; NXRect tRect; // keep track of how many time we've been called; we can use it do // decide when to do certain things... if ([NXApp isHidden]) return self; // don't suck cycles if we're hidden cycles++; if (((![preferences speed]) || ([preferences speed] == 2)) && (!demoMode)) { // slow speed, so ignore 50% of entries if (cycles & 0x01) return self; } if (!(cycles & 0x0f)) { // power dots always blink no matter what. [self blinkPowerDot]; // happens every 16 cycles. erasePwr = YES; } if (fruitPointCount) { if (!--fruitPointCount) { fruitPointCount = WIPETEXT; } } for (i=0; i<4; i++) { if (ghostPointCount[i]) { if (!--ghostPointCount[i]) { ghostPointCount[i] = WIPETEXT; } } } if (state == GAMEOVER) { // when demowait counter hits WAITFORDEMO, we start up demo mode. // it basically restarts the game--but with "demoMode" turned on. if (demoWait++ == WAITFORDEMO) { state = NORMALSTATE; [self restartGameForDemo:YES]; [controller unpause]; [[self window] setTitle:"PacMan Demo"]; [scoreKeeper resetScore]; } } // in this state, the "Get Ready!" sign has been put up; we stall for a] // while and then we take it away and start up the next level. if ((state == READY) || (state == DIEREADY)) { for (i=0; i<=3; i++) { [ghost[i] move:self]; } if (!(--begin)) { eraseReady = YES; if (demoMode) [player resetPlayer]; state = NORMALSTATE; updateFlag = YES; } } // make the maze blink on and off at the end of the level. if (state == BLINK_LEVEL) { if (begin--) { if (begin < 32) { if (!(cycles & 0x03)) { // make the maze blink [maze visible:(![maze isVisible])]; updateFlag = YES; } } } else { state = READY; [maze visible:YES]; [player resetPlayer]; [controller nextLevel]; updateFlag = YES; begin = 40; } } // stall. when player dies, the player object needs time to get through // it's whole sequence. if (state == DYING_PAC) { if (!(--begin)) { // stall if (![player newPlayer]) { // no pacs left == game over. [controller gameOver]; state = GAMEOVER; updateFlag = YES; } else { // do ready, next pac... state = DIEREADY; [self startScreen]; [player resetPlayer]; updateFlag = YES; begin = 40; } } } // this state is the meat of the game. move the player and monsters // and deal with player/ghost collisions and eating dots and fruit. if (state == NORMALSTATE) { // move all the ghosts and the pac if (!paused) { if (demoMode) lcnt = 2; else { lcnt = [preferences speed] / 2 + 1; // allow hyper speeds: // since 0 <= speed <= 3, we'll move one or two frames per update } while (lcnt) { // figure out how to move player [player move:self]; // put up/take away the fruit [maze playerPosition:&x :&y]; // get position of fruit if ((numFruits < 3)||demoMode) { // only two fruits per level if (fruitCount++ == timeToFruit) { // time for new fruit ? numFruits++; if (numFruits < 3) fruitOn = DRAW; NXSetRect(&tRect, x * scale + mazePos.x, y * scale + mazePos.y, FRUIT_SIZE * scale, FRUIT_SIZE * scale); [self rebuildStaticAt:&tRect]; } else // one or the other if (fruitCount == ERASEFRUIT) { // it's been there a while... so remove it timeToFruit = 200 + (random() & 0x01f0); fruitOn = ERASE; if (numFruits == 2) numFruits++; NXSetRect(&tRect, x * scale + mazePos.x, y * scale + mazePos.y, FRUIT_SIZE * scale, FRUIT_SIZE * scale); [self rebuildStaticAt:&tRect]; fruitCount = 0; } } // handle eating dots if ([maze eatDotAt:[player xpos] :[player ypos]]) { [customSound playNum:DOTEATSOUND]; dotEat = YES; } if ([player pacAlive]) { // if double timing, need to check. if ([maze powerDotAt:[player xpos] :[player ypos]]) { [customSound playNum:POWERDOTEATSOUND]; [scoreKeeper resetBonus:ghostId]; flag = YES; dotEat = YES; } } if (dotEat) { [maze lastDot:&x :&y]; NX_X(&eraseRect) = x * scale + mazePos.x; NX_Y(&eraseRect) = y * scale + mazePos.y; [self rebuildStaticAt:&eraseRect]; dotEat = NO; } if (![maze dots]) { state = BLINK_LEVEL; begin = 64; fruitOn = ERASE; // erase fruit if finished level fruitPointCount = 0; for (i=0; i<4; i++) ghostPointCount[i] = 0; [maze playerPosition:&x :&y]; // get position of fruit NXSetRect(&tRect, x * scale + mazePos.x, y * scale + mazePos.y, FRUIT_SIZE * scale, FRUIT_SIZE * scale); [self rebuildStaticAt:&tRect]; fruitCount = 0; } // see if player ate the fruit [maze playerPosition:&x :&y]; // get position of fruit if (fruitOn == YES) { // fruit's there... if ((abs([player ypos] - y) < GHOST_SIZE / 2) && (abs([player xpos] - x) < GHOST_SIZE / 2)) { // ate it! fruitOn = ERASE; [customSound playNum:FRUITEATSOUND]; NXSetRect(&tRect, x * scale + mazePos.x, y * scale + mazePos.y, FRUIT_SIZE * scale, FRUIT_SIZE * scale); [self rebuildStaticAt:&tRect]; // add value of fruit to the score fruitPoints = [scoreKeeper addBonusToScore:fruitId advance:NO]; // tell the player how many points he/she got ftx = (x + TEXTOFFSET) * scale + mazePos.x; fty = y * scale + mazePos.y; ftx2 = ftx + OFFSET_X; fty2 = fty + OFFSET_Y; // now, align coords to the maze so erase // does it's job right (otherwise, we end up SOVERing // maze parts twice, and it looks _ugly_! ftx -= mazePos.x; fty -= mazePos.y; // chop off lower four bits (floor to mult. of 16) ftx /= 16 * scale; ftx = (ftx << (scale + 3)) + mazePos.x; fty /= 16 * scale; fty = (fty << (scale + 3)) + mazePos.y; ZAPRECT(textEraseRect, ftx, fty); fruitPointCount = 48; } } // check for player/ghost collision: (and deal with it) if ([maze dots]) { for (i=0; i<=3; i++) { // tell ghost of power dot if (flag) [ghost[i] powerDot:YES]; [ghost[i] at:&gx :&gy]; if ((abs(gx - [player xpos]) < GHOST_SIZE / 2) && (abs(gy - [player ypos]) < GHOST_SIZE / 2)) { // collision! decide who dies... switch ([ghost[i] munch]) { case YES : { // player got ghost [customSound playNum:MONSTEREATSOUND]; // (-munch already added any bonus.) gtx[i] = (gx + TEXTOFFSET) * scale + mazePos.x; gty[i] = gy * scale + mazePos.y; gtx2[i] = gtx[i] + OFFSET_X; gty2[i] = gty[i] + OFFSET_Y; ghostPoints[i] = [scoreKeeper addBonusToScore:ghostId advance:YES]; // now, align to maze as above gtx[i] -= mazePos.x; gtx[i] /= scale * 16; gty[i] -= mazePos.y; gty[i] /= scale * 16; gtx[i] = (gtx[i] << (3 + scale)) + mazePos.x; gty[i] = (gty[i] << (3 + scale)) + mazePos.y; ZAPRECT(textEraseRect, gtx[i], gty[i]); ghostPointCount[i] = 48; break; } case NO : { // ghost got player, so die... if (![player pacAlive]) break; [customSound playNum:DEADSOUND]; if (cheatMode) break; state = DYING_PAC; begin = 48; // stall [player pacDie]; // now, erase the fruit fruitOn = ERASE; [maze playerPosition:&x :&y]; NXSetRect(&tRect, x * scale + mazePos.x, y * scale + mazePos.y, FRUIT_SIZE * scale, FRUIT_SIZE * scale); [self rebuildStaticAt:&tRect]; updateFlag = YES; break; } case HARMLESS : default : { // eyes do nothing. break; } } } } } // figure out where ghosts will go next for (i=0; i<=3; i++) { [ghost[i] move:self]; } if (lcnt > 1) { // make movement take effect w/o render // doing this extra movement gives faster perceived speeds for (i=0; i<=3; i++) { [ghost[i] moveOneFrame]; [ghost[i] powerCount]; } [player moveOneFrame]; } lcnt--; } } } // draw all the changes, if applicable. if (updateFlag) [self update]; /*if ((state != READY) && (state != DIEREADY))*/ [self updateSelf:&bounds :1]; return self; } // This renders the whole screen, much like updateSelf:: below, but since // it always redraws the _entire_ screen, it is unnaceptably inefficient for // handling individual animation frames. - drawSelf:(NXRect *)rects :(int)rectCount // redraws the screen. { // right now, it's stupid and always redraws the whole view. //register int i;//, f; //int x, y; NXPoint pos; //NXRect from;//, bezel, mazeRect; if ([self window] == nil) return self; // exit if no window to draw in if ((state == BLINK_LEVEL) && ![maze isVisible]) [dirtPile fullRedraw:self :backGround2]; else [dirtPile fullRedraw:self :staticBuffer]; if (state == GAMEOVER) { pos.x = (NX_WIDTH(&bounds) - gameOverSize[scale].width) / 2; pos.y = (NX_HEIGHT(&bounds) - gameOverSize[scale].height) / 2; [gameOver[scale] composite:NX_SOVER toPoint:&pos]; } if (NXDrawingStatus == NX_PRINTING) { // make sure actors are in print [self updateSelf:rects :rectCount]; } NXPing(); return self; } // This is the main drawing here. It works like drawSelf, but only // _changes_ stuff; it doesn't re-draw the whole view each time. This // has been done to speed things up. We erase the old, and then redraw // all the spots we erased after calculating where things have moved to. - updateSelf:(NXRect *)rects :(int)rectCount // redraws the screen. { // it redraws only what has changed since last redraw. register int i; int x, y, j, order[4], flag[4]; // NXRect from; if ([self window] == nil) return self; // exit if no window to draw in // if blinking maze and maze isn't on screen then this update is unneeded // note that technically, even if maze is visible, we could skip out, but // we don't because we want the last dot and the monsters, etc. to go // away right as we enter BLINK_LEVEL; this won't happen otherwise. if (((state == BLINK_LEVEL) && ![maze isVisible]) || (state == GAMEOVER)) return self; [buffer lockFocus]; // erase ghosts and ghost text fields. for (i=0; i<=3; i++) { if ((ghostPointCount[i] == WIPETEXT) && (state != GAME_OVER)) { ghostPointCount[i] = 0; ZAPRECT(textEraseRect, gtx[i], gty[i]); CLRRECT(textEraseRect); } if (state != GAME_OVER) { [ghost[i] lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)]; NX_X(&eraseRect) = NX_X(&eraseRect) * scale + mazePos.x; NX_Y(&eraseRect) = NX_Y(&eraseRect) * scale + mazePos.y; CLRRECT(eraseRect); } } // erase ready text if (eraseReady) { [maze playerPosition:&x :&y]; eraseReady = NO; ZAPRECT(readyRect, READY_X, READY_Y); // erase ready text CLRRECT(readyRect); } // erase fruit text if ((fruitPointCount == WIPETEXT) && (state != GAME_OVER)) { fruitPointCount = 0; ZAPRECT(textEraseRect, ftx, fty); CLRRECT(textEraseRect); } // erase the player's PacMan if (state != GAME_OVER) { [player lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)]; NX_X(&eraseRect) = NX_X(&eraseRect) * scale + mazePos.x; NX_Y(&eraseRect) = NX_Y(&eraseRect) * scale + mazePos.y; CLRRECT(eraseRect); } // put the player's Pac on the screen if in a state where it's visible. if ((state != BLINK_LEVEL) || ((state == BLINK_LEVEL) && (begin > 31))) { [player renderAt:mazePos.x :mazePos.y move:((!paused) && (state == NORMALSTATE))]; [player lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)]; NX_X(&eraseRect) = NX_X(&eraseRect) * scale + mazePos.x; NX_Y(&eraseRect) = NX_Y(&eraseRect) * scale + mazePos.y; [dirtPile addRegion:&eraseRect]; } // put ghosts on screen if we're in a state where they are visible. if ((state != DYING_PAC) && (state != BLINK_LEVEL) && (state != GAMEOVER)) { for (i=0; i<=3; i++) { [ghost[i] renderAt:mazePos.x :mazePos.y move: ((!paused) && (state == NORMALSTATE))]; [ghost[i] lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)]; NX_X(&eraseRect) = NX_X(&eraseRect) * scale + mazePos.x; NX_Y(&eraseRect) = NX_Y(&eraseRect) * scale + mazePos.y; [dirtPile addRegion:&eraseRect]; } } // draw text messages if (fruitPointCount) { drawScore(ftx2, fty2, POINTS, fruitPoints); ZAPRECT(textEraseRect, ftx, fty); } for (i=0; i<4; i++) flag[i] = NO; for (i=0; i<4; i++) { // sort scores to draw in lowest to highest order // A selection sort is used. O(n^2) but n=4 so who cares? int next = 0; int val = 100000; for (j=0; j<4; j++) { // find next lowest, unused score value if (!flag[j] && (ghostPoints[j] < val)) { val = ghostPoints[j]; next = j; } } flag[next] = YES; order[i] = next; } for (i=0; i<4; i++) if (ghostPointCount[order[i]]) { drawScore(gtx2[order[i]], gty2[order[i]], POINTS, ghostPoints[order[i]]); ZAPRECT(textEraseRect, gtx[order[i]], gty[order[i]]); } if ((state == READY) || (state == DIEREADY)) { [maze playerPosition:&x :&y]; drawReady(READY_X + 10 * scale, READY_Y + OFFSET_Y, POINTS); ZAPRECT(readyRect, READY_X, READY_Y); } [buffer unlockFocus]; // housekeeping for the graphics: [self lockFocus]; [dirtPile doRedraw:buffer]; // flush buffer out [self unlockFocus]; if (fruitOn == ERASE) fruitOn = NO; NXPing(); erasePwr = NO; // we've listened to this flag, so turn it off now. return self; } // Handle player movement. We respond to the arrow keys. If we don't // recognize the key, we pass it on. This method just shunts the key // off to the player object, which is what really deals with it. - keyDown:(NXEvent *)myevent { unsigned short charCode; unsigned short charSet; register int i; if (!myevent) return self; // if no event when coalescing, go away PSobscurecursor(); #ifdef CHEATMODES // secret cheat mode: control-c if (myevent->data.key.charCode == 0x03) { cheatMode = YES; fflush(stderr); [preferences setUnfair]; // don't allow net high scores. local OK [customSound playNum:POWERDOTEATSOUND]; return self; } // secret cheat: control-d (eats a power dot) if (myevent->data.key.charCode == 0x04) { for (i=0; i<4; i++) [ghost[i] powerDot:YES]; [preferences setUnfair]; // don't allow net high scores. local OK [customSound playNum:POWERDOTEATSOUND]; [scoreKeeper resetBonus:ghostId]; return self; } // secret cheat: control-f (put fruit on screen) if (myevent->data.key.charCode == 0x06) { fruitCount = timeToFruit - 1; numFruits = 0; [preferences setUnfair]; // don't allow net high scores. local OK return self; } // secret cheat: control-l (advance a level instantly) if (myevent->data.key.charCode == 0x0c) { NXRect tRect; int x, y; state = BLINK_LEVEL; begin = 64; fruitPointCount = 0; for (i=0; i<4; i++) ghostPointCount[i] = 0; fruitOn = ERASE; // erase fruit if finished level [maze playerPosition:&x :&y]; // get position of fruit NXSetRect(&tRect, x * scale + mazePos.x, y * scale + mazePos.y, FRUIT_SIZE * scale, FRUIT_SIZE * scale); [self rebuildStaticAt:&tRect]; fruitCount = 0; [preferences setUnfair]; // don't allow net high scores. local OK return self; } #endif // check for arrow keys or space bar if (!(myevent->flags&(NX_CONTROLMASK|NX_ALTERNATEMASK|NX_COMMANDMASK))) { charCode = myevent->data.key.charCode; charSet = myevent->data.key.charSet; if (charSet == NX_SYMBOLSET) { // symbol set contains the arrows if (charCode == 0xAD) { // Up Arrow [player newDirection:PAC_UP]; if (paused) [controller unpause]; return self; } else if (charCode == 0xAF) { // Down Arrow [player newDirection:PAC_DOWN]; if (paused) [controller unpause]; return self; } else if (charCode == 0xAC) { // Left Arrow [player newDirection:PAC_LEFT]; if (paused) [controller unpause]; return self; } else if (charCode == 0xAE) { // Right Arrow [player newDirection:PAC_RIGHT]; if (paused) [controller unpause]; return self; } } else if (myevent->data.key.charCode == ' ') { // Space Bar [player newDirection:PAC_STOP]; if (paused) [controller unpause]; return self; } else if ((myevent->data.key.charCode == 'a') && cheatMode) { #ifdef CHEATMODES // abort pac -- it's the only way you can die in cheat mode. [customSound playNum:DEADSOUND]; state = DYING_PAC; begin = 48; // stall [player pacDie]; #endif } else { [super keyDown:myevent]; } } else [super keyDown:myevent]; [self keyDown:[NXApp peekAndGetNextEvent:NX_KEYDOWN]]; // coalesce keydowns // this is done recursively...primitive, but easy to implement :-) return self; } // set up the screen at the start of a new level. Loads in the maze. - setUpScreen { [maze makeMaze:screens[(([controller level] >= NUMSCREENS) ? (NUMSCREENS - 1) : [controller level]) - 1]]; fruitOn = NO; [self rebuildStaticBuffer]; [self startScreen]; timeToFruit = 200 + (random() & 0x01f0); fruitCount = 0; numFruits = 0; [[scoreKeeper bonusTracker:fruitId] advanceBonus]; // changes with level [super setUpScreen]; return self; } // This lets us put the ghosts back where they are at the start of the level. // This is separate from above because the above also resets the dots, and if // the player dies, we only want to reset the ghosts, not the dots, too. - startScreen { const int *gh; int i; gh = [maze ghosts]; // pointer to array of ghost coordinates for (i=0; i<=3; i++) { // re-initialize each ghost [ghost[i] initGhost:i player:player maze:maze at:gh[i*2] :gh[i*2+1]]; [ghost[i] setScale:scale]; ghostPointCount[i] = 0; } fruitPointCount = 0; // don't let fruit come out too quickly if (timeToFruit - fruitCount < 200) timeToFruit += 250; return self; } - restartGame { return [self restartGameForDemo:NO]; } - restartGameForDemo:(BOOL)doingDemo { // go to READY state to start game; but want DIEREADY so we don't advance // the level; the controller, by virtue of calling this method, has already // advanced the level. state = DIEREADY; begin = 100; fruitOn = NO; // make sure that all artifacts of demo mode are gone demoWait = 0; cheatMode = NO; if (demoMode) { demoMode = NO; [[self window] setTitle:"PacMan"]; } [scoreKeeper resetScore]; // clear the score [scoreKeeper resetBonus:(-1)]; // clear all bonuses [self getPreferences]; // make sure we're up to date [player newPlayer]; // get a new pac to play with // (above always sets up the pac, but in demoMode, the gameBrain hasn't // given us 3 pacs, so we end up taking a negative # of pacs, which means // demo mode will end as soon as the pac dies.) if (doingDemo) demoMode = YES; // cut off the info panel sound (if playing) (only if not demo mode) else [[controller infoController] stopSound:self]; [customSound shutUpUntil:[[DAYTime alloc] initWithCurrentTime]]; [self update]; [self updateSelf:&bounds :1]; NXPing(); // show screen // start up the sound player object with the "start game" music if ([preferences music]) [customSound playNum:STARTLEVELSOUND]; return self; } - setBackgroundFile:(const char *)fileName andRemember:(BOOL)remember { NXRect bezel = {{NX_X(&bounds) + 5, NX_Y(&bounds) + 5}, {NX_WIDTH(&bounds) - 10, NX_HEIGHT(&bounds) - 10}}; [super setBackgroundFile:fileName andRemember:remember]; [backGround2 lockFocus]; NXDrawGrayBezel(&bezel, &bounds); NXFrameRectWithWidth(&bounds, 5); NX_WIDTH(&bezel) -= 6; NX_HEIGHT(&bezel) -= 6; NX_X(&bezel) += 3; NX_Y(&bezel) += 3; [self drawBackground:&bezel]; [backGround2 unlockFocus]; [self rebuildStaticBuffer]; [self update]; return self; } - buildColorBackground { NXRect bezel = {{NX_X(&bounds) + 5, NX_Y(&bounds) + 5}, {NX_WIDTH(&bounds) - 10, NX_HEIGHT(&bounds) - 10}}; [backGround2 lockFocus]; NXDrawGrayBezel(&bezel, &bounds); NXFrameRectWithWidth(&bounds, 5); NX_WIDTH(&bezel) -= 6; NX_HEIGHT(&bezel) -= 6; NX_X(&bezel) += 3; NX_Y(&bezel) += 3; [self drawBackground:&bezel]; [backGround2 unlockFocus]; [self rebuildStaticBuffer]; return self; } - acceptColor:(NXColor)color atPoint:(const NXPoint *)aPoint { // override to redraw background buffer. backIsColor = YES; backColor = color; [self buildColorBackground]; [[self writeColor] update]; return self; } - rebuildStaticBuffer { NXRect mazeRect = {{0, 0}, {GHOST_SIZE * BLOCK_WIDTH, GHOST_SIZE * BLOCK_HEIGHT}}; int x, y; register int f; NXRect from; NXPoint pos; [staticBuffer lockFocus]; [backGround2 composite:NX_COPY fromRect:&bounds toPoint:&(bounds.origin)]; [maze render:&mazeRect at:&mazePos]; [maze playerPosition:&x :&y]; if (fruitOn) { if (fruitOn != ERASE) { // decide which fruit to draw f = fruits[(([controller level] > FRUIT_LEVELS) ? FRUIT_LEVELS : [controller level])]; // draw the fruit pos.x = x * scale + mazePos.x; pos.y = y * scale + mazePos.y; NXSetRect(&from, (f % FRUIT_PER_ROW) * FRUIT_SIZE * scale, (f / FRUIT_PER_ROW) * FRUIT_SIZE * scale, FRUIT_SIZE * scale, FRUIT_SIZE * scale); [fruit[scale] composite:NX_SOVER fromRect:&from toPoint:&pos]; fruitOn = YES; } else fruitOn = NO; } [staticBuffer unlockFocus]; [buffer lockFocus]; [staticBuffer composite:NX_COPY fromRect:&bounds toPoint:&(bounds.origin)]; [buffer unlockFocus]; [dirtPile addRegion:&bounds]; return self; } - rebuildStaticAt:(NXRect *)rect; { // assumes that bezel is intact, so doesn't draw it NXRect mazeRect = {{(NX_X(rect) - mazePos.x) / scale, (NX_Y(rect) - mazePos.y) / scale}, {(NX_WIDTH(rect) - 1.0) / scale, (NX_HEIGHT(rect) - 1.0) / scale}}; NXRect xRect = {{0.0, 0.0}, {GHOST_SIZE * scale, GHOST_SIZE * scale}}; NXPoint pos; NXRect from; register int f; int x, y; [staticBuffer lockFocus]; [super rebuildStaticAt:rect]; [maze render:&mazeRect at:&mazePos]; // see if rect intersects fruit && fruit is on screen [maze playerPosition:&x :&y]; NX_X(&xRect) = x * scale + mazePos.x; NX_Y(&xRect) = y * scale + mazePos.y; if (NXIntersectsRect(&xRect, rect) && fruitOn) { if (fruitOn != ERASE) { // decide which fruit to draw f = fruits[(([controller level] > FRUIT_LEVELS) ? FRUIT_LEVELS : [controller level])]; // draw the fruit pos.x = x * scale + mazePos.x; pos.y = y * scale + mazePos.y; NXSetRect(&from, (f % FRUIT_PER_ROW) * FRUIT_SIZE * scale, (f / FRUIT_PER_ROW) * FRUIT_SIZE * scale, FRUIT_SIZE * scale, FRUIT_SIZE * scale); [fruit[scale] composite:NX_SOVER fromRect:&from toPoint:&(xRect.origin)]; fruitOn = YES; } else fruitOn = NO; } [staticBuffer unlockFocus]; [buffer lockFocus]; [staticBuffer composite:NX_COPY fromRect:rect toPoint:&(rect->origin)]; [buffer unlockFocus]; [dirtPile addRegion:rect]; return self; } - blinkPowerDot // update static buffer where power dots are. { const int *pd; // pointer to array of power dot coords register int i; [maze blinkPowerDot]; pd = [maze powerDot]; // get power dot coords for (i=0; i<=3; i++) { // erase power dot every time it blinks. NX_X(&eraseRect) = pd[i * 2 ] * GHOST_SIZE * scale + mazePos.x; NX_Y(&eraseRect) = pd[i * 2 + 1] * GHOST_SIZE * scale + mazePos.y; [self rebuildStaticAt:&eraseRect]; } return self; } - (int)scale { return scale; } - setScale:(int)newScale { int i; if ((newScale < 1) || (newScale > 2)) return self; if (newScale == scale) return self; // already that size. scale = newScale; for (i=0; i<4; i++) { [ghost[i] setScale:scale]; } [player setScale:scale]; [maze setScale:scale]; // resize self and windows [window disableFlushWindow]; [window disableDisplay]; [self sizeTo:windowX[scale] :windowY[scale]]; NXSetRect(&eraseRect, 0, 0, GHOST_SIZE * scale, GHOST_SIZE * scale); NXSetRect(&readyRect, 0, 0, GHOST_SIZE * scale * 5, GHOST_SIZE * scale); NXSetRect(&textEraseRect, 0, 0, 3 * GHOST_SIZE * scale, 2 * GHOST_SIZE * scale); // rebuild all the offscreen buffers. [buffer free]; [staticBuffer free]; [backGround2 free]; buffer = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size]; staticBuffer = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size]; backGround2 = [[NXImage allocFromZone:[self zone]] initSize:&bounds.size]; if (backIsColor) [self buildColorBackground]; else [self setBackgroundFile:NXGetDefaultValue([NXApp appName], "BackGround") andRemember:NO]; [self rebuildStaticBuffer]; [[window delegate] windowDidMove:window]; [window sizeWindow:windowX[scale] :windowY[scale]]; [window reenableFlushWindow]; [window reenableDisplay]; [[window display] flushWindowIfNeeded]; [self update]; return self; } @end
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.