home *** CD-ROM | disk | FTP | other *** search
Text File | 1993-03-19 | 33.5 KB | 1,141 lines |
- /*
- * BreakView.m, view to implement the "BreakApp" game.
- * Author: Ali Ozer
- * Written for 0.8 October 88.
- * Modified for 0.9 March 89.
- * Modified for 1.0 July 89.
- * Removed use of Bitmap and threw away some classes May 90.
- * Final 2.0 fixes/enhancements Sept 90.
- * 3.0 update March 92.
- * Sound-related changes for 3.1 March 92.
- *
- * BreakView implements an interactive custom view that allows the user
- * to play "BreakApp," a game similar to a popular arcade classic.
- *
- * BreakView's main control methods are based on the target-action
- * paradigm; thus you can include BreakView in an Interface-Builder based
- * application. Please refer to BreakView.h for a list of "public" methods
- * that you should provide links to in Interface Builder.
- *
- * You may freely copy, distribute and reuse the code in this example.
- * NeXT disclaims any warranty of any kind, expressed or implied,
- * as to its fitness for any particular use.
- */
-
- #import <libc.h>
- #import <math.h>
- #import <defaults/defaults.h> // For writing/reading high score
- #import <appkit/appkit.h>
-
- #import "BreakView.h"
- #import "SoundEffect.h"
-
- // Max absolute x and y velocities of the ball, in base coordinates per msec.
-
- #define MAXXV ((level > 6) ? 0.3 : 0.2)
- #define MAXYV (0.4)
-
- // Maximum amount of time that is allowed to pass between two calls to the
- // step method. If the time is greater than MAXTIMEDIFFERENCE, then this
- // value is used instead. MAXTIMEDIFFERENCE should be no greater
- // than the time it takes for the ball to go the height of a tile
- // or the height of the ball + height of paddle. The units
- // are in milliseconds.
-
- #define MAXTIMEDIFFERENCE (TILEHEIGHT * 0.8 / MAXYV)
- #define MINTIMEDIFFERENCE 1
-
- // Max revolution speed of the ball; this is the maximum
- // number of radians it will turn per millisecond when rotating...
-
- #define MAXREVOLUTIONSPEED (M_PI / 250.0) // Max is 2 revs/sec
-
- // The following values are the default sizes for the various pieces.
-
- #define RADIUS 8.0 // Ball radius
- #define PADDLEWIDTH (TILEWIDTH * 1.8) // Paddle width
- #define PADDLEHEIGHT (TILEHEIGHT * 0.6) // Paddle height
- #define BALLWIDTH (RADIUS * 2.0) // Ball width
- #define BALLHEIGHT (RADIUS * 2.0) // Ball height
-
- // SHADOWOFFSET defines the amount the shadow is offset from the piece.
-
- #define SHADOWOFFSET 3.0
-
- #define LIVES 5 // Number of lives per game
-
- #define STOPGAMEAT (-10) // Number of loops through the
- // game after all tiles die
-
- #define LEVELBONUS 50 // Bonus at the end of a level
-
- // Starting locations...
-
- #define PADDLEX ((gameSize.width - paddleSize.width) / 2.0)
- #define PADDLEY 1.0
- #define BALLX ((gameSize.width - ballSize.width) / 2.0)
- #define BALLY (paddleY + paddleSize.height)
-
- // Accelaration & score values of the different tile types.
-
- static const float tileAccs[NUMTILETYPES] = {1.0, 1.3};
- static const int tileScores[NUMTILETYPES] = {5, 25};
-
- #define NOTILE -1
-
- extern void srandom(); // Hmm; not in libc.h
- #define RANDINT(n) (random() % ((n)+1)) // Random integer 0..n
- #define ONEIN(n) ((random() % (n)) == 0) // TRUE one in n times
- #define INITRAND srandom(time(0)) // Randomizer
-
- #define gameSize bounds.size
-
- // Restrict a value to the range -max .. max.
-
- inline float restrictValue(float val, float max)
- {
- if (val > max) return max;
- else if (val < -max) return -max;
- else return val;
- }
-
- // Convert x-location to left/right pan for playing sounds
-
- @implementation BreakView
-
- - initFrame:(const NXRect *)frm
- {
- [super initFrame:frm];
-
- [self allocateGState]; // For faster lock/unlockFocus
-
- [(ball = [[NXImage allocFromZone:[self zone]] init]) setScalable:NO];
- [ball useDrawMethod:@selector(drawBall:) inObject:self];
-
- [(paddle = [[NXImage allocFromZone:[self zone]] init]) setScalable:NO];
- [paddle useDrawMethod:@selector(drawPaddle:) inObject:self];
-
- [(tile[0] = [[NXImage allocFromZone:[self zone]] init]) setScalable:NO];
- [(tile[1] = [[NXImage allocFromZone:[self zone]] init]) setScalable:NO];
- [tile[0] useDrawMethod:@selector(drawNormalTile:) inObject:self];
- [tile[1] useDrawMethod:@selector(drawToughTile:) inObject:self];
-
- wallSound = [[SoundEffect allocFromZone:[self zone]] initFromSection:"Wall.snd"];
- tileSound = [[SoundEffect allocFromZone:[self zone]] initFromSection:"Tile.snd"];
- missSound = [[SoundEffect allocFromZone:[self zone]] initFromSection:"Miss.snd"];
- paddleSound = [[SoundEffect allocFromZone:[self zone]] initFromSection:"Paddle.snd"];
-
- [self setBackgroundFile:NXGetDefaultValue([NXApp appName], "BackGround") andRemember:NO];
-
- [self resizePieces];
-
- [self getHighScore];
-
- demoMode = NO;
-
- INITRAND;
-
- return self;
- }
-
- // free simply gets rid of everything we created for BreakView, including
- // the instance of BreakView itself. This is how nice objects clean up.
-
- - free
- {
- int cnt;
-
- if (gameRunning) {
- DPSRemoveTimedEntry (timer);
- }
- for (cnt = 0; cnt < NUMTILETYPES; cnt++) {
- [tile[cnt] free];
- }
-
- [ball free];
- [paddle free];
- [backGround free];
- [wallSound free];
- [tileSound free];
- [missSound free];
- [paddleSound free];
-
- return [super free];
- }
-
- // resizePieces calculates the new sizes of all the pieces after the game is
- // started or the playing field (the BreakView) is resized.
-
- - resizePieces
- {
- int cnt;
- float xRatio = gameSize.width / GAMEWIDTH;
- float yRatio = gameSize.height / GAMEHEIGHT;
-
- [backGround setSize:&gameSize];
-
- tileSize.width = floor(xRatio * TILEWIDTH);
- tileSize.height = floor(yRatio * TILEHEIGHT);
- for (cnt = 0; cnt < NUMTILETYPES; cnt++) {
- [tile[cnt] setSize:&tileSize];
- }
- leftMargin = floor((gameSize.width - (tileSize.width + INTERTILE) *
- NUMTILESX) / 2.0 + 1.0);
-
- paddleSize.width = floor(xRatio * PADDLEWIDTH);
- paddleSize.height = floor(yRatio * PADDLEHEIGHT);
- [paddle setSize:&paddleSize];
-
- ballSize.width = floor(xRatio * BALLWIDTH);
- ballSize.height = floor(yRatio * BALLHEIGHT);
- [ball setSize:&ballSize];
-
- return self;
- }
-
- // The following allows BreakView to grab the mousedown event that activates
- // the window. By default, the View's acceptsFirstMouse returns NO.
-
- - (BOOL)acceptsFirstMouse
- {
- return YES;
- }
-
- // This methods allows changing the file used to paint the background of the
- // playing field. Set fileName to NULL to revert to the default. Set
- // remember to YES if you wish the write the value out in the defaults.
-
- - setBackgroundFile:(const char *)fileName andRemember:(BOOL)remember
- {
- [backGround free];
- backGround = [[NXImage allocFromZone:[self zone]] initSize:&gameSize];
- if (fileName) {
- [backGround useFromFile:fileName];
- [backGround setScalable:YES];
- if (remember) {
- NXWriteDefault ([NXApp appName], "BackGround", fileName);
- }
- } else {
- [backGround useDrawMethod:@selector(drawDefaultBackground:) inObject:self];
- [backGround setScalable:NO];
- if (remember) {
- NXRemoveDefault ([NXApp appName], "BackGround");
- }
- }
- [backGround setBackgroundColor:NX_COLORWHITE];
- [self display];
-
- return self;
- }
-
- // The following two methods allow changing the background image from
- // menu items or buttons.
-
- - changeBackground:sender
- {
- if ([[OpenPanel new] runModalForTypes:[NXImage imageFileTypes]]) {
- [self setBackgroundFile:[[OpenPanel new] filename] andRemember:YES];
- [self display];
- }
-
- return self;
- }
-
- - revertBackground:sender
- {
- [self setBackgroundFile:NULL andRemember:YES];
- [self display];
- return self;
- }
-
- // getHighScore reads the previous high score from the user's defaults file.
- // If no such default is found, then the high score is set to zero.
-
- - getHighScore
- {
- const char *tmpstr;
- if (((tmpstr = NXGetDefaultValue ([NXApp appName], "HighScore")) &&
- (sscanf(tmpstr, "%d", &highScore) != 1))) highScore = 0;
-
- return self;
- }
-
- // setHighScore should be called when the user score for a game is above
- // the current high score. setHighScore sets the high score and
- // writes it out the defaults file so that it can be remembered for eternity.
-
- - setHighScore:(int)hScore
- {
- char str[10];
- [hscoreView setIntValue:(highScore = hScore)];
- sprintf (str, "%d", highScore);
- NXWriteDefault ([NXApp appName], "HighScore", str);
- return self;
- }
-
- - (int)score
- {
- return score;
- }
-
- - (int)level
- {
- return level;
- }
-
- - (int)lives
- {
- return lives;
- }
-
- // gotoFirstLevel: sets everything up for a new game.
-
- - gotoFirstLevel:sender
- {
- score = 0;
- level = 0;
- lives = LIVES;
- return [self gotoNextLevel:sender];
- }
-
- // gotoNextLevel: sets everything up for the next level of the game; the level
- // count is incremented and the pieces are set up on the field. The ball and
- // the paddle are also brought to the starting locations.
- //
- // This routine can of course be made infinitely more complicated in
- // determining where the tiles go. Left as an exercise to the reader. 8-)
-
- - gotoNextLevel:sender
- {
- int xcnt, ycnt, yFrom, yTo, xFrom, xTo;
-
- // We are at the next level... Stop the game and increment the level.
-
- [self stop:sender];
-
- level++;
-
- // Now place the tiles. Here's where we could do some fancy tile layout,
- // depending on the game level. yFrom, yTo, xFrom, and xTo define the "box"
- // in which we will lay the tiles out. These values are inclusive.
-
- switch (level % 6) {
- case 0: yTo = NUMTILESY-2; break;
- case 4: yTo = NUMTILESY-4; break;
- case 5: yTo = 2 * (NUMTILESY / 3); break;
- default: yTo = 3 * (NUMTILESY / 4); break;
- }
-
- xFrom = 0; xTo = NUMTILESX-1; yFrom = yTo - 3;
-
- switch (level % 10) {
- case 1: yFrom++; break;
- case 2: yFrom--; xFrom++; xTo--; break;
- case 4: xFrom += 2; xTo -= 2; break;
- case 6: yFrom = MIN(yFrom, NUMTILESY / 4); xFrom++; xTo--; break;
- case 7: xTo -= 3; break;
- case 8: yFrom -= 2; xFrom += 2; xTo -= 2; break;
- case 9: yFrom = MIN(yFrom, NUMTILESY / 4);
- yTo = MAX(yTo, yFrom+4);
- break;
- case 0: yFrom = MIN(yFrom, NUMTILESY / 5);
- xFrom += (NUMTILESX / 2);
- break;
- default: break;
- }
-
- // The area in the playing field where we place tiles is at least 3 tiles
- // high and at least NUMTILESX-4 tiles wide.
-
- // Empty out the whole playing field.
- for (xcnt = 0; xcnt < NUMTILESX; xcnt++) {
- for (ycnt = 0; ycnt < NUMTILESY; ycnt++) {
- tiles[xcnt][ycnt] = NOTILE;
- }
- }
-
- // Fill up the tile area with wimpy tiles
- for (ycnt = yFrom; ycnt <= yTo; ycnt++) {
- for (xcnt = xFrom; xcnt <= xTo; xcnt++) {
- tiles[xcnt][ycnt] = 0;
- }
- }
-
- // Erase or change some of the tiles, depending on the level.
- // Assumption is that we have at least 3 rows of tiles, yFrom..yTo.
-
- switch (level % 7) {
- case 2: // clear two rows in the middle
- for (xcnt = xFrom; xcnt <= xTo; xcnt++) {
- tiles[xcnt][yFrom+1] = tiles[xcnt][yTo-1] = NOTILE;
- }
- break;
- case 3: // randomly clear out some tiles
- for (xcnt = 0; xcnt < 5; xcnt++) {
- tiles[xFrom+RANDINT(xTo-xFrom)][yFrom+1+RANDINT(yTo-yFrom-2)] =
- NOTILE;
- }
- break;
- case 4: // clear middle columns
- for (xcnt = xFrom + 2; xcnt <= xTo - 2; xcnt++) {
- for (ycnt = yFrom; ycnt <= yTo; ycnt++) {
- tiles[xcnt][ycnt] = NOTILE;
- }
- }
- break;
- case 6: // clear out the insides
- for (ycnt = yFrom+1; ycnt < yTo; ycnt++) {
- for (xcnt = xFrom+1; xcnt < xTo; xcnt++) {
- tiles[xcnt][ycnt] = NOTILE;
- }
- }
- break;
- default:
- break;
- }
-
- // Drop in some tough tiles in all rows except the first one
- for (xcnt = 0; xcnt < 5; xcnt++) {
- tiles[xFrom+RANDINT(xTo-xFrom)][yFrom+1+RANDINT(yTo-yFrom-1)] = 1;
- }
-
- // Compute the number of tiles we actually ended up putting down...
- numTilesLeft = 0;
- for (ycnt = yFrom; ycnt <= yTo; ycnt++) {
- for (xcnt = xFrom; xcnt <= xTo; xcnt++) {
- if (tiles[xcnt][ycnt] != NOTILE) numTilesLeft++;
- }
- }
-
- // Of course you might think there are too many braces in the above code,
- // where probably none would've sufficed. Too many braces never hurt, & it
- // will save you from some bozo bug some day. So use them! They're cheap!
-
- [self resetBallAndPaddle];
-
- [levelView setIntValue:level];
- [scoreView setIntValue:score];
- [livesView setIntValue:lives];
- [hscoreView setIntValue:highScore];
- [statusView setStringValue:NXLocalString("Game Ready", NULL, "Message indicating the game is ready to run")];
-
- killerBall = ((level % 12) == 0); // Every 12 turns, the loses its
- // ability to bounce off tiles
- niceBall = (level % 5 == 0); // Every 5 turns, make the ball
- // bounce towards the paddle
-
- // If the background image is not from a file but our own default,
- // poke it so its redrawn. This way every level will look different.
- // We could've simply used a BOOL to remember if the image is the default
- // one, but this test here works as well.
-
- if ([[backGround lastRepresentation] isKindOf:[NXCustomImageRep class]]) {
- [backGround recache];
- }
-
- [self display]; // Display the new arrangement
-
- if (demoMode) {
- [self go:sender]; // If in demo mode, start rolling
- }
-
- return self;
- }
-
- // setDemoMode: allows the user to put the game in a demo mode.
- // In the demo mode, the paddle constantly follows the ball.
-
- - setDemoMode:sender
- {
- if (demoMode = ([sender state] == 0 ? NO : YES)) {
- [self go:sender];
- } else {
- [self stop:sender];
- }
- return self;
- }
-
- // This method should be called when a new level or game is started or the
- // player misses the ball. It resets the ball & paddle locations back to
- // default.
-
- - resetBallAndPaddle
- {
- paddleX = PADDLEX;
- paddleY = PADDLEY;
- ballX = BALLX;
- ballY = BALLY;
-
- ballXVel = 0.0;
- ballYVel = 0.0;
-
- // The ball shouldn't start out rotating...
- revolutionsLeft = 0;
-
- return self;
- }
-
- // The directBallAt: initializes the velocity vector of the ball so that
- // the ball will go from its current location to the specified destination
- // point. The speed of the ball is determined by the current level. If ballYVel
- // is already set, then only the x velocity & y direction is changed.
-
- - directBallAt:(NXPoint *)dest
- {
- float desiredYVel = dest->y - (ballY + ballSize.height / 2.0);
- float desiredXVel = dest->x - (ballX + ballSize.width / 2.0);
-
- // Transform back to original game coords (velocity values are measured
- // in these).
-
- desiredYVel /= (gameSize.height / GAMEHEIGHT);
- desiredXVel /= (gameSize.width / GAMEWIDTH);
-
- if (fabs(desiredYVel) < 1.0) {
- desiredYVel = desiredYVel < 0.0 ? -1.0 : 1.0;
- }
- if (ballYVel == 0.0) {
- // Come up with a value between 60 and 100% of MAXYV.
- ballYVel = restrictValue(((RANDINT(level * 8) + 60.0) / 100.0) * MAXYV,
- MAXYV);
- }
- ballYVel = fabs(ballYVel) * (desiredYVel < 0.0 ? -1.0 : 1.0);
- ballXVel = restrictValue(ballYVel * (desiredXVel / desiredYVel) ,MAXXV);
-
- return self;
- }
-
- // The stop method will pause a running game. The go method will start it up
- // again. They can be assigned to buttons or other appkit objects through IB.
-
- - go:sender
- {
- void runOneStep ();
- if (lives && !gameRunning) {
- // If the ball velocity wasn't initialized, start it rolling
- // towards the mouse location...
- if (ballXVel == 0.0 && ballYVel == 0.0) {
- NXPoint mouseLoc;
- [[self window] getMouseLocation:&mouseLoc];
- [self convertPoint:&mouseLoc fromView:nil];
- [self directBallAt:&mouseLoc];
- ballYVel = fabs(ballYVel);
- }
- gameRunning = YES;
- timer = DPSAddTimedEntry(0.03, &runOneStep, self, NX_BASETHRESHOLD);
- [statusView setStringValue:NXLocalString("Running", NULL, "Message indicating that the game is running")];
- }
- return self;
- }
-
- - stop:sender
- {
- if (gameRunning) {
- gameRunning = NO;
- DPSRemoveTimedEntry (timer);
- [statusView setStringValue:NXLocalString("Paused", NULL, "Message indicating that the game is paused")];
- }
-
- return self;
- }
-
- - sizeTo:(NXCoord)width :(NXCoord)height
- {
- NXSize oldSize = bounds.size;
-
- [super sizeTo:width :height];
-
- ballX = (ballX * width / oldSize.width);
- ballY = (ballY * height / oldSize.height);
- paddleX = (paddleX * width / oldSize.width);
- paddleY = (paddleY * height / oldSize.height);
-
- [self resizePieces];
-
- [self display];
- return self;
- }
-
- // A mousedown effectively allows pausing and unpausing the game by
- // alternately calling one of the above two functions (stop/go).
-
- - mouseDown:(NXEvent *)event
- {
- if (gameRunning) {
- [self stop:self];
- } else if (lives) {
- [self go:self];
- }
- return self;
- }
-
- // The following few methods draw the pieces.
-
- - drawBall:imageRep
- {
- PSscale (ballSize.width / BALLWIDTH, ballSize.height / BALLHEIGHT);
-
- // First draw the shadow under the ball.
-
- PSarc (RADIUS+SHADOWOFFSET/2, RADIUS-SHADOWOFFSET/2,
- RADIUS-SHADOWOFFSET-1.0, 0.0, 360.0);
- PSsetgray (NX_BLACK);
- if (NXDrawingStatus == NX_DRAWING) {
- PSsetalpha (0.666);
- }
- PSfill ();
- if (NXDrawingStatus == NX_DRAWING) {
- PSsetalpha (1.0);
- }
-
- // Then the ball.
-
- PSarc (RADIUS-SHADOWOFFSET/2, RADIUS+SHADOWOFFSET/2,
- RADIUS-SHADOWOFFSET-1.0, 0.0, 360.0);
- PSsetgray (NX_LTGRAY);
- PSfill ();
-
- // And the lighter & darker spots on the ball...
-
- PSarcn (RADIUS-SHADOWOFFSET/2, RADIUS+SHADOWOFFSET/2,
- RADIUS-SHADOWOFFSET-3.0, 170.0, 100.0);
- PSarc (RADIUS-SHADOWOFFSET/2, RADIUS+SHADOWOFFSET/2,
- RADIUS-SHADOWOFFSET-2.0, 100.0, 170.0);
- PSsetgray (NX_WHITE);
- PSfill ();
- PSarcn (RADIUS-SHADOWOFFSET/2, RADIUS+SHADOWOFFSET/2,
- RADIUS-SHADOWOFFSET-2.0, 350.0, 280.0);
- PSarc (RADIUS-SHADOWOFFSET/2, RADIUS+SHADOWOFFSET/2,
- RADIUS-SHADOWOFFSET-2.0, 280.0, 350.0);
- PSsetgray (NX_DKGRAY);
- PSfill ();
-
- return self;
- }
-
- // Function to draw a shadow under the given rectangle.
-
- static void drawRectangularShadowUnder (NXRect *rect, float offset)
- {
- NXRect shadeRect = *rect;
- NXOffsetRect (&shadeRect, offset, -offset);
-
- PSsetgray (NX_BLACK);
- if (NXDrawingStatus != NX_PRINTING) {
- PSsetalpha (0.666);
- }
- NXRectFill (&shadeRect);
- if (NXDrawingStatus != NX_PRINTING) {
- PSsetalpha (1.0);
- }
- }
-
- - drawPaddle:imageRep
- {
- NXRect pieceRect = {{0.0, SHADOWOFFSET},
- {(paddleSize.width-SHADOWOFFSET)-1,
- (paddleSize.height-SHADOWOFFSET)-1}};
-
- drawRectangularShadowUnder (&pieceRect, SHADOWOFFSET);
-
- NXDrawButton (&pieceRect, NULL);
-
- return self;
- }
-
- - drawToughTile:imageRep
- {
- NXRect pieceRect = {{0.0, SHADOWOFFSET},
- {(tileSize.width-SHADOWOFFSET)-1,
- (tileSize.height-SHADOWOFFSET)-1}};
-
- drawRectangularShadowUnder (&pieceRect, SHADOWOFFSET);
-
- NXDrawButton (&pieceRect, NULL);
- NXInsetRect (&pieceRect, 3.0, 3.0);
- NXDrawWhiteBezel (&pieceRect, NULL);
-
- return self;
- }
-
- - drawNormalTile:imageRep
- {
- NXRect pieceRect = {{0.0, SHADOWOFFSET},
- {(tileSize.width-SHADOWOFFSET)-1,
- (tileSize.height-SHADOWOFFSET)-1}};
-
- drawRectangularShadowUnder (&pieceRect, SHADOWOFFSET);
-
- NXDrawButton (&pieceRect, NULL);
-
- return self;
- }
-
- #define NUMYBOXES 10
- #define NUMXBOXES 6
-
- // This method draws the default background. The default background consists of
- // NUMXBOXES x NUMYBOXES raised boxes. Each box is drawn as four triangles to
- // provide a raised effect. Boxes near the top left corner are lighter in color
- // than the ones near the bottom right.
-
- - drawDefaultBackground:imageRep
- {
- #define NOTFOUND ((id)-1)
- static NXColorList *colorList = nil; // Static because it's shared
- NXSize boxSize = {gameSize.width / NUMXBOXES, gameSize.height / NUMYBOXES};
- int xCnt, yCnt;
- NXColor color;
-
- // The first time we're here, we load and cache the color list. If we don't find
- // it, we remember that fact so that we don't go through the search again.
- if (colorList == nil) {
- char colorListPath[MAXPATHLEN];
- if ([[NXBundle mainBundle] getPath:colorListPath forResource:"BreakApp" ofType:"clr"]) {
- colorList = [[NXColorList allocFromZone:NXDefaultMallocZone()] initWithName:NULL fromFile:colorListPath];
- }
- if (!colorList) {
- NXLogError ("Can't find color list for backgrounds.");
- colorList = NOTFOUND;
- }
- }
-
- // Now get the color. If the color list wasn't found, we use some default random color.
- // Note that because colors in different color spaces might look different on
- // different devices (although they might look identical on screen), it's
- // important to always use colors from the same color space when creating
- // a wash. Below we assure that our colors always start off in HSB color space.
-
- if (colorList != NOTFOUND) {
- color = [colorList colorNamed:[colorList nameOfColorAt:(level % [colorList colorCount])]];
- color = NXConvertHSBToColor (NXHueComponent(color), NXSaturationComponent(color), 1.0);
- } else {
- color = NXConvertHSBToColor((level % 8) / 7.0, 0.8, 1.0);
- }
-
- for (yCnt = 0; yCnt < NUMYBOXES; yCnt++) {
- for (xCnt = 0; xCnt < NUMXBOXES; xCnt++) {
- // Determine brightness (each box has a different brightness)
- color = NXChangeBrightnessComponent(color, 0.4 + (yCnt + (4 - xCnt)) * (0.2 / (NUMYBOXES + NUMXBOXES)));
- // The bottom triangle
- PSmoveto (xCnt * boxSize.width + boxSize.width / 2.0, yCnt * boxSize.height + boxSize.height / 2.0);
- PSrlineto (-boxSize.width / 2.0, -boxSize.height / 2.0);
- PSrlineto (boxSize.width, 0);
- NXSetColor (color);
- PSfill ();
- // The right triangle
- PSmoveto (xCnt * boxSize.width + boxSize.width / 2.0, yCnt * boxSize.height + boxSize.height / 2.0);
- PSrlineto (boxSize.width / 2.0, boxSize.height / 2.0);
- PSrlineto (0, -boxSize.height);
- NXSetColor (NXChangeBrightnessComponent(color, NXBrightnessComponent(color) * 1.2));
- PSfill ();
- // The left triangle
- PSmoveto (xCnt * boxSize.width + boxSize.width / 2.0, yCnt * boxSize.height + boxSize.height / 2.0);
- PSrlineto (-boxSize.width / 2.0, boxSize.height / 2.0);
- PSrlineto (0, -boxSize.height);
- NXSetColor (NXChangeBrightnessComponent(color, NXBrightnessComponent(color) * 1.2 * 1.2));
- PSfill ();
- // The right triangle
- PSmoveto (xCnt * boxSize.width + boxSize.width / 2.0, yCnt * boxSize.height + boxSize.height / 2.0);
- PSrlineto (boxSize.width / 2.0, boxSize.height / 2.0);
- PSrlineto (-boxSize.width, 0.0);
- NXSetColor (NXChangeBrightnessComponent(color, NXBrightnessComponent(color) * 1.2 * 1.2 * 1.2));
- PSfill ();
- }
- }
- return self;
- }
-
- // The following methods show or erase the ball and the paddle from the field.
-
- - showBall
- {
- NXRect tmpRect = {{floor(ballX), floor(ballY)},
- {ballSize.width, ballSize.height}};
- [ball composite:NX_SOVER toPoint:&tmpRect.origin];
- return self;
- }
-
- - showPaddle
- {
- NXRect tmpRect = {{floor(paddleX), floor(paddleY)},
- {paddleSize.width, paddleSize.height}};
- [paddle composite:NX_SOVER toPoint:&tmpRect.origin];
- return self;
- }
-
- - eraseBall
- {
- NXRect tmpRect = {{ballX, ballY}, {ballSize.width, ballSize.height}};
- return [self drawBackground:&tmpRect];
- }
-
- - erasePaddle
- {
- NXRect tmpRect = {{paddleX, paddleY},
- {paddleSize.width, paddleSize.height}};
- return [self drawBackground:&tmpRect];
- }
-
- // drawBackground: just draws the specified piece of the background by
- // compositing from the background image.
-
- - drawBackground:(NXRect *)rect
- {
- NXRect tmpRect = *rect;
-
- NX_X(&tmpRect) = floor(NX_X(&tmpRect));
- NX_Y(&tmpRect) = floor(NX_Y(&tmpRect));
- if (NXDrawingStatus == NX_DRAWING) {
- PSsetgray (NX_WHITE);
- PScompositerect (NX_X(&tmpRect), NX_Y(&tmpRect),
- NX_WIDTH(&tmpRect), NX_HEIGHT(&tmpRect), NX_COPY);
- }
- [backGround composite:NX_SOVER fromRect:&tmpRect toPoint:&tmpRect.origin];
- return self;
- }
-
- // drawSelf::, a method every decent View should have, redraws the game
- // in its current state. This allows us to print the game very easily.
-
- - drawSelf:(NXRect *)rects :(int)rectCount
- {
- int xcnt, ycnt;
-
- [self drawBackground:(rects ? rects : &bounds)];
-
- for (xcnt = 0; xcnt < NUMTILESX; xcnt++) {
- for (ycnt = 0; ycnt < NUMTILESY; ycnt++) {
- if (tiles[xcnt][ycnt] != NOTILE) {
- NXPoint tileLoc = {floor(leftMargin + (tileSize.width + INTERTILE) * xcnt), floor((tileSize.height + INTERTILE) * ycnt)};
- [tile[tiles[xcnt][ycnt]] composite:NX_SOVER toPoint:&tileLoc];
- }
- }
- }
-
- if (lives) {
- [self showBall];
- [self showPaddle];
- }
-
- return self;
- }
-
- // incrementGameScore: adds the value of the argument to the score if the game
- // is not in demo mode.
-
- - incrementGameScore:(int)scoreIncrement
- {
- if (demoMode == NO) {
- score += scoreIncrement;
- }
- return self;
- }
-
- // hitTileAt:: checks to see if there's a tile at tile location x, y;
- // if so, it is considered hit by the ball and cleared. hitTileTile:: also
- // updates the score and the ball velocity. hitTileAt:: returns YES if there
- // was a tile, NO otherwise.
-
- -(BOOL) hitTileAt:(int)x :(int)y
- {
- NXRect rect = {{floor(leftMargin + (tileSize.width + INTERTILE) * x),
- floor((tileSize.height + INTERTILE) * y)},
- {tileSize.width, tileSize.height}};
-
- if (x < NUMTILESX && y < NUMTILESY && x >= 0 && y >= 0 &&
- (tiles[x][y] != NOTILE)) {
- [self incrementGameScore:tileScores[tiles[x][y]]];
- ballYVel = restrictValue(ballYVel * tileAccs[tiles[x][y]], MAXYV);
- [self drawBackground:&rect];
- tiles[x][y] = NOTILE;
- numTilesLeft--;
- return YES;
- } else {
- return NO;
- }
- }
-
-
- // The paddleHit method is called whenever the ball hits the paddle.
- // This method bounces the ball back at an angle depending on what part of
- // the paddle was hit.
-
- - paddleHit
- {
- float whereHit = ((ballX + RADIUS) - paddleX) / paddleSize.width;
-
- ballYVel = -ballYVel;
- ballY = paddleSize.height;
-
- [self playSound:paddleSound atXLoc:paddleX];
-
- // Alter the x-velocity and make sure it is in the valid range.
- // If the ball hits the edges of the paddle, bounce it back at some angle.
-
- if (whereHit < 0.1) {
- ballXVel = - MAXXV;
- } else if (whereHit > 0.9) {
- ballXVel = MAXXV;
- } else {
- // Now whereHit is in the range 0.1 .. 0.9, with 0.5 indicating middle
- // of the paddle. Convert to a number in the range 0.2 to 1, with 0.2
- // indicating the middle and 1 either end.
- whereHit = (fabs(whereHit - 0.5) + 0.1) * 2.0;
- ballXVel = ((ballXVel > 0.0) ? 1.0 : -1.0) * MAXXV * whereHit;
- }
-
- return self;
- }
-
- // If upon launch we discover that there's no sound, then we fail
- // silently. Note that although the SoundEffect class has the ability
- // to enable/disable sounds, because we might have multiple
- // BreakViews each with its own sound state, we keep a local state in
- // addition to the one in SoundEffect.
-
- // Note that although this is an outlet method, we do not actually
- // have an outlet (instance variable) named soundStateFrom; we just
- // want to take a look at the initial value of the button and see
- // if sound needs to be turned on...
-
- - setSoundStateFrom:sender
- {
- if ([sender state]) {
- [SoundEffect setSoundEnabled:YES];
- if (!(soundEnabled = [SoundEffect soundEnabled])) {
- [sender setState:NO]; // Silently fail
- }
- } else {
- soundEnabled = NO;
- }
- return self;
- }
-
- // If user tries to enable sound once the game is launched, and it
- // fails, then we do tell him/her about it.
-
- - setSoundMode:sender
- {
- BOOL desiredState = [sender state];
- [self setSoundStateFrom:sender];
- if (desiredState && !soundEnabled) {
- NXRunAlertPanel (
- NXLocalString ("No Sound", NULL, "Title of alert indicating sounds aren't available"),
- NXLocalString ("Can't play sounds.", NULL, "Contents of alert panel"),
- NXLocalString ("Bummer", NULL, "Acceptance that sounds can't be played"),
- NULL, NULL);
- }
- return self;
- }
-
- - (void)playSound:sound atXLoc:(float)xLoc
- {
- if (soundEnabled) {
- [sound play:1.0 pan:restrictValue((xLoc / gameSize.width - 0.5) * 2.0, 1.0)];
- }
- }
-
- // Alters the given velocity vector so that it is
- // rotated by the indicated amount. We restrict both the resulting x and v
- // velocity values to the maximum of their max possible values...
-
- - rotate:(float *)xVel :(float *)yVel by:(float)radians
- {
- float newAngle = atan2 (*yVel, *xVel) + radians;
- float velocity = hypot (*xVel, *yVel);
-
- *yVel = restrictValue(velocity * sin(newAngle), MAX(MAXYV, MAXXV));
- *xVel = restrictValue(velocity * cos(newAngle), MAX(MAXYV, MAXXV));
-
- return self;
- }
-
- // The step method implements one step through the main game loop.
- // The distance traveled by the ball is adjusted by the time between frames.
-
- - step:(double)timeNow
- {
- NXPoint mouseLoc;
- float newX;
- unsigned int timeDelta = MIN(MAX((timeNow - lastFrameTime) * 1000, MINTIMEDIFFERENCE), MAXTIMEDIFFERENCE);
- lastFrameTime = timeNow;
-
- [self lockFocus];
-
- [self eraseBall];
-
- // If the ball is rotating, rotate it by the indicated amount.
-
- if (revolutionsLeft > 0.0) {
- float revsThisTime = revolutionSpeed * timeDelta;
- [self rotate:&ballXVel :&ballYVel by:revsThisTime];
- revolutionsLeft -= revsThisTime;
- if (revolutionsLeft <= 0.0 && (fabs(ballYVel) < MAXYV * 0.6)) {
- // Done rotating; make sure we have a good y-velocity
- ballYVel = MAXYV * 0.8 * (ballYVel < 0.0 ? -1 : 1);
- ballXVel = restrictValue(ballXVel,MAXXV);
- }
- } else if (ONEIN(1000 + (level < 8 ? (8 - level) * 250 : 0)) && (ballY > gameSize.height * 0.6)) {
- // If we're not rotating, we go into rotating mode one out of
- // 1500 or more steps, provided that the ball is not too close to
- // the paddle at the time.
- revolutionsLeft = M_PI * (2 + RANDINT(5)); // 1 to 3.5 full turns
- revolutionSpeed = MAXREVOLUTIONSPEED * (RANDINT(8) + 2.0) / 10.0;
- }
-
- // Update the ball location
-
- ballX += ballXVel * timeDelta * gameSize.width / GAMEWIDTH;
- ballY += ballYVel * timeDelta * gameSize.height / GAMEHEIGHT;
-
-
- if (gameRunning) {
-
- if (ballX < 0.0) { // Hit on the left wall
- ballX = 0.0;
- ballXVel = -ballXVel;
- [self playSound:wallSound atXLoc:ballX];
- } else if (ballX > gameSize.width - ballSize.width) { // Right wall
- ballX = gameSize.width - ballSize.width;
- ballXVel = -ballXVel;
- [self playSound:wallSound atXLoc:ballX];
- }
-
- if (ballY > gameSize.height - ballSize.height) { // Top wall
- ballY = gameSize.height - ballSize.height;
- ballYVel = -ballYVel;
- if (niceBall && !ONEIN(5) && !demoMode) {
- NXPoint mid = {paddleX + paddleSize.width / 2.0, paddleY};
- [self directBallAt:&mid];
- } else if (ONEIN(10)) {
- ballXVel = MAXXV-(RANDINT((int)(MAXXV*20))/10.0);
- }
- [self playSound:wallSound atXLoc:ballX];
- }
-
- // Now checking for collisions with tiles...
-
- {
- int y1 = (int)(floor(ballY /
- (tileSize.height + INTERTILE)));
- int x1 = (int)(floor((ballX - leftMargin) /
- (tileSize.width + INTERTILE)));
- int y2 = (int)(floor((ballY + ballSize.height) /
- (tileSize.height + INTERTILE)));
- int x2 = (int)(floor((ballX + ballSize.width - leftMargin) /
- (tileSize.width + INTERTILE)));
-
- if ([self hitTileAt:x1 :y1] | [self hitTileAt:x2 :y1] |
- [self hitTileAt:x1 :y2] | [self hitTileAt:x2 :y2]) {
- [self playSound:tileSound atXLoc:ballX];
- if (!killerBall) {
- ballYVel = -ballYVel;
- }
- [scoreView setIntValue:score];
- [[self window] flushWindow];
- }
- }
- }
-
- // Get the mouse location and convert from window to the view coords.
- // If in demo, mode, make the paddle track the ball. Endless fun.
-
- if (demoMode) {
- mouseLoc.x = ballX + ballSize.width / 2.0;
- } else {
- [[self window] getMouseLocation:&mouseLoc];
- [self convertPoint:&mouseLoc fromView:nil];
- }
-
- newX = MAX(MIN(mouseLoc.x - paddleSize.width/2,
- gameSize.width - paddleSize.width), 0);
-
- if (ballY >= paddleY + paddleSize.height) {
-
- // Ball is above the paddle; redraw it and the paddle and continue
- // We flush twice as the ball and the paddle are not too close
- // together
-
- [self showBall];
- [[self window] flushWindow];
- [self erasePaddle];
- paddleX = newX;
- [self showPaddle];
- [[self window] flushWindow];
-
- } else if (ballY + ballSize.height > 0) {
-
- // Ball is past the paddle but not totally gone...
-
- [self erasePaddle];
- paddleX = newX;
-
- // Check to see if the user managed to catch the ball after all
-
- if ((ballY > paddleY - ballSize.height / 2.0) &&
- (ballX <= paddleX + paddleSize.width) &&
- (ballX + ballSize.width > paddleX)) {
- [self paddleHit];
- }
-
- // The ball and the paddle are close, so one flushWindow is fine.
-
- [self showBall];
- [self showPaddle];
- [[self window] flushWindow];
-
- } else {
-
- // Too late; the ball is out of sight...
-
- [self erasePaddle];
- [self stop:self];
- [self playSound:missSound atXLoc:0.0];
-
- if (--lives == 0) {
- if (score > highScore) [self setHighScore:score];
- [statusView setStringValue:NXLocalString("Game Over", NULL, "Message indicating that the game is over...")];
- } else {
- [self resetBallAndPaddle];
- [self showBall];
- [self showPaddle];
- [statusView setStringValue:NXLocalString("Game Ready", NULL, "Message indicating the game is ready to run")];
- }
- [[self window] flushWindow];
-
- [livesView setIntValue:lives];
-
- }
-
- // numTilesLeft <= 0 indicates that we've blown away every tile. But,
- // to make the game more exciting, we start decrementing numTilesLeft,
- // by one everytime through this loop, until it reaches the value
- // STOPGAMEAT. This makes the ball move a bit more after all the tiles
- // are gone. But, if gameRunning is NO, then it means we probably just
- // missed the ball, in which case we should go ahead and jump to the
- // next level.
-
- if ((numTilesLeft <= 0) &&
- ((lives && !gameRunning) || (--numTilesLeft == STOPGAMEAT))) {
- [self incrementGameScore:LEVELBONUS];
- [self gotoNextLevel:self];
- }
-
- NXPing (); // Synchronize postscript for smoother animation
-
- [self unlockFocus];
-
- return self;
- }
-
- // Pretty much a dummy function to invoke the step method.
-
- void runOneStep (DPSTimedEntry timedEntry, double timeNow, void *data)
- {
- [(id)data step:timeNow];
- }
-
-
- @end
-