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.