|
|
1.1 ! root 1: /* ! 2: * BreakView.m, view to implement the "BreakApp" game. ! 3: * Author: Ali Ozer ! 4: * Written for 0.8 October 88. ! 5: * Modified for 0.9 March 89. ! 6: * Modified for 1.0 July 89. ! 7: * Removed use of Bitmap and threw away some classes May 90. ! 8: * Final 2.0 fixes/enhancements Sept 90. ! 9: * 3.0 update March 92. ! 10: * Sound-related changes for 3.1 March 92. ! 11: * ! 12: * BreakView implements an interactive custom view that allows the user ! 13: * to play "BreakApp," a game similar to a popular arcade classic. ! 14: * ! 15: * BreakView's main control methods are based on the target-action ! 16: * paradigm; thus you can include BreakView in an Interface-Builder based ! 17: * application. Please refer to BreakView.h for a list of "public" methods ! 18: * that you should provide links to in Interface Builder. ! 19: * ! 20: * You may freely copy, distribute and reuse the code in this example. ! 21: * NeXT disclaims any warranty of any kind, expressed or implied, ! 22: * as to its fitness for any particular use. ! 23: */ ! 24: ! 25: #import <libc.h> ! 26: #import <math.h> ! 27: #import <defaults/defaults.h> // For writing/reading high score ! 28: #import <appkit/appkit.h> ! 29: ! 30: #import "BreakView.h" ! 31: #import "SoundEffect.h" ! 32: ! 33: // Max absolute x and y velocities of the ball, in base coordinates per msec. ! 34: ! 35: #define MAXXV ((level > 6) ? 0.3 : 0.2) ! 36: #define MAXYV (0.4) ! 37: ! 38: // Maximum amount of time that is allowed to pass between two calls to the ! 39: // step method. If the time is greater than MAXTIMEDIFFERENCE, then this ! 40: // value is used instead. MAXTIMEDIFFERENCE should be no greater ! 41: // than the time it takes for the ball to go the height of a tile ! 42: // or the height of the ball + height of paddle. The units ! 43: // are in milliseconds. ! 44: ! 45: #define MAXTIMEDIFFERENCE (TILEHEIGHT * 0.8 / MAXYV) ! 46: #define MINTIMEDIFFERENCE 1 ! 47: ! 48: // Max revolution speed of the ball; this is the maximum ! 49: // number of radians it will turn per millisecond when rotating... ! 50: ! 51: #define MAXREVOLUTIONSPEED (M_PI / 250.0) // Max is 2 revs/sec ! 52: ! 53: // The following values are the default sizes for the various pieces. ! 54: ! 55: #define RADIUS 8.0 // Ball radius ! 56: #define PADDLEWIDTH (TILEWIDTH * 1.8) // Paddle width ! 57: #define PADDLEHEIGHT (TILEHEIGHT * 0.6) // Paddle height ! 58: #define BALLWIDTH (RADIUS * 2.0) // Ball width ! 59: #define BALLHEIGHT (RADIUS * 2.0) // Ball height ! 60: ! 61: // SHADOWOFFSET defines the amount the shadow is offset from the piece. ! 62: ! 63: #define SHADOWOFFSET 3.0 ! 64: ! 65: #define LIVES 5 // Number of lives per game ! 66: ! 67: #define STOPGAMEAT (-10) // Number of loops through the ! 68: // game after all tiles die ! 69: ! 70: #define LEVELBONUS 50 // Bonus at the end of a level ! 71: ! 72: // Starting locations... ! 73: ! 74: #define PADDLEX ((gameSize.width - paddleSize.width) / 2.0) ! 75: #define PADDLEY 1.0 ! 76: #define BALLX ((gameSize.width - ballSize.width) / 2.0) ! 77: #define BALLY (paddleY + paddleSize.height) ! 78: ! 79: // Accelaration & score values of the different tile types. ! 80: ! 81: static const float tileAccs[NUMTILETYPES] = {1.0, 1.3}; ! 82: static const int tileScores[NUMTILETYPES] = {5, 25}; ! 83: ! 84: #define NOTILE -1 ! 85: ! 86: extern void srandom(); // Hmm; not in libc.h ! 87: #define RANDINT(n) (random() % ((n)+1)) // Random integer 0..n ! 88: #define ONEIN(n) ((random() % (n)) == 0) // TRUE one in n times ! 89: #define INITRAND srandom(time(0)) // Randomizer ! 90: ! 91: #define gameSize bounds.size ! 92: ! 93: // Restrict a value to the range -max .. max. ! 94: ! 95: inline float restrictValue(float val, float max) ! 96: { ! 97: if (val > max) return max; ! 98: else if (val < -max) return -max; ! 99: else return val; ! 100: } ! 101: ! 102: // Convert x-location to left/right pan for playing sounds ! 103: ! 104: @implementation BreakView ! 105: ! 106: - initFrame:(const NXRect *)frm ! 107: { ! 108: [super initFrame:frm]; ! 109: ! 110: [self allocateGState]; // For faster lock/unlockFocus ! 111: ! 112: [(ball = [[NXImage allocFromZone:[self zone]] init]) setScalable:NO]; ! 113: [ball useDrawMethod:@selector(drawBall:) inObject:self]; ! 114: ! 115: [(paddle = [[NXImage allocFromZone:[self zone]] init]) setScalable:NO]; ! 116: [paddle useDrawMethod:@selector(drawPaddle:) inObject:self]; ! 117: ! 118: [(tile[0] = [[NXImage allocFromZone:[self zone]] init]) setScalable:NO]; ! 119: [(tile[1] = [[NXImage allocFromZone:[self zone]] init]) setScalable:NO]; ! 120: [tile[0] useDrawMethod:@selector(drawNormalTile:) inObject:self]; ! 121: [tile[1] useDrawMethod:@selector(drawToughTile:) inObject:self]; ! 122: ! 123: wallSound = [[SoundEffect allocFromZone:[self zone]] initFromSection:"Wall.snd"]; ! 124: tileSound = [[SoundEffect allocFromZone:[self zone]] initFromSection:"Tile.snd"]; ! 125: missSound = [[SoundEffect allocFromZone:[self zone]] initFromSection:"Miss.snd"]; ! 126: paddleSound = [[SoundEffect allocFromZone:[self zone]] initFromSection:"Paddle.snd"]; ! 127: ! 128: [self setBackgroundFile:NXGetDefaultValue([NXApp appName], "BackGround") andRemember:NO]; ! 129: ! 130: [self resizePieces]; ! 131: ! 132: [self getHighScore]; ! 133: ! 134: demoMode = NO; ! 135: ! 136: INITRAND; ! 137: ! 138: return self; ! 139: } ! 140: ! 141: // free simply gets rid of everything we created for BreakView, including ! 142: // the instance of BreakView itself. This is how nice objects clean up. ! 143: ! 144: - free ! 145: { ! 146: int cnt; ! 147: ! 148: if (gameRunning) { ! 149: DPSRemoveTimedEntry (timer); ! 150: } ! 151: for (cnt = 0; cnt < NUMTILETYPES; cnt++) { ! 152: [tile[cnt] free]; ! 153: } ! 154: ! 155: [ball free]; ! 156: [paddle free]; ! 157: [backGround free]; ! 158: [wallSound free]; ! 159: [tileSound free]; ! 160: [missSound free]; ! 161: [paddleSound free]; ! 162: ! 163: return [super free]; ! 164: } ! 165: ! 166: // resizePieces calculates the new sizes of all the pieces after the game is ! 167: // started or the playing field (the BreakView) is resized. ! 168: ! 169: - resizePieces ! 170: { ! 171: int cnt; ! 172: float xRatio = gameSize.width / GAMEWIDTH; ! 173: float yRatio = gameSize.height / GAMEHEIGHT; ! 174: ! 175: [backGround setSize:&gameSize]; ! 176: ! 177: tileSize.width = floor(xRatio * TILEWIDTH); ! 178: tileSize.height = floor(yRatio * TILEHEIGHT); ! 179: for (cnt = 0; cnt < NUMTILETYPES; cnt++) { ! 180: [tile[cnt] setSize:&tileSize]; ! 181: } ! 182: leftMargin = floor((gameSize.width - (tileSize.width + INTERTILE) * ! 183: NUMTILESX) / 2.0 + 1.0); ! 184: ! 185: paddleSize.width = floor(xRatio * PADDLEWIDTH); ! 186: paddleSize.height = floor(yRatio * PADDLEHEIGHT); ! 187: [paddle setSize:&paddleSize]; ! 188: ! 189: ballSize.width = floor(xRatio * BALLWIDTH); ! 190: ballSize.height = floor(yRatio * BALLHEIGHT); ! 191: [ball setSize:&ballSize]; ! 192: ! 193: return self; ! 194: } ! 195: ! 196: // The following allows BreakView to grab the mousedown event that activates ! 197: // the window. By default, the View's acceptsFirstMouse returns NO. ! 198: ! 199: - (BOOL)acceptsFirstMouse ! 200: { ! 201: return YES; ! 202: } ! 203: ! 204: // This methods allows changing the file used to paint the background of the ! 205: // playing field. Set fileName to NULL to revert to the default. Set ! 206: // remember to YES if you wish the write the value out in the defaults. ! 207: ! 208: - setBackgroundFile:(const char *)fileName andRemember:(BOOL)remember ! 209: { ! 210: [backGround free]; ! 211: backGround = [[NXImage allocFromZone:[self zone]] initSize:&gameSize]; ! 212: if (fileName) { ! 213: [backGround useFromFile:fileName]; ! 214: [backGround setScalable:YES]; ! 215: if (remember) { ! 216: NXWriteDefault ([NXApp appName], "BackGround", fileName); ! 217: } ! 218: } else { ! 219: [backGround useDrawMethod:@selector(drawDefaultBackground:) inObject:self]; ! 220: [backGround setScalable:NO]; ! 221: if (remember) { ! 222: NXRemoveDefault ([NXApp appName], "BackGround"); ! 223: } ! 224: } ! 225: [backGround setBackgroundColor:NX_COLORWHITE]; ! 226: [self display]; ! 227: ! 228: return self; ! 229: } ! 230: ! 231: // The following two methods allow changing the background image from ! 232: // menu items or buttons. ! 233: ! 234: - changeBackground:sender ! 235: { ! 236: if ([[OpenPanel new] runModalForTypes:[NXImage imageFileTypes]]) { ! 237: [self setBackgroundFile:[[OpenPanel new] filename] andRemember:YES]; ! 238: [self display]; ! 239: } ! 240: ! 241: return self; ! 242: } ! 243: ! 244: - revertBackground:sender ! 245: { ! 246: [self setBackgroundFile:NULL andRemember:YES]; ! 247: [self display]; ! 248: return self; ! 249: } ! 250: ! 251: // getHighScore reads the previous high score from the user's defaults file. ! 252: // If no such default is found, then the high score is set to zero. ! 253: ! 254: - getHighScore ! 255: { ! 256: const char *tmpstr; ! 257: if (((tmpstr = NXGetDefaultValue ([NXApp appName], "HighScore")) && ! 258: (sscanf(tmpstr, "%d", &highScore) != 1))) highScore = 0; ! 259: ! 260: return self; ! 261: } ! 262: ! 263: // setHighScore should be called when the user score for a game is above ! 264: // the current high score. setHighScore sets the high score and ! 265: // writes it out the defaults file so that it can be remembered for eternity. ! 266: ! 267: - setHighScore:(int)hScore ! 268: { ! 269: char str[10]; ! 270: [hscoreView setIntValue:(highScore = hScore)]; ! 271: sprintf (str, "%d", highScore); ! 272: NXWriteDefault ([NXApp appName], "HighScore", str); ! 273: return self; ! 274: } ! 275: ! 276: - (int)score ! 277: { ! 278: return score; ! 279: } ! 280: ! 281: - (int)level ! 282: { ! 283: return level; ! 284: } ! 285: ! 286: - (int)lives ! 287: { ! 288: return lives; ! 289: } ! 290: ! 291: // gotoFirstLevel: sets everything up for a new game. ! 292: ! 293: - gotoFirstLevel:sender ! 294: { ! 295: score = 0; ! 296: level = 0; ! 297: lives = LIVES; ! 298: return [self gotoNextLevel:sender]; ! 299: } ! 300: ! 301: // gotoNextLevel: sets everything up for the next level of the game; the level ! 302: // count is incremented and the pieces are set up on the field. The ball and ! 303: // the paddle are also brought to the starting locations. ! 304: // ! 305: // This routine can of course be made infinitely more complicated in ! 306: // determining where the tiles go. Left as an exercise to the reader. 8-) ! 307: ! 308: - gotoNextLevel:sender ! 309: { ! 310: int xcnt, ycnt, yFrom, yTo, xFrom, xTo; ! 311: ! 312: // We are at the next level... Stop the game and increment the level. ! 313: ! 314: [self stop:sender]; ! 315: ! 316: level++; ! 317: ! 318: // Now place the tiles. Here's where we could do some fancy tile layout, ! 319: // depending on the game level. yFrom, yTo, xFrom, and xTo define the "box" ! 320: // in which we will lay the tiles out. These values are inclusive. ! 321: ! 322: switch (level % 6) { ! 323: case 0: yTo = NUMTILESY-2; break; ! 324: case 4: yTo = NUMTILESY-4; break; ! 325: case 5: yTo = 2 * (NUMTILESY / 3); break; ! 326: default: yTo = 3 * (NUMTILESY / 4); break; ! 327: } ! 328: ! 329: xFrom = 0; xTo = NUMTILESX-1; yFrom = yTo - 3; ! 330: ! 331: switch (level % 10) { ! 332: case 1: yFrom++; break; ! 333: case 2: yFrom--; xFrom++; xTo--; break; ! 334: case 4: xFrom += 2; xTo -= 2; break; ! 335: case 6: yFrom = MIN(yFrom, NUMTILESY / 4); xFrom++; xTo--; break; ! 336: case 7: xTo -= 3; break; ! 337: case 8: yFrom -= 2; xFrom += 2; xTo -= 2; break; ! 338: case 9: yFrom = MIN(yFrom, NUMTILESY / 4); ! 339: yTo = MAX(yTo, yFrom+4); ! 340: break; ! 341: case 0: yFrom = MIN(yFrom, NUMTILESY / 5); ! 342: xFrom += (NUMTILESX / 2); ! 343: break; ! 344: default: break; ! 345: } ! 346: ! 347: // The area in the playing field where we place tiles is at least 3 tiles ! 348: // high and at least NUMTILESX-4 tiles wide. ! 349: ! 350: // Empty out the whole playing field. ! 351: for (xcnt = 0; xcnt < NUMTILESX; xcnt++) { ! 352: for (ycnt = 0; ycnt < NUMTILESY; ycnt++) { ! 353: tiles[xcnt][ycnt] = NOTILE; ! 354: } ! 355: } ! 356: ! 357: // Fill up the tile area with wimpy tiles ! 358: for (ycnt = yFrom; ycnt <= yTo; ycnt++) { ! 359: for (xcnt = xFrom; xcnt <= xTo; xcnt++) { ! 360: tiles[xcnt][ycnt] = 0; ! 361: } ! 362: } ! 363: ! 364: // Erase or change some of the tiles, depending on the level. ! 365: // Assumption is that we have at least 3 rows of tiles, yFrom..yTo. ! 366: ! 367: switch (level % 7) { ! 368: case 2: // clear two rows in the middle ! 369: for (xcnt = xFrom; xcnt <= xTo; xcnt++) { ! 370: tiles[xcnt][yFrom+1] = tiles[xcnt][yTo-1] = NOTILE; ! 371: } ! 372: break; ! 373: case 3: // randomly clear out some tiles ! 374: for (xcnt = 0; xcnt < 5; xcnt++) { ! 375: tiles[xFrom+RANDINT(xTo-xFrom)][yFrom+1+RANDINT(yTo-yFrom-2)] = ! 376: NOTILE; ! 377: } ! 378: break; ! 379: case 4: // clear middle columns ! 380: for (xcnt = xFrom + 2; xcnt <= xTo - 2; xcnt++) { ! 381: for (ycnt = yFrom; ycnt <= yTo; ycnt++) { ! 382: tiles[xcnt][ycnt] = NOTILE; ! 383: } ! 384: } ! 385: break; ! 386: case 6: // clear out the insides ! 387: for (ycnt = yFrom+1; ycnt < yTo; ycnt++) { ! 388: for (xcnt = xFrom+1; xcnt < xTo; xcnt++) { ! 389: tiles[xcnt][ycnt] = NOTILE; ! 390: } ! 391: } ! 392: break; ! 393: default: ! 394: break; ! 395: } ! 396: ! 397: // Drop in some tough tiles in all rows except the first one ! 398: for (xcnt = 0; xcnt < 5; xcnt++) { ! 399: tiles[xFrom+RANDINT(xTo-xFrom)][yFrom+1+RANDINT(yTo-yFrom-1)] = 1; ! 400: } ! 401: ! 402: // Compute the number of tiles we actually ended up putting down... ! 403: numTilesLeft = 0; ! 404: for (ycnt = yFrom; ycnt <= yTo; ycnt++) { ! 405: for (xcnt = xFrom; xcnt <= xTo; xcnt++) { ! 406: if (tiles[xcnt][ycnt] != NOTILE) numTilesLeft++; ! 407: } ! 408: } ! 409: ! 410: // Of course you might think there are too many braces in the above code, ! 411: // where probably none would've sufficed. Too many braces never hurt, & it ! 412: // will save you from some bozo bug some day. So use them! They're cheap! ! 413: ! 414: [self resetBallAndPaddle]; ! 415: ! 416: [levelView setIntValue:level]; ! 417: [scoreView setIntValue:score]; ! 418: [livesView setIntValue:lives]; ! 419: [hscoreView setIntValue:highScore]; ! 420: [statusView setStringValue:NXLocalString("Game Ready", NULL, "Message indicating the game is ready to run")]; ! 421: ! 422: killerBall = ((level % 12) == 0); // Every 12 turns, the loses its ! 423: // ability to bounce off tiles ! 424: niceBall = (level % 5 == 0); // Every 5 turns, make the ball ! 425: // bounce towards the paddle ! 426: ! 427: // If the background image is not from a file but our own default, ! 428: // poke it so its redrawn. This way every level will look different. ! 429: // We could've simply used a BOOL to remember if the image is the default ! 430: // one, but this test here works as well. ! 431: ! 432: if ([[backGround lastRepresentation] isKindOf:[NXCustomImageRep class]]) { ! 433: [backGround recache]; ! 434: } ! 435: ! 436: [self display]; // Display the new arrangement ! 437: ! 438: if (demoMode) { ! 439: [self go:sender]; // If in demo mode, start rolling ! 440: } ! 441: ! 442: return self; ! 443: } ! 444: ! 445: // setDemoMode: allows the user to put the game in a demo mode. ! 446: // In the demo mode, the paddle constantly follows the ball. ! 447: ! 448: - setDemoMode:sender ! 449: { ! 450: if (demoMode = ([sender state] == 0 ? NO : YES)) { ! 451: [self go:sender]; ! 452: } else { ! 453: [self stop:sender]; ! 454: } ! 455: return self; ! 456: } ! 457: ! 458: // This method should be called when a new level or game is started or the ! 459: // player misses the ball. It resets the ball & paddle locations back to ! 460: // default. ! 461: ! 462: - resetBallAndPaddle ! 463: { ! 464: paddleX = PADDLEX; ! 465: paddleY = PADDLEY; ! 466: ballX = BALLX; ! 467: ballY = BALLY; ! 468: ! 469: ballXVel = 0.0; ! 470: ballYVel = 0.0; ! 471: ! 472: // The ball shouldn't start out rotating... ! 473: revolutionsLeft = 0; ! 474: ! 475: return self; ! 476: } ! 477: ! 478: // The directBallAt: initializes the velocity vector of the ball so that ! 479: // the ball will go from its current location to the specified destination ! 480: // point. The speed of the ball is determined by the current level. If ballYVel ! 481: // is already set, then only the x velocity & y direction is changed. ! 482: ! 483: - directBallAt:(NXPoint *)dest ! 484: { ! 485: float desiredYVel = dest->y - (ballY + ballSize.height / 2.0); ! 486: float desiredXVel = dest->x - (ballX + ballSize.width / 2.0); ! 487: ! 488: // Transform back to original game coords (velocity values are measured ! 489: // in these). ! 490: ! 491: desiredYVel /= (gameSize.height / GAMEHEIGHT); ! 492: desiredXVel /= (gameSize.width / GAMEWIDTH); ! 493: ! 494: if (fabs(desiredYVel) < 1.0) { ! 495: desiredYVel = desiredYVel < 0.0 ? -1.0 : 1.0; ! 496: } ! 497: if (ballYVel == 0.0) { ! 498: // Come up with a value between 60 and 100% of MAXYV. ! 499: ballYVel = restrictValue(((RANDINT(level * 8) + 60.0) / 100.0) * MAXYV, ! 500: MAXYV); ! 501: } ! 502: ballYVel = fabs(ballYVel) * (desiredYVel < 0.0 ? -1.0 : 1.0); ! 503: ballXVel = restrictValue(ballYVel * (desiredXVel / desiredYVel) ,MAXXV); ! 504: ! 505: return self; ! 506: } ! 507: ! 508: // The stop method will pause a running game. The go method will start it up ! 509: // again. They can be assigned to buttons or other appkit objects through IB. ! 510: ! 511: - go:sender ! 512: { ! 513: void runOneStep (); ! 514: if (lives && !gameRunning) { ! 515: // If the ball velocity wasn't initialized, start it rolling ! 516: // towards the mouse location... ! 517: if (ballXVel == 0.0 && ballYVel == 0.0) { ! 518: NXPoint mouseLoc; ! 519: [[self window] getMouseLocation:&mouseLoc]; ! 520: [self convertPoint:&mouseLoc fromView:nil]; ! 521: [self directBallAt:&mouseLoc]; ! 522: ballYVel = fabs(ballYVel); ! 523: } ! 524: gameRunning = YES; ! 525: timer = DPSAddTimedEntry(0.03, &runOneStep, self, NX_BASETHRESHOLD); ! 526: [statusView setStringValue:NXLocalString("Running", NULL, "Message indicating that the game is running")]; ! 527: } ! 528: return self; ! 529: } ! 530: ! 531: - stop:sender ! 532: { ! 533: if (gameRunning) { ! 534: gameRunning = NO; ! 535: DPSRemoveTimedEntry (timer); ! 536: [statusView setStringValue:NXLocalString("Paused", NULL, "Message indicating that the game is paused")]; ! 537: } ! 538: ! 539: return self; ! 540: } ! 541: ! 542: - sizeTo:(NXCoord)width :(NXCoord)height ! 543: { ! 544: NXSize oldSize = bounds.size; ! 545: ! 546: [super sizeTo:width :height]; ! 547: ! 548: ballX = (ballX * width / oldSize.width); ! 549: ballY = (ballY * height / oldSize.height); ! 550: paddleX = (paddleX * width / oldSize.width); ! 551: paddleY = (paddleY * height / oldSize.height); ! 552: ! 553: [self resizePieces]; ! 554: ! 555: [self display]; ! 556: return self; ! 557: } ! 558: ! 559: // A mousedown effectively allows pausing and unpausing the game by ! 560: // alternately calling one of the above two functions (stop/go). ! 561: ! 562: - mouseDown:(NXEvent *)event ! 563: { ! 564: if (gameRunning) { ! 565: [self stop:self]; ! 566: } else if (lives) { ! 567: [self go:self]; ! 568: } ! 569: return self; ! 570: } ! 571: ! 572: // The following few methods draw the pieces. ! 573: ! 574: - drawBall:imageRep ! 575: { ! 576: PSscale (ballSize.width / BALLWIDTH, ballSize.height / BALLHEIGHT); ! 577: ! 578: // First draw the shadow under the ball. ! 579: ! 580: PSarc (RADIUS+SHADOWOFFSET/2, RADIUS-SHADOWOFFSET/2, ! 581: RADIUS-SHADOWOFFSET-1.0, 0.0, 360.0); ! 582: PSsetgray (NX_BLACK); ! 583: if (NXDrawingStatus == NX_DRAWING) { ! 584: PSsetalpha (0.666); ! 585: } ! 586: PSfill (); ! 587: if (NXDrawingStatus == NX_DRAWING) { ! 588: PSsetalpha (1.0); ! 589: } ! 590: ! 591: // Then the ball. ! 592: ! 593: PSarc (RADIUS-SHADOWOFFSET/2, RADIUS+SHADOWOFFSET/2, ! 594: RADIUS-SHADOWOFFSET-1.0, 0.0, 360.0); ! 595: PSsetgray (NX_LTGRAY); ! 596: PSfill (); ! 597: ! 598: // And the lighter & darker spots on the ball... ! 599: ! 600: PSarcn (RADIUS-SHADOWOFFSET/2, RADIUS+SHADOWOFFSET/2, ! 601: RADIUS-SHADOWOFFSET-3.0, 170.0, 100.0); ! 602: PSarc (RADIUS-SHADOWOFFSET/2, RADIUS+SHADOWOFFSET/2, ! 603: RADIUS-SHADOWOFFSET-2.0, 100.0, 170.0); ! 604: PSsetgray (NX_WHITE); ! 605: PSfill (); ! 606: PSarcn (RADIUS-SHADOWOFFSET/2, RADIUS+SHADOWOFFSET/2, ! 607: RADIUS-SHADOWOFFSET-2.0, 350.0, 280.0); ! 608: PSarc (RADIUS-SHADOWOFFSET/2, RADIUS+SHADOWOFFSET/2, ! 609: RADIUS-SHADOWOFFSET-2.0, 280.0, 350.0); ! 610: PSsetgray (NX_DKGRAY); ! 611: PSfill (); ! 612: ! 613: return self; ! 614: } ! 615: ! 616: // Function to draw a shadow under the given rectangle. ! 617: ! 618: static void drawRectangularShadowUnder (NXRect *rect, float offset) ! 619: { ! 620: NXRect shadeRect = *rect; ! 621: NXOffsetRect (&shadeRect, offset, -offset); ! 622: ! 623: PSsetgray (NX_BLACK); ! 624: if (NXDrawingStatus != NX_PRINTING) { ! 625: PSsetalpha (0.666); ! 626: } ! 627: NXRectFill (&shadeRect); ! 628: if (NXDrawingStatus != NX_PRINTING) { ! 629: PSsetalpha (1.0); ! 630: } ! 631: } ! 632: ! 633: - drawPaddle:imageRep ! 634: { ! 635: NXRect pieceRect = {{0.0, SHADOWOFFSET}, ! 636: {(paddleSize.width-SHADOWOFFSET)-1, ! 637: (paddleSize.height-SHADOWOFFSET)-1}}; ! 638: ! 639: drawRectangularShadowUnder (&pieceRect, SHADOWOFFSET); ! 640: ! 641: NXDrawButton (&pieceRect, NULL); ! 642: ! 643: return self; ! 644: } ! 645: ! 646: - drawToughTile:imageRep ! 647: { ! 648: NXRect pieceRect = {{0.0, SHADOWOFFSET}, ! 649: {(tileSize.width-SHADOWOFFSET)-1, ! 650: (tileSize.height-SHADOWOFFSET)-1}}; ! 651: ! 652: drawRectangularShadowUnder (&pieceRect, SHADOWOFFSET); ! 653: ! 654: NXDrawButton (&pieceRect, NULL); ! 655: NXInsetRect (&pieceRect, 3.0, 3.0); ! 656: NXDrawWhiteBezel (&pieceRect, NULL); ! 657: ! 658: return self; ! 659: } ! 660: ! 661: - drawNormalTile:imageRep ! 662: { ! 663: NXRect pieceRect = {{0.0, SHADOWOFFSET}, ! 664: {(tileSize.width-SHADOWOFFSET)-1, ! 665: (tileSize.height-SHADOWOFFSET)-1}}; ! 666: ! 667: drawRectangularShadowUnder (&pieceRect, SHADOWOFFSET); ! 668: ! 669: NXDrawButton (&pieceRect, NULL); ! 670: ! 671: return self; ! 672: } ! 673: ! 674: #define NUMYBOXES 10 ! 675: #define NUMXBOXES 6 ! 676: ! 677: // This method draws the default background. The default background consists of ! 678: // NUMXBOXES x NUMYBOXES raised boxes. Each box is drawn as four triangles to ! 679: // provide a raised effect. Boxes near the top left corner are lighter in color ! 680: // than the ones near the bottom right. ! 681: ! 682: - drawDefaultBackground:imageRep ! 683: { ! 684: #define NOTFOUND ((id)-1) ! 685: static NXColorList *colorList = nil; // Static because it's shared ! 686: NXSize boxSize = {gameSize.width / NUMXBOXES, gameSize.height / NUMYBOXES}; ! 687: int xCnt, yCnt; ! 688: NXColor color; ! 689: ! 690: // The first time we're here, we load and cache the color list. If we don't find ! 691: // it, we remember that fact so that we don't go through the search again. ! 692: if (colorList == nil) { ! 693: char colorListPath[MAXPATHLEN]; ! 694: if ([[NXBundle mainBundle] getPath:colorListPath forResource:"BreakApp" ofType:"clr"]) { ! 695: colorList = [[NXColorList allocFromZone:NXDefaultMallocZone()] initWithName:NULL fromFile:colorListPath]; ! 696: } ! 697: if (!colorList) { ! 698: NXLogError ("Can't find color list for backgrounds."); ! 699: colorList = NOTFOUND; ! 700: } ! 701: } ! 702: ! 703: // Now get the color. If the color list wasn't found, we use some default random color. ! 704: // Note that because colors in different color spaces might look different on ! 705: // different devices (although they might look identical on screen), it's ! 706: // important to always use colors from the same color space when creating ! 707: // a wash. Below we assure that our colors always start off in HSB color space. ! 708: ! 709: if (colorList != NOTFOUND) { ! 710: color = [colorList colorNamed:[colorList nameOfColorAt:(level % [colorList colorCount])]]; ! 711: color = NXConvertHSBToColor (NXHueComponent(color), NXSaturationComponent(color), 1.0); ! 712: } else { ! 713: color = NXConvertHSBToColor((level % 8) / 7.0, 0.8, 1.0); ! 714: } ! 715: ! 716: for (yCnt = 0; yCnt < NUMYBOXES; yCnt++) { ! 717: for (xCnt = 0; xCnt < NUMXBOXES; xCnt++) { ! 718: // Determine brightness (each box has a different brightness) ! 719: color = NXChangeBrightnessComponent(color, 0.4 + (yCnt + (4 - xCnt)) * (0.2 / (NUMYBOXES + NUMXBOXES))); ! 720: // The bottom triangle ! 721: PSmoveto (xCnt * boxSize.width + boxSize.width / 2.0, yCnt * boxSize.height + boxSize.height / 2.0); ! 722: PSrlineto (-boxSize.width / 2.0, -boxSize.height / 2.0); ! 723: PSrlineto (boxSize.width, 0); ! 724: NXSetColor (color); ! 725: PSfill (); ! 726: // The right triangle ! 727: PSmoveto (xCnt * boxSize.width + boxSize.width / 2.0, yCnt * boxSize.height + boxSize.height / 2.0); ! 728: PSrlineto (boxSize.width / 2.0, boxSize.height / 2.0); ! 729: PSrlineto (0, -boxSize.height); ! 730: NXSetColor (NXChangeBrightnessComponent(color, NXBrightnessComponent(color) * 1.2)); ! 731: PSfill (); ! 732: // The left triangle ! 733: PSmoveto (xCnt * boxSize.width + boxSize.width / 2.0, yCnt * boxSize.height + boxSize.height / 2.0); ! 734: PSrlineto (-boxSize.width / 2.0, boxSize.height / 2.0); ! 735: PSrlineto (0, -boxSize.height); ! 736: NXSetColor (NXChangeBrightnessComponent(color, NXBrightnessComponent(color) * 1.2 * 1.2)); ! 737: PSfill (); ! 738: // The right triangle ! 739: PSmoveto (xCnt * boxSize.width + boxSize.width / 2.0, yCnt * boxSize.height + boxSize.height / 2.0); ! 740: PSrlineto (boxSize.width / 2.0, boxSize.height / 2.0); ! 741: PSrlineto (-boxSize.width, 0.0); ! 742: NXSetColor (NXChangeBrightnessComponent(color, NXBrightnessComponent(color) * 1.2 * 1.2 * 1.2)); ! 743: PSfill (); ! 744: } ! 745: } ! 746: return self; ! 747: } ! 748: ! 749: // The following methods show or erase the ball and the paddle from the field. ! 750: ! 751: - showBall ! 752: { ! 753: NXRect tmpRect = {{floor(ballX), floor(ballY)}, ! 754: {ballSize.width, ballSize.height}}; ! 755: [ball composite:NX_SOVER toPoint:&tmpRect.origin]; ! 756: return self; ! 757: } ! 758: ! 759: - showPaddle ! 760: { ! 761: NXRect tmpRect = {{floor(paddleX), floor(paddleY)}, ! 762: {paddleSize.width, paddleSize.height}}; ! 763: [paddle composite:NX_SOVER toPoint:&tmpRect.origin]; ! 764: return self; ! 765: } ! 766: ! 767: - eraseBall ! 768: { ! 769: NXRect tmpRect = {{ballX, ballY}, {ballSize.width, ballSize.height}}; ! 770: return [self drawBackground:&tmpRect]; ! 771: } ! 772: ! 773: - erasePaddle ! 774: { ! 775: NXRect tmpRect = {{paddleX, paddleY}, ! 776: {paddleSize.width, paddleSize.height}}; ! 777: return [self drawBackground:&tmpRect]; ! 778: } ! 779: ! 780: // drawBackground: just draws the specified piece of the background by ! 781: // compositing from the background image. ! 782: ! 783: - drawBackground:(NXRect *)rect ! 784: { ! 785: NXRect tmpRect = *rect; ! 786: ! 787: NX_X(&tmpRect) = floor(NX_X(&tmpRect)); ! 788: NX_Y(&tmpRect) = floor(NX_Y(&tmpRect)); ! 789: if (NXDrawingStatus == NX_DRAWING) { ! 790: PSsetgray (NX_WHITE); ! 791: PScompositerect (NX_X(&tmpRect), NX_Y(&tmpRect), ! 792: NX_WIDTH(&tmpRect), NX_HEIGHT(&tmpRect), NX_COPY); ! 793: } ! 794: [backGround composite:NX_SOVER fromRect:&tmpRect toPoint:&tmpRect.origin]; ! 795: return self; ! 796: } ! 797: ! 798: // drawSelf::, a method every decent View should have, redraws the game ! 799: // in its current state. This allows us to print the game very easily. ! 800: ! 801: - drawSelf:(NXRect *)rects :(int)rectCount ! 802: { ! 803: int xcnt, ycnt; ! 804: ! 805: [self drawBackground:(rects ? rects : &bounds)]; ! 806: ! 807: for (xcnt = 0; xcnt < NUMTILESX; xcnt++) { ! 808: for (ycnt = 0; ycnt < NUMTILESY; ycnt++) { ! 809: if (tiles[xcnt][ycnt] != NOTILE) { ! 810: NXPoint tileLoc = {floor(leftMargin + (tileSize.width + INTERTILE) * xcnt), floor((tileSize.height + INTERTILE) * ycnt)}; ! 811: [tile[tiles[xcnt][ycnt]] composite:NX_SOVER toPoint:&tileLoc]; ! 812: } ! 813: } ! 814: } ! 815: ! 816: if (lives) { ! 817: [self showBall]; ! 818: [self showPaddle]; ! 819: } ! 820: ! 821: return self; ! 822: } ! 823: ! 824: // incrementGameScore: adds the value of the argument to the score if the game ! 825: // is not in demo mode. ! 826: ! 827: - incrementGameScore:(int)scoreIncrement ! 828: { ! 829: if (demoMode == NO) { ! 830: score += scoreIncrement; ! 831: } ! 832: return self; ! 833: } ! 834: ! 835: // hitTileAt:: checks to see if there's a tile at tile location x, y; ! 836: // if so, it is considered hit by the ball and cleared. hitTileTile:: also ! 837: // updates the score and the ball velocity. hitTileAt:: returns YES if there ! 838: // was a tile, NO otherwise. ! 839: ! 840: -(BOOL) hitTileAt:(int)x :(int)y ! 841: { ! 842: NXRect rect = {{floor(leftMargin + (tileSize.width + INTERTILE) * x), ! 843: floor((tileSize.height + INTERTILE) * y)}, ! 844: {tileSize.width, tileSize.height}}; ! 845: ! 846: if (x < NUMTILESX && y < NUMTILESY && x >= 0 && y >= 0 && ! 847: (tiles[x][y] != NOTILE)) { ! 848: [self incrementGameScore:tileScores[tiles[x][y]]]; ! 849: ballYVel = restrictValue(ballYVel * tileAccs[tiles[x][y]], MAXYV); ! 850: [self drawBackground:&rect]; ! 851: tiles[x][y] = NOTILE; ! 852: numTilesLeft--; ! 853: return YES; ! 854: } else { ! 855: return NO; ! 856: } ! 857: } ! 858: ! 859: ! 860: // The paddleHit method is called whenever the ball hits the paddle. ! 861: // This method bounces the ball back at an angle depending on what part of ! 862: // the paddle was hit. ! 863: ! 864: - paddleHit ! 865: { ! 866: float whereHit = ((ballX + RADIUS) - paddleX) / paddleSize.width; ! 867: ! 868: ballYVel = -ballYVel; ! 869: ballY = paddleSize.height; ! 870: ! 871: [self playSound:paddleSound atXLoc:paddleX]; ! 872: ! 873: // Alter the x-velocity and make sure it is in the valid range. ! 874: // If the ball hits the edges of the paddle, bounce it back at some angle. ! 875: ! 876: if (whereHit < 0.1) { ! 877: ballXVel = - MAXXV; ! 878: } else if (whereHit > 0.9) { ! 879: ballXVel = MAXXV; ! 880: } else { ! 881: // Now whereHit is in the range 0.1 .. 0.9, with 0.5 indicating middle ! 882: // of the paddle. Convert to a number in the range 0.2 to 1, with 0.2 ! 883: // indicating the middle and 1 either end. ! 884: whereHit = (fabs(whereHit - 0.5) + 0.1) * 2.0; ! 885: ballXVel = ((ballXVel > 0.0) ? 1.0 : -1.0) * MAXXV * whereHit; ! 886: } ! 887: ! 888: return self; ! 889: } ! 890: ! 891: // If upon launch we discover that there's no sound, then we fail ! 892: // silently. Note that although the SoundEffect class has the ability ! 893: // to enable/disable sounds, because we might have multiple ! 894: // BreakViews each with its own sound state, we keep a local state in ! 895: // addition to the one in SoundEffect. ! 896: ! 897: // Note that although this is an outlet method, we do not actually ! 898: // have an outlet (instance variable) named soundStateFrom; we just ! 899: // want to take a look at the initial value of the button and see ! 900: // if sound needs to be turned on... ! 901: ! 902: - setSoundStateFrom:sender ! 903: { ! 904: if ([sender state]) { ! 905: [SoundEffect setSoundEnabled:YES]; ! 906: if (!(soundEnabled = [SoundEffect soundEnabled])) { ! 907: [sender setState:NO]; // Silently fail ! 908: } ! 909: } else { ! 910: soundEnabled = NO; ! 911: } ! 912: return self; ! 913: } ! 914: ! 915: // If user tries to enable sound once the game is launched, and it ! 916: // fails, then we do tell him/her about it. ! 917: ! 918: - setSoundMode:sender ! 919: { ! 920: BOOL desiredState = [sender state]; ! 921: [self setSoundStateFrom:sender]; ! 922: if (desiredState && !soundEnabled) { ! 923: NXRunAlertPanel ( ! 924: NXLocalString ("No Sound", NULL, "Title of alert indicating sounds aren't available"), ! 925: NXLocalString ("Can't play sounds.", NULL, "Contents of alert panel"), ! 926: NXLocalString ("Bummer", NULL, "Acceptance that sounds can't be played"), ! 927: NULL, NULL); ! 928: } ! 929: return self; ! 930: } ! 931: ! 932: - (void)playSound:sound atXLoc:(float)xLoc ! 933: { ! 934: if (soundEnabled) { ! 935: [sound play:1.0 pan:restrictValue((xLoc / gameSize.width - 0.5) * 2.0, 1.0)]; ! 936: } ! 937: } ! 938: ! 939: // Alters the given velocity vector so that it is ! 940: // rotated by the indicated amount. We restrict both the resulting x and v ! 941: // velocity values to the maximum of their max possible values... ! 942: ! 943: - rotate:(float *)xVel :(float *)yVel by:(float)radians ! 944: { ! 945: float newAngle = atan2 (*yVel, *xVel) + radians; ! 946: float velocity = hypot (*xVel, *yVel); ! 947: ! 948: *yVel = restrictValue(velocity * sin(newAngle), MAX(MAXYV, MAXXV)); ! 949: *xVel = restrictValue(velocity * cos(newAngle), MAX(MAXYV, MAXXV)); ! 950: ! 951: return self; ! 952: } ! 953: ! 954: // The step method implements one step through the main game loop. ! 955: // The distance traveled by the ball is adjusted by the time between frames. ! 956: ! 957: - step:(double)timeNow ! 958: { ! 959: NXPoint mouseLoc; ! 960: float newX; ! 961: unsigned int timeDelta = MIN(MAX((timeNow - lastFrameTime) * 1000, MINTIMEDIFFERENCE), MAXTIMEDIFFERENCE); ! 962: lastFrameTime = timeNow; ! 963: ! 964: [self lockFocus]; ! 965: ! 966: [self eraseBall]; ! 967: ! 968: // If the ball is rotating, rotate it by the indicated amount. ! 969: ! 970: if (revolutionsLeft > 0.0) { ! 971: float revsThisTime = revolutionSpeed * timeDelta; ! 972: [self rotate:&ballXVel :&ballYVel by:revsThisTime]; ! 973: revolutionsLeft -= revsThisTime; ! 974: if (revolutionsLeft <= 0.0 && (fabs(ballYVel) < MAXYV * 0.6)) { ! 975: // Done rotating; make sure we have a good y-velocity ! 976: ballYVel = MAXYV * 0.8 * (ballYVel < 0.0 ? -1 : 1); ! 977: ballXVel = restrictValue(ballXVel,MAXXV); ! 978: } ! 979: } else if (ONEIN(1000 + (level < 8 ? (8 - level) * 250 : 0)) && (ballY > gameSize.height * 0.6)) { ! 980: // If we're not rotating, we go into rotating mode one out of ! 981: // 1500 or more steps, provided that the ball is not too close to ! 982: // the paddle at the time. ! 983: revolutionsLeft = M_PI * (2 + RANDINT(5)); // 1 to 3.5 full turns ! 984: revolutionSpeed = MAXREVOLUTIONSPEED * (RANDINT(8) + 2.0) / 10.0; ! 985: } ! 986: ! 987: // Update the ball location ! 988: ! 989: ballX += ballXVel * timeDelta * gameSize.width / GAMEWIDTH; ! 990: ballY += ballYVel * timeDelta * gameSize.height / GAMEHEIGHT; ! 991: ! 992: ! 993: if (gameRunning) { ! 994: ! 995: if (ballX < 0.0) { // Hit on the left wall ! 996: ballX = 0.0; ! 997: ballXVel = -ballXVel; ! 998: [self playSound:wallSound atXLoc:ballX]; ! 999: } else if (ballX > gameSize.width - ballSize.width) { // Right wall ! 1000: ballX = gameSize.width - ballSize.width; ! 1001: ballXVel = -ballXVel; ! 1002: [self playSound:wallSound atXLoc:ballX]; ! 1003: } ! 1004: ! 1005: if (ballY > gameSize.height - ballSize.height) { // Top wall ! 1006: ballY = gameSize.height - ballSize.height; ! 1007: ballYVel = -ballYVel; ! 1008: if (niceBall && !ONEIN(5) && !demoMode) { ! 1009: NXPoint mid = {paddleX + paddleSize.width / 2.0, paddleY}; ! 1010: [self directBallAt:&mid]; ! 1011: } else if (ONEIN(10)) { ! 1012: ballXVel = MAXXV-(RANDINT((int)(MAXXV*20))/10.0); ! 1013: } ! 1014: [self playSound:wallSound atXLoc:ballX]; ! 1015: } ! 1016: ! 1017: // Now checking for collisions with tiles... ! 1018: ! 1019: { ! 1020: int y1 = (int)(floor(ballY / ! 1021: (tileSize.height + INTERTILE))); ! 1022: int x1 = (int)(floor((ballX - leftMargin) / ! 1023: (tileSize.width + INTERTILE))); ! 1024: int y2 = (int)(floor((ballY + ballSize.height) / ! 1025: (tileSize.height + INTERTILE))); ! 1026: int x2 = (int)(floor((ballX + ballSize.width - leftMargin) / ! 1027: (tileSize.width + INTERTILE))); ! 1028: ! 1029: if ([self hitTileAt:x1 :y1] | [self hitTileAt:x2 :y1] | ! 1030: [self hitTileAt:x1 :y2] | [self hitTileAt:x2 :y2]) { ! 1031: [self playSound:tileSound atXLoc:ballX]; ! 1032: if (!killerBall) { ! 1033: ballYVel = -ballYVel; ! 1034: } ! 1035: [scoreView setIntValue:score]; ! 1036: [[self window] flushWindow]; ! 1037: } ! 1038: } ! 1039: } ! 1040: ! 1041: // Get the mouse location and convert from window to the view coords. ! 1042: // If in demo, mode, make the paddle track the ball. Endless fun. ! 1043: ! 1044: if (demoMode) { ! 1045: mouseLoc.x = ballX + ballSize.width / 2.0; ! 1046: } else { ! 1047: [[self window] getMouseLocation:&mouseLoc]; ! 1048: [self convertPoint:&mouseLoc fromView:nil]; ! 1049: } ! 1050: ! 1051: newX = MAX(MIN(mouseLoc.x - paddleSize.width/2, ! 1052: gameSize.width - paddleSize.width), 0); ! 1053: ! 1054: if (ballY >= paddleY + paddleSize.height) { ! 1055: ! 1056: // Ball is above the paddle; redraw it and the paddle and continue ! 1057: // We flush twice as the ball and the paddle are not too close ! 1058: // together ! 1059: ! 1060: [self showBall]; ! 1061: [[self window] flushWindow]; ! 1062: [self erasePaddle]; ! 1063: paddleX = newX; ! 1064: [self showPaddle]; ! 1065: [[self window] flushWindow]; ! 1066: ! 1067: } else if (ballY + ballSize.height > 0) { ! 1068: ! 1069: // Ball is past the paddle but not totally gone... ! 1070: ! 1071: [self erasePaddle]; ! 1072: paddleX = newX; ! 1073: ! 1074: // Check to see if the user managed to catch the ball after all ! 1075: ! 1076: if ((ballY > paddleY - ballSize.height / 2.0) && ! 1077: (ballX <= paddleX + paddleSize.width) && ! 1078: (ballX + ballSize.width > paddleX)) { ! 1079: [self paddleHit]; ! 1080: } ! 1081: ! 1082: // The ball and the paddle are close, so one flushWindow is fine. ! 1083: ! 1084: [self showBall]; ! 1085: [self showPaddle]; ! 1086: [[self window] flushWindow]; ! 1087: ! 1088: } else { ! 1089: ! 1090: // Too late; the ball is out of sight... ! 1091: ! 1092: [self erasePaddle]; ! 1093: [self stop:self]; ! 1094: [self playSound:missSound atXLoc:0.0]; ! 1095: ! 1096: if (--lives == 0) { ! 1097: if (score > highScore) [self setHighScore:score]; ! 1098: [statusView setStringValue:NXLocalString("Game Over", NULL, "Message indicating that the game is over...")]; ! 1099: } else { ! 1100: [self resetBallAndPaddle]; ! 1101: [self showBall]; ! 1102: [self showPaddle]; ! 1103: [statusView setStringValue:NXLocalString("Game Ready", NULL, "Message indicating the game is ready to run")]; ! 1104: } ! 1105: [[self window] flushWindow]; ! 1106: ! 1107: [livesView setIntValue:lives]; ! 1108: ! 1109: } ! 1110: ! 1111: // numTilesLeft <= 0 indicates that we've blown away every tile. But, ! 1112: // to make the game more exciting, we start decrementing numTilesLeft, ! 1113: // by one everytime through this loop, until it reaches the value ! 1114: // STOPGAMEAT. This makes the ball move a bit more after all the tiles ! 1115: // are gone. But, if gameRunning is NO, then it means we probably just ! 1116: // missed the ball, in which case we should go ahead and jump to the ! 1117: // next level. ! 1118: ! 1119: if ((numTilesLeft <= 0) && ! 1120: ((lives && !gameRunning) || (--numTilesLeft == STOPGAMEAT))) { ! 1121: [self incrementGameScore:LEVELBONUS]; ! 1122: [self gotoNextLevel:self]; ! 1123: } ! 1124: ! 1125: NXPing (); // Synchronize postscript for smoother animation ! 1126: ! 1127: [self unlockFocus]; ! 1128: ! 1129: return self; ! 1130: } ! 1131: ! 1132: // Pretty much a dummy function to invoke the step method. ! 1133: ! 1134: void runOneStep (DPSTimedEntry timedEntry, double timeNow, void *data) ! 1135: { ! 1136: [(id)data step:timeNow]; ! 1137: } ! 1138: ! 1139: ! 1140: @end
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.