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