home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
NeXTSTEP 3.0
/
NeXTSTEP3.0.iso
/
NextDeveloper
/
Examples
/
AppKit
/
BreakApp
/
BreakView.m
< prev
next >
Wrap
Text File
|
1992-04-29
|
32KB
|
1,085 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.
*
* 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 @mplied,
* as to its fitness for any particular use.
*/
#import <appkit/appkit.h>
#import <libc.h>
#import <math.h>
#import <defaults/defaults.h> // For writing/reading high score
#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.
@ne 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.h@t = 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 @ld 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 o@he 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
n@ all = (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 = r@ictValue(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);
PSse@y (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
{
float hue = level / 7.7 - (int)(level / 7.7);
NXRect rect = {{0.0, 0.0}, gameSize};
NXSiz@xSize = {gameSize.width / NUMXBOXES, gameSize.height / NUMYBOXES};
int xCnt, yCnt;
for (yCnt = 0; yCnt < NUMYBOXES; yCnt++) {
for (xCnt = 0; xCnt < NUMXBOXES; xCnt++) {
NXColor color = NXConvertHSBToColor(hue, 0.8, 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 drawBackgrou@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 ba@t 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;
}
// Turns sound on/off... By telling IB that there's an outlet
// named "soundOn," this method also gets called at startup time and
// sets the default value of this parameter from the .nib file.
- setSoundOn:sender
{
soundEnabled = [sender state];
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 < 10 ? (10 - level) * 750 : 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] flushWin@;
}
}
}
// 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 lev@
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