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