home *** CD-ROM | disk | FTP | other *** search
/ NeXTSTEP 3.3 (Developer) / NeXT_Developer-3.3.iso / NextDeveloper / Examples / AppKit / BreakApp / BreakView.m < prev    next >
Encoding:
Text File  |  1993-03-19  |  33.5 KB  |  1,141 lines

  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
  1141.