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;
}