Retired Document
Important: This sample code may not represent best practices for current development. The project may use deprecated symbols and illustrate technologies and techniques that are no longer recommended.
GameSource/Sprite.c
#include "CoreAssertion.h" |
#include "ZAMProtos.h" |
/* |
What is a sprite? |
In the context of this code a sprite is a small graphic object on the screen that has |
multiple frames, kind of like a movie. These sprites are loaded from a series of Cicn resources |
which are handy because they have a mask built right into the structure, so it is easier to |
associate the data together. |
So, a sprite is like a little movie, you give it a frame rate and a movement rate, |
and this sprite manager will move it automatically for you, depending on flag settings. |
With the flags you can turn off default behavior. It is also flexible in that it allows you |
to install callback routines for each animation frame drawn, and each time a sprite is moved. |
Also, each sprite has a colission handler, and when you request that colissions be checked for |
your sprite colission handler will be called when it overlaps another sprite. |
This is all done with built in features of the Macintosh and QuickDraw. Thanks Bill and Kon. |
Some of this code is hairy and messy. If I were starting this over again today, knowing what |
I do now having done it once, I would do it differently. Hopefully, since I am making this |
code available to you, you will see what I mean and be able to implement your own animation |
architecture that fits your needs and works for you. |
CoMationª Architecture by Brigham Stevens |
Be sure to put CoMation on the box of your game or MultiMedia application if you |
are sychronizing interactive animation between multiple computers. Thats right. |
You can say CoMation Architecture Support right on your box, and the customers will |
clamor for more. |
Hey for more excellent examples of Macintosh animation, see SpriteWorld by Tony Myles. I |
think it is on the developer CD, perhaps in the same area as this one. |
*/ |
/* this is where all the sprites are kept */ |
/* applications should keep their own references to the sprites */ |
/* see TankSprites, MissileSprites, and ExplosionSprites for examples of using */ |
/* most of the routines here */ |
static spriteLayerPtr MasterSpriteHead; |
static spriteLayerPtr MasterSpriteTail; |
void AnimateSprites(void) |
/* |
This function is what you call once each time through your main loop. |
It will erase and draw all the sprites that are flagged for update. |
This handles the layering by doing all the necessary erasing first. |
A sprite is erased by CopyBitsing the background in over its previous location. |
Then the sprites are drawn in layer order. NOTE that this does NOT happen unless |
at least one sprite was erased. This may be a flaw, but it saves time. |
The sprites are drawn using a mask region, which has to be moved to the current position |
each time. That is what MoveCellMaskRgnToRect. A Cell is an animation cell, or frame. |
Each frame in a sprite frame set has its own mask region. |
There are a lot of flags used by this, so be careful. Sometimes the flags gave me grief, but |
I wanted to make it flexible. Also, lots of the flags were added in for the AppleEvent |
support. |
Remember, if I was going to do this again, it would be done a lot differently. But, this |
works pretty well as it is. |
*/ |
{ |
register spritePtr spr; |
register spriteLayerPtr sprLayer; |
register frameCellPtr curFrame; |
register PixMapHandle srcPix; |
register PixMapHandle destPix; |
register Boolean needToDraw = false; |
/* Erase all sprites that need to be erased */ |
for(sprLayer = MasterSpriteTail; sprLayer != nil; sprLayer = sprLayer->prev) { |
if( (sprLayer->layerFlags & kLayerDirty) != 0) { |
needToDraw = true; |
for(spr = sprLayer->sprites; spr != nil; spr = spr->next) { |
if( (spr->spriteFlags & kNeedsToBeErased) != 0) { |
curFrame = spr->frameList->finfo.curImage; |
srcPix = PreserveGraf(sprLayer->backdrop); |
destPix = PreserveGraf(sprLayer->tween); |
// ERASE from backdrop to the tween layer |
CopyBits(*srcPix, |
*destPix, |
&spr->prevBounds, |
&spr->prevBounds, |
srcCopy, |
nil); |
RestoreGraf(); |
RestoreGraf(); |
spr->spriteFlags &= ~kNeedsToBeErased; |
} |
} |
} |
} |
/* draw all sprites in layer order */ |
if( needToDraw ) { |
for(sprLayer = MasterSpriteTail; sprLayer != nil; sprLayer = sprLayer->prev) { |
for(spr = sprLayer->sprites; spr != nil; spr = spr->next) { |
if(spr->visible) { |
curFrame = spr->frameList->finfo.curImage; |
srcPix = PreserveGraf(curFrame->image); |
destPix = PreserveGraf(sprLayer->tween); |
MoveCellMaskRgnToRect(curFrame, &spr->bounds); |
// DRAW the sprite into the offscreen tween layer |
CopyBits(*srcPix, |
*destPix, |
&curFrame->image->portRect, |
&spr->bounds, |
srcCopy, |
curFrame->mask); |
RestoreGraf(); |
RestoreGraf(); |
spr->prevBounds = spr->bounds; |
} |
} |
} |
} |
/* draw all sprites that need to be updated on the screen */ |
if( needToDraw ) { |
for(sprLayer = MasterSpriteTail; sprLayer != nil; sprLayer = sprLayer->prev) { |
if( (sprLayer->layerFlags & kLayerDirty) != 0) { |
for(spr = sprLayer->sprites; spr != nil; spr = spr->next) { |
if(spr->spriteFlags & kNeedsToBeDrawn) { |
srcPix = PreserveGraf(sprLayer->tween); |
destPix = PreserveGraf((GWorldPtr)sprLayer->window); |
// DRAW the sprite to the window on screen |
CopyBits(*srcPix, |
*destPix, |
&spr->updateBounds, |
&spr->updateBounds, |
srcCopy, |
nil); |
RestoreGraf(); |
RestoreGraf(); |
spr->spriteFlags &= ~kNeedsToBeDrawn; |
} |
} |
sprLayer->layerFlags &= ~kLayerDirty; |
} |
} |
} |
} |
void SpriteUpdateEvent(void) |
/* |
Call this from your event loop when a window containing sprites |
has an update event. This just refreshes the screen from the |
offscreen sprite world, which should be current. |
*/ |
{ |
PixMapHandle srcPix; |
PixMapHandle destPix; |
if(MasterSpriteHead) { |
srcPix = PreserveGraf(MasterSpriteHead->tween); |
destPix = PreserveGraf((GWorldPtr)MasterSpriteHead->window); |
CopyBits(*srcPix, |
*destPix, |
&MasterSpriteHead->tween->portRect, |
&MasterSpriteHead->tween->portRect, |
srcCopy, |
nil); |
RestoreGraf(); |
RestoreGraf(); |
} |
} |
void InitSprites(void) |
/* |
Call this routine very early in the program. |
It pre-allocates memory for all the sprite records as non-relocatable blocks. |
As you can see, it does not pre-allocate anything, which may slow things down |
if you are dynamically allocating memory at runtime. I changed this to simplify the |
use of Sprites, but my program pre-allocates all the sprites anyway. |
*/ |
{ |
MasterSpriteHead = nil; |
MasterSpriteTail = nil; |
} |
OSErr CreateSpriteLayer(spriteLayerPtr *retSprite, |
GWorldPtr tween, |
GWorldPtr backdrop, |
WindowPtr spriteWin) |
/* |
Sprites live in layers that define which sprites overlap each other. |
Layers also make it easy to group sprites for colission detection. |
Thanks Tony for the layering concept. My first cut through this |
was not using layers, just one big gantic sprite list. Yep, gantic. |
*/ |
{ |
OSErr err = noErr; |
spriteLayerPtr sl; |
sl = (spriteLayerPtr)NewPtrClear(sizeof(spriteLayer)); |
if(!sl) { |
err = MemError(); |
ErrMsgCode("\pCreateSpriteLayer NewPtrClear failed.",err); |
} |
if(err == noErr) { |
sl->tween = tween; |
sl->backdrop = backdrop; |
sl->window = spriteWin; |
} |
/* add this layer to the layer list */ |
/* layers are created in front to back order */ |
if(MasterSpriteTail) { |
sl->prev = MasterSpriteTail; |
MasterSpriteTail->next = sl; |
MasterSpriteTail = sl; |
} else { |
MasterSpriteHead = MasterSpriteTail = sl; |
} |
*retSprite = sl; |
return err; |
} |
void StopSpriteAction(spritePtr spr) |
/* |
This removes the sprites from the time manager queue |
if they are currently active. |
The Time Manager seems like it removes tasks when they complete |
because it has been changed to only insert them when they are primed, |
so, this will only remove the task if the bit is set indicating that it is |
active. PrimeTime sets this bit. |
*/ |
{ |
if( (spr->frameTask.timer.qType & TaskActiveFlag) != 0) { |
(void)RmvTime(&spr->frameTask.timer); |
} |
if( (spr->moveTask.timer.qType & TaskActiveFlag) != 0) { |
(void)RmvTime(&spr->moveTask.timer); |
} |
} |
void StopSpriteLayerAction(spriteLayerPtr sprLayer) |
/* |
Freeze an entire sprite layer. |
This does not deallocate the sprite at all. |
Only stops it from moving around so much. |
*/ |
{ |
spritePtr killSprite; |
for(killSprite = sprLayer->sprites; killSprite != nil; killSprite = killSprite->prev) |
StopSpriteAction(killSprite); |
} |
void KillSprites(void) |
/* |
Perhaps a misleading name, |
because this does not deallocate either. This just stops all Layers at once. |
I guess I never wrote a sprite deallocator, because it is complicated because |
frame sets are shared. I should add a user count to the frame set so that they |
know when no one else is using it, then the deallocator could be written. |
However, since ZAM only loads sprites once, I don't need one, so I'm not writing it. |
*/ |
{ |
spriteLayerPtr killLayer; |
for(killLayer = MasterSpriteTail; killLayer != nil; killLayer = killLayer->prev) |
StopSpriteLayerAction(killLayer); |
} |
void AddSpriteToLayer(spritePtr spr, spriteLayerPtr sprLayer) |
/* |
Yes, this will take the sprite and make it a member of the layer. |
*/ |
{ |
spr->next = sprLayer->sprites; |
sprLayer->sprites->prev = spr->next; |
sprLayer->sprites = spr; |
} |
void RemoveSpriteFromLayer(spritePtr spr, spriteLayerPtr sprLayer) |
{ |
if(spr->next) { |
spr->next->prev = spr->prev; |
} |
if(spr->prev) { |
spr->prev->next = spr->next; |
} |
spr->prev = nil; |
spr->next = nil; |
} |
void MoveCellMaskRgnToRect(frameCellPtr curFrame, Rect *r) |
/* |
Offset a region to move it with the sprite. |
The maskLoc is the original topLeft of the region, and |
it is needed to preserve the region position within the sprite rectangle |
*/ |
{ |
Point rgnOffset; |
/* translate the region position to the new image position */ |
rgnOffset.h = r->left - (**curFrame->mask).rgnBBox.left |
+ curFrame->maskLoc.h; |
rgnOffset.v = r->top - (**curFrame->mask).rgnBBox.top |
+ curFrame->maskLoc.v; |
OffsetRgn(curFrame->mask, rgnOffset.h, rgnOffset.v); |
} |
OSErr CreateEmptySprite(spriteLayerPtr sprLayer, |
spritePtr *newSprite, /* new sprite returned here */ |
long spriteFlags, |
long moveTimeInterval, |
long frameTimeInterval, |
long refCon) /* application value */ |
/* |
Create a new sprite from parameters specified with no frame set. |
To add a frame set, either use CreateEmptyFrameSet, and then SetSpriteFrameSet |
to copy a frame set to it. |
Some things about this are bad. FrameSet headers are block moved around |
when the frame sets are shared. This is not good, but it is not that much memory. |
See SpriteFrameSet.c for more info on frame sets. |
*/ |
{ |
spritePtr tSprite; |
OSErr err = noErr; |
tSprite = (spritePtr)NewPtrClear(sizeof(sprite)); |
if(tSprite == nil) { |
err = paramErr; |
ErrMsg("\pNo More Sprites may be allocated. ¥¥¥¥EXITING¥¥¥¥."); |
} else { |
tSprite->usrNext = nil; |
tSprite->usrPrev = nil; |
tSprite->moveHandler = nil; |
tSprite->visible = false; |
tSprite->loc.h = 0; |
tSprite->loc.v = 0; |
tSprite->vel.h = 0; |
tSprite->vel.v = 0; |
tSprite->frameList = nil; |
tSprite->refCon = refCon; |
tSprite->spriteFlags = spriteFlags; |
tSprite->inUse = false; |
tSprite->moveTimeInterval = moveTimeInterval; |
tSprite->frameTimeInterval = frameTimeInterval; |
tSprite->ownerLayer = sprLayer; |
*newSprite = tSprite; |
AddSpriteToLayer(tSprite, sprLayer); |
} |
return err; |
} |
OSErr CreateColorIconSprite(spriteLayerPtr sprLayer, |
spritePtr *newSprite, /* new sprite returned here */ |
short startID, /* starting resource ID of cicn */ |
short numFrames, /* number of resources to load */ |
long spriteFlags, |
long moveTimeInterval, |
long frameTimeInterval, |
long refCon) /* application value */ |
/* |
Create a new sprite from parameters specified |
starting location for all sprites is 0,0 |
velocity is 0,0 |
bounds taken from icon dimensions - GWORLD of first frame |
assumes that all icons have the same dimension |
center - calcd from bounds |
dimension - h width v = height calcd from bounds |
frameList - built from color icons starting from startID |
visible - set to false |
Sprites created with this can then be copied. See MissileSprites.c (LoadMIssileSprites) |
for an example, or ExplosionSprites.c. |
*/ |
{ |
spritePtr tSprite; |
OSErr err; |
/* create an empty sprite first */ |
err = CreateEmptySprite(sprLayer, |
&tSprite, /* new sprite returned here */ |
spriteFlags, |
moveTimeInterval, |
frameTimeInterval, |
refCon); /* application value */ |
/* now create the frameset list, and attach it to the sprite */ |
if(err == noErr) { |
err = CreateColorIconFrameSet(&tSprite->frameList, startID, numFrames); |
if(err != noErr) { |
ErrMsgCode("\pError in CreateColorIconFrameSet!",err); |
} else { |
*newSprite = tSprite; |
} |
} |
return err; |
} |
void SetSpriteLoc(spritePtr spr, Fixed h, Fixed v) |
/* |
Change the position of the sprite on the screen |
*/ |
{ |
Boolean showIt = false; |
if(spr->visible) { |
showIt = true; |
HideSprite(spr); |
} |
spr->prevBounds = spr->bounds; |
spr->loc.h = h; |
spr->loc.v = v; |
spr->bounds.top = FixToInt(spr->loc.v) - spr->frameList->finfo.center.v; |
spr->bounds.left = FixToInt(spr->loc.h) - spr->frameList->finfo.center.v; |
spr->bounds.bottom = spr->bounds.top + spr->frameList->finfo.dimension.v; |
spr->bounds.right = spr->bounds.left + spr->frameList->finfo.dimension.h; |
MyUnionRect(&spr->prevBounds, &spr->bounds, &spr->updateBounds); |
spr->spriteFlags |= kNeedsToBeErased | kNeedsToBeDrawn; |
if(showIt) |
ShowSprite(spr); |
} |
void ShowSprite(spritePtr spr) |
/* |
Make the sprite visibile. |
*/ |
{ |
if(!spr->visible) { |
spr->visible = true; |
spr->spriteFlags |= kNeedsToBeDrawn; |
} |
} |
void HideSprite(spritePtr spr) |
/* |
hide the sprite on the screen |
and draw it. |
In this case if the sprite was not ever |
previously drawn, the drawing routine will |
not draw anything. |
*/ |
{ |
if(spr->visible) { |
spr->visible = false; |
spr->spriteFlags |= kNeedsToBeErased; |
} |
} |
void StartSpriteAction(spritePtr spr) |
/* |
This launches the XThing tasks for updating the sprites, which in turn |
uses the Time Manger. XThings are periodical tasks that run as close |
if any of the intervals are zero, then the task is not launched |
*/ |
{ |
if(spr->frameTimeInterval) { |
(void)StartXThing(&spr->frameTask, spr->frameTimeInterval, |
(updateProc)SpriteFrameTask, (long)spr); |
} |
if(spr->moveTimeInterval) { |
(void)StartXThing(&spr->moveTask, spr->moveTimeInterval, |
(updateProc)SpriteMoveTask, (long)spr); |
} |
} |
void StartRemoteSpriteAction(spritePtr spr) |
/* |
Same as above, only the timer is never fired. |
Instead, they are manually updated when a network message is |
received saying to update the sprite. |
*/ |
{ |
(void)AddXThing(&spr->frameTask, spr->frameTimeInterval, |
(updateProc)SpriteFrameTask, (long)spr); |
(void)AddXThing(&spr->moveTask, spr->moveTimeInterval, |
(updateProc)SpriteMoveTask, (long)spr); |
} |
Boolean SpriteFrameTask(xthing *xtp, spritePtr spr) |
/* |
This is the XThing task that changes the frame of the sprite. |
If the kFrameTaskBeforeUpdate bit it set in spriteFlags and if there |
is a global frame task installed, then it will be called before |
the frame of the sprite is changed. |
If the kDefaultFrameAdvance bit is set, then the task |
will go ahead and advance the frame on to the next one. |
if the kFrameTaskAfterUpdate bit is set, then the task will |
also call the global frame task after the frame is advanced. |
Finally, if the current frame has a callback set up, then it will be called. |
All of the callbacks return a boolean that determines if this task is to be rescheduled. |
*/ |
{ |
register frameCellPtr curFrame; |
register frameSetPtr frameset; |
register long frameDelay; |
register Boolean reTimeTask = true; |
frameset = spr->frameList; |
if( (spr->spriteFlags & kFrameTaskBeforeUpdate) != 0) { |
if(spr->frameHandler) { |
reTimeTask = (*spr->frameHandler)(spr, nil); |
} |
} |
if( (spr->spriteFlags & kDefaultFrameAdvance) != 0) { |
frameset->finfo.frameIndex++; |
if(frameset->finfo.frameIndex >= frameset->finfo.frameCount) { |
frameset->finfo.frameIndex = 0; |
} |
curFrame = &frameset->flist[frameset->finfo.frameIndex]; |
frameset->finfo.prevImage = frameset->finfo.curImage; |
frameset->finfo.curImage = curFrame; |
spr->updateBounds = spr->bounds; |
spr->spriteFlags |= kNeedsToBeDrawn | kNeedsToBeErased; |
spr->ownerLayer->layerFlags |= kLayerDirty; |
} |
if( (spr->spriteFlags & kFrameTaskAfterUpdate) != 0) { |
if(spr->frameHandler) { |
reTimeTask = (*spr->frameHandler)(spr, (struct frameCell *)-1); |
} |
} |
/* check if this frame has a callback and call it */ |
curFrame = &frameset->flist[frameset->finfo.frameIndex]; |
if(curFrame->frameCB) { |
reTimeTask = (*curFrame->frameCB)(spr, (struct frameCell *)curFrame); |
} |
if( (spr->spriteFlags & kRemoteSprite) != 0) |
reTimeTask = false; |
return reTimeTask; |
} |
Boolean SpriteMoveTask(xthing *xtp, spritePtr spr) |
/* |
This procedure moves the sprites in the default way, |
applying the velocity to the location, and then recalculating the |
bounds based on this position. |
This procedure changes fields that DrawSprite depends upon: |
updateBounds covers the total area of the screen that needs to be changed, |
incorporating the previous position and the current position. |
prevBounds is the position the sprite WAS in. This is used for erasing the |
previous image. |
prevImage is the previous frameCell the sprite was drawn with. This is used to |
erase the previous image. |
*/ |
{ |
moveProc moveJSR; |
Boolean result = true; |
short adjustPos; |
Boolean reAdjustNecessary; |
if( (spr->vel.h != 0) || (spr->vel.v != 0) ) { |
spr->prevBounds = spr->bounds; /* save previous location */ |
/* the sprite location is the center of the sprite */ |
if(spr->spriteFlags & kRemoteUpdate) { |
spr->loc = spr->remoteLoc; /* slam the sprite */ |
} else { |
spr->loc.h += spr->vel.h; /* offset the sprite */ |
spr->loc.v += spr->vel.v; |
} |
/* build the sprite integer rectangle location from the fixed center point */ |
spr->bounds.top = FixToInt(spr->loc.v) - spr->frameList->finfo.center.v; |
spr->bounds.left = FixToInt(spr->loc.h) - spr->frameList->finfo.center.v; |
spr->bounds.bottom = spr->bounds.top + spr->frameList->finfo.dimension.v; |
spr->bounds.right = spr->bounds.left + spr->frameList->finfo.dimension.h; |
/* calculate the entire area that needs updating */ |
MyUnionRect(&spr->bounds, &spr->prevBounds, &spr->updateBounds); |
spr->frameList->finfo.prevImage = spr->frameList->finfo.curImage; |
/* set the update flag if we have moved a whole pixel at least */ |
if(spr->prevBounds.top != spr->bounds.top) { |
spr->spriteFlags |= kNeedsToBeDrawn | kNeedsToBeErased; |
spr->ownerLayer->layerFlags |= kLayerDirty; |
} |
else if(spr->prevBounds.left != spr->bounds.left) { |
spr->spriteFlags |= kNeedsToBeDrawn | kNeedsToBeErased; |
spr->ownerLayer->layerFlags |= kLayerDirty; |
} |
/* this constrains the sprite to within the constrain rectangle */ |
if(spr->spriteFlags & kConstrainToRect) { |
reAdjustNecessary = false; |
if(spr->bounds.right > spr->constrainRect.right) { |
adjustPos = spr->bounds.right - spr->constrainRect.right; |
spr->loc.h -= ff(adjustPos); |
reAdjustNecessary = true; |
} else if(spr->bounds.left < spr->constrainRect.left) { |
adjustPos = spr->constrainRect.left - spr->bounds.left; |
spr->loc.h += ff(adjustPos); |
reAdjustNecessary = true; |
} |
if(spr->bounds.bottom > spr->constrainRect.bottom) { |
adjustPos = spr->bounds.bottom - spr->constrainRect.bottom; |
spr->loc.v -= ff(adjustPos); |
reAdjustNecessary = true; |
} else if(spr->bounds.top < spr->constrainRect.top) { |
adjustPos = spr->constrainRect.top - spr->bounds.top; |
spr->loc.v += ff(adjustPos); |
reAdjustNecessary = true; |
} |
if(reAdjustNecessary) { |
spr->bounds.top = FixToInt(spr->loc.v) - spr->frameList->finfo.center.v; |
spr->bounds.left = FixToInt(spr->loc.h) - spr->frameList->finfo.center.v; |
spr->bounds.bottom = spr->bounds.top + spr->frameList->finfo.dimension.v; |
spr->bounds.right = spr->bounds.left + spr->frameList->finfo.dimension.h; |
} |
} |
} |
/* check if there is a move callback */ |
if(spr->moveHandler) { |
moveJSR = (moveProc)spr->moveHandler; |
result = (*moveJSR)(spr); |
} |
/* remote sprites are not timed by the time manager. They are moved when an update is rcvd */ |
if( (spr->spriteFlags & kRemoteSprite) != 0) |
result = false; |
/* tell XThing if we want to be added back as time manager task */ |
return result; |
} |
Copyright © 2003 Apple Computer, Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2003-01-14