This is TemplateView.m in view mode; [Download] [Up]
/* * Shane Artis (shanega@athena.mit.edu) * * The guts of this method are straight from BreakApp, courtesy of Ali Ozer * at NeXT. It should be useful to anyone writing a generic game, or * any type of animation loop which must be smooth and quick. * * TemplateView implements an interactive custom view that allows the user * to generate any single-image animation by placing it within the -step * method at the appropriate location. Multi-image animation is also * straightforward. * * Hooks are provided for sound, game logic (# of lives, high scores, etc.), * and a screen background picture which is loaded by default and may be set * from a menu or button. * */ #import "TemplateView.h" #import "SoundGenerator.h" #import <libc.h> #import <math.h> #import <dpsclient/wraps.h> // PSxxx functions #import <appkit/color.h> // For background colors of images #import <appkit/defaults.h> // For writing/reading high score #import <appkit/Application.h> // For NXApp #import <appkit/Button.h> #import <appkit/Control.h> #import <appkit/NXImage.h> #import <appkit/OpenPanel.h> #import <appkit/Panel.h> #import <appkit/Window.h> #import <musickit/TuningSystem.h> #import <musickit/pitches.h> // 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 any object to completely pass through // another object (or else we can miss collisions between objects). #define IMAGEHEIGHT imageSize.height #define MAXYV fabs(imageYVel) #define MAXTIMEDIFFERENCE (IMAGEHEIGHT * 0.8 / MAXYV) // Game logic definitions #define LIVES 5 // Number of lives per game #define STOPGAMEAT (-10) // Number of loops through the // game after all pieces die #define LEVELBONUS 50 // Bonus at the end of a level // Starting location definitions can go here... // Score value definitions of various things can go here... // Randomizers 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 // Screen size #define gameSize bounds.size // Now for some music stuff. We define a few notes. #define GENERICNOTE c4 // Choose some notes. #define NUMGAMENOTES 8 static MKKeyNum gameNotes[NUMGAMENOTES] = {c6k,d6k,b6k,g6k,a6k,f6k,d7k,b5k}; #define GAMENOTE (MKKeyNumToFreq (gameNotes[ \ ((level % 6) == 0) ? \ RANDINT(NUMGAMENOTES-1) : \ (level % NUMGAMENOTES) \ ])) // Restrict a value to the range -max .. max. It's here, so I just left it. inline float restrictValue(float val, float max) { if (val > max) return max; else if (val < -max) return -max; else return val; } @implementation TemplateView - initFrame:(const NXRect *)frm { [super initFrame:frm]; // Initialize our frame. [self allocateGState]; // For faster lock/unlockFocus // Generic technique for getting bitmap image from our executable or // directory. findImageNamed will check everywhere for us. Just put // the image.(tiff/eps) file into the interface builder project and it will // be incorporated into the executable automagically. [(image = [NXImage findImageNamed:"Object.tiff"]) setScalable:NO]; // Generic technique for getting a default value and setting a background [self setBackgroundFile:NXGetDefaultValue([NXApp appName], "BackGround") andRemember:NO]; // Generic method to get high score from a high score list in user defaults [self getHighScore]; // Provision for an alternate or demo mode in the game logic. demoMode = NO; // Display initial values [levelView setIntValue:level]; [scoreView setIntValue:score]; [livesView setIntValue:lives]; [hscoreView setIntValue:highScore]; [statusView setStringValue:"Game Ready"]; // Some default values for animation globals imageXVel = 0.1; imageYVel = 0.1; // moving right and down [image getSize:&imageSize]; imageX = bounds.origin.x + imageSize.width; imageY = bounds.size.height - 1.5 * imageSize.height; // upper left imageYAccel = -.015; // acclerating downwards INITRAND; return self; } // free simply gets rid of everything we created for TemplateView, including // the instance of BreakView itself. This is how nice objects clean up. - free { if (gameRunning) { DPSRemoveTimedEntry (timer); } // Send free to any objects we have created [image free]; [backGround free]; return [super free]; } // The following allows TemplateView to grab the mousedown event that activates // the window. By default, the View's acceptsFirstMouse returns NO. - (BOOL)acceptsFirstMouse { return YES; } // Allows us to grab keyboard events - (BOOL)acceptsFirstResponder { return YES; } // This method 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]; if (remember) { NXWriteDefault ([NXApp appName], "BackGround", fileName); } } else { [backGround useFromSection:"BackGround.tiff"]; if (remember) { NXRemoveDefault ([NXApp appName], "BackGround"); } } [backGround setBackgroundColor:NX_COLORWHITE]; [backGround setScalable:YES]; [self display]; return self; } // The following two methods allow changing the background image from // menu items or buttons. - changeBackground:sender { const char *const types[] = {"tiff", "eps", NULL}; if ([[OpenPanel new] runModalForTypes:types]) { [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\0", highScore); NXWriteDefault ([NXApp appName], "HighScore", str); return self; } - (int)score { return score; } - (int)level { return level; } - (int)lives { return lives; } // 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; } // 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 - gotoNextLevel:sender { // We are at the next level... Stop the game and increment the level. [self stop:sender]; level++; // setup and draw your new level here. [levelView setIntValue:level]; [scoreView setIntValue:score]; [livesView setIntValue:lives]; [hscoreView setIntValue:highScore]; [statusView setStringValue:"Game Ready"]; [self display]; // Display the new arrangement [self go:sender]; // start rolling return (self); } // setDemoMode allows the user to put the game in a demo mode. - setDemoMode:sender { if (demoMode = ([sender state] == 0 ? NO : YES)) { [self go:sender]; } else { [self stop:sender]; } return (self); } - go:sender { void runOneStep (); if (lives && !gameRunning) { // Write initialization code here gameRunning = YES; timer = DPSAddTimedEntry(0.0, &runOneStep, self, NX_BASETHRESHOLD); [statusView setStringValue:"Running"]; // This lets us capture keyDowns without a mouse down in the window. [[self window] makeFirstResponder:self]; } return self; } - stop:sender { if (gameRunning) { gameRunning = NO; DPSRemoveTimedEntry (timer); [statusView setStringValue:"Paused"]; [soundGenerator shutUp]; } return self; } // It may be that passing around images and bitmaps is slow, therefore // These methods use global variables instead of taking args. // This uses more space, but may take less time. If the number of images // used is few you can write a showImage method for each, but this one // has the image passed in. - showImage:(id)anImage { NXRect tmpRect = {{floor(imageX), floor(imageY)}, imageSize}; [anImage composite:NX_SOVER toPoint:&tmpRect.origin]; return self; } - eraseImage { NXRect tmpRect = {{imageX, imageY}, imageSize}; 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_COPY 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 { [self drawBackground:&bounds]; [self showImage:image]; 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; } - playNote:(double)note { [soundGenerator playNoteAtFreq:note]; return self; } - keyDown:(NXEvent *)myevent { NXEvent* eptr = NULL; NXEvent e; do { if (myevent->data.key.charSet == NX_ASCIISET && (myevent->flags&(NX_CONTROLMASK|NX_ALTERNATEMASK|NX_COMMANDMASK)) == 0) { // A 'pause' key implementation if (myevent->data.key.charCode == 'p') { if (gameRunning) { [self stop:self]; } else { [self go:self]; } } if (myevent->data.key.charCode == 'a') { imageYAccel *= 2; } if (myevent->data.key.charCode == 'd') { imageYAccel /= 2; } if (myevent->data.key.charCode == 'r') { imageXVel *= 2; } if (myevent->data.key.charCode == 'l') { imageXVel /= 2; } } else return [super keyDown:myevent]; } while (eptr = [NXApp peekNextEvent:NX_KEYDOWNMASK into:&e]); return self; } unsigned currentTimeInMs() { struct timeval curTime; gettimeofday (&curTime, NULL); return ((unsigned)curTime.tv_sec) * 1000 + curTime.tv_usec / 1000; } // The step method implements one step through the main game loop. Actually, // for efficiency purposes, right before returning, this method checks to see // if there are any pending events; if there are none, step will simply loop // instead of returning. This avoids the overhead of a lock/unlockFocus and // increases the frame rate. The distance between animation steps should be // adjusted by the time between frames, so we should get the game on // different processors. - step { NXEvent dummyEvent; NXPoint mouseLoc; float newX; unsigned int timeBefore = 0; NXRect aRect = {{imageX, imageY}, imageSize}; [self lockFocus]; do { unsigned int timeNow = currentTimeInMs(); unsigned int timeDelta = MIN(MAXTIMEDIFFERENCE, timeNow-timeBefore); timeBefore = timeNow; // Clear screen of animated objects [self eraseImage]; // Update image locations (Depends on time between frames // for consistent behaviour across platforms) imageX += imageXVel * timeDelta * gameSize.width / GAMEWIDTH; imageY += imageYVel * timeDelta * gameSize.height / GAMEHEIGHT; if (gameRunning) { // Test for collisions, do game logic // EXAMPLE: This is a trivial animation. It bounces // a cute picture around the background. // This is everything right here. All the rest // is just setup and frills. if (imageY < bounds.origin.y - 17.0 || imageY > bounds.size.height - imageSize.height) { imageY -= imageYVel * timeDelta * gameSize.height / GAMEHEIGHT; imageYVel *= -1.0; } if (imageX < bounds.origin.x - 14.0 || imageX > bounds.size.width - imageSize.height) { // we let the shadow go off screen so it's more convincing. imageX -= imageXVel * timeDelta * gameSize.width / GAMEWIDTH; imageXVel *= -1.0; } imageYVel += imageYAccel; } [self showImage:image]; // Get the mouse location and convert from window to the view coords. // Not necessarily needed so commented out, but a useful thing to have. // [[self window] getMouseLocation:&mouseLoc]; // [self convertPoint:&mouseLoc fromView:nil]; // It may be that a flushwindow should be done for each image drawn. // This could be slower but smoother - I'm not really sure. // Find out for yourself... // BTW: Make sure you make your animation FAST so us poor '030 // users can still play!! - SGA [[self window] flushWindow]; NXPing (); // Synchronize postscript for smoother animation } while (gameRunning && ([NXApp peekNextEvent:NX_ALLEVENTS into:&dummyEvent] == NULL)); [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]; } @end
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.