MoofWars/TTileGrid.cp

/*
    File:       TTileGrid.cp
 
    Contains:   A TileGrid is a class that describes a 2 dimensional array of tiles that can be 
                drawn as the background for a game.  This current version only supplies
                graphics routines, but collision and other logic should be fairly easy to
                generate.
 
    Written by: Timothy Carroll 
 
    Copyright:  Copyright © 1996-1999 by Apple Computer, Inc., All Rights Reserved.
 
                You may incorporate this Apple sample source code into your program(s) without
                restriction. This Apple sample source code has been provided "AS IS" and the
                responsibility for its operation is yours. You are not permitted to redistribute
                this Apple sample source code as "Apple sample source code" after having made
                changes. If you're going to re-distribute the source, we require that you make
                it clear in the source that the code was descended from Apple sample source
                code, but that you've made changes.
 
    Change History (most recent first):
                7/2/1999    Karl Groethe    Updated for Metrowerks Codewarror Pro 2.1
                
                4/2/97      Timothy Carroll fTileData is a handle, but the destructor 
                                            was calling DisposePtr on it. Ouch!  Bad 
                                            programmer, no donut.  Because my handle 
                                            was cast to a **CellGridType, I had to recast
                                            it back when I disposed of it, and I did it
                                            wrong.  Spotlight caught this one for me.
                                            
                1/23/97     Timothy Carroll Added include for Moofwars.h so that MrC 
                                            will compile
                8/15/96     Timothy Carroll Initial Release
                
 
*/
#include <Memory.h>
#include <Resources.h>
#include "Moofwars.h"
#include "TTileGrid.h"
#include "scaling.h"
 
 
 
 
/*************************************************************************************
TTileGrid::TTileGrid
~TTileGrid::TTileGrid
*************************************************************************************/
 
TTileGrid::TTileGrid (void)
{   
    fWidth = 0;
    fHeight = 0;
    fDefaultTile = 0;
    fTiles = NULL;
    fTileData = NULL;
    
}
 
TTileGrid::~TTileGrid (void)
{
    if (fTiles != NULL)
        DisposeTileCollection (fTiles);
    if (fTileData != NULL)
        DisposeHandle ((Handle) fTileData);
}
 
/*************************************************************************************
TTileGrid::CreateGridFromResource
*************************************************************************************/
OSErr
TTileGrid::CreateGridFromResource (SInt16 resID)
{
    OSErr               theErr = noErr;
    TileGridResHeader   **grid;
    Ptr                 srcPtr, destPtr;
    
    grid = (TileGridResHeader **) Get1Resource (TileGridResType, resID);
    theErr = ResError();
    FAIL_OSERR (theErr,"\pFailed to load GRID resource")
    FAIL_NIL (grid, "\pFailed to load GRID resource")
    
    if ((**grid).version != 0) SIGNAL_ERROR ("\pUnable to read this GRID format")
    
    fWidth = (**grid).width;
    fHeight = (**grid).height;
    fDefaultTile = (**grid).defaultTile;
    fTiles = NewTileCollection ((**grid).tileResID);
    FAIL_NIL (fTiles, "\pFailed to load the Tiles needed for GRID.")
    fTiles->LockCollection();
    
    fTileData = (CellGridType **) NewHandle (fWidth * fHeight * sizeof (CellGridType));
    theErr = MemError();
    FAIL_OSERR (theErr, "\pFailed to allocate memory for the GRID data")
    FAIL_NIL (fTileData, "\pFailed to allocate memory for the GRID data")
 
    destPtr = (Ptr) *fTileData;
    srcPtr = (Ptr) (*grid)+sizeof (TileGridResHeader);
    
    BlockMoveData (srcPtr, destPtr, fWidth * fHeight * sizeof (CellGridType));
    
    goto cleanup;
    
    error:
    
    if (theErr == noErr)
        theErr = paramErr;
    
    cleanup:
    
    if (grid != NULL)
        ReleaseResource ((Handle) grid);
    
    return theErr;
 
}
 
 
/*************************************************************************************
Accessor Functions
*************************************************************************************/
 
CellGridType
TTileGrid::GetGridValue (SInt32 h, SInt32 v)
{
    SInt32 offset;
    if ((h < 0) || (h >= fWidth) || (v < 0) || (v >= fHeight))
        return fDefaultTile;
    
    offset = (v*fHeight + h);
    return *((*fTileData)+offset);
}
 
void
TTileGrid::SetGridValue (SInt32 h, SInt32 v, CellGridType value)
{
    SInt32 offset;
    
    if ((h < 0) || (h >= fWidth) || (v < 0) || (v >= fHeight))
        return;
        
    offset = (v*fHeight + h);
    
    *((*fTileData)+offset) = value;
}
 
/*************************************************************************************
TTileGrid::DrawGrid
 
The heart of the current TTileGrid code.  This routine uses the values stored in the grid to draw
into a rectangle on the screen.
 
Inputs:
 
We input a rectangle (in screen coordinates) to draw to, and the top left corner (in world coordinates)
that corresponds to the top left corner of the rectangle.  This routine figures out which tiles need to be
drawn and draws them as quickly as possible.
 
 
Further optimizations:
In our worse case, we clip both vertically and horizontally.  A normal unclipped tile grid would be 20x15
for a 640x480 screen, or 300 tiles.  The fully clipped case would be 21x16 or 336 tiles.  Of these tiles,
70 of them will be drawn with the clipped algorithm or about 20%.  So speeding up the handling of clipped
tiles would improve the speed of grid drawing in most cases.
 
The best way to speed up clipping is probably to differentiate between vertical and horizontal clipping.
If we are just vertical clipping, we can use the same fast inner blit loops that we've been using for
the unclipped blitters -- we just draw less rows.  Horizontal clipping can use the fact that we always
have a set of data aligned to the same alignment as the destination.
 
As mentioned below, there is one switch that can be moved from once per tile to once per grid.  The main
reason this isn't done in the code below is that I haven't found a clean C++ way to do it that doesn't 
involve copying the same loop 8 times.  I figured that clarity was better in this case, because the
performance difference was negligible.
*************************************************************************************/
 
void
TTileGrid::DrawGrid (Rect *screenRect, SInt32 topGlobal, SInt32 leftGlobal)
{
    SInt32  tilesWidth, tilesHeight;        // number of tiles in each direction to draw.
    SInt32  indexH, indexV;                 // index coordinates of the tile currently being drawn
    SInt32  startH;                         // base index to start each line at
    SInt32  drawY, drawX;                   // screen coordinates of the tile currently being drawn
    SInt32  startX;                         // base x coordinate to start each line of tiles at.
    Boolean horizontalClip, verticalClip;   // whether or not we need to clip
    
    SInt32  hLoop, vLoop;                   // used to loop over the tiles being drawn
    
    unsigned char   *destPtr;               // Points to the destination inside the buffer
    unsigned char   *destRowPtr;            // Points to the beginning of the line in a buffer
    UInt32          vertOffset;             // Use this to increment destRowPtr to the next line of tiles
    UInt32          alignment;              // we can calculate the alignment for all the tiles, which
                                            // is their exact byte alignment.  This lets us call a custom
                                            // blitter for that alignment.  See the TTileCollection
                                            // blitters for more info.
    
    SInt32          temp;
 
 
// ASSERTIONS
// The screenRectangle should start on a multiple of 8 bytes, and should be an integral number of tiles
// tall and wide.  This helps us determine exactly which tiles to draw.
 
#if qDebugging
    if (screenRect->left % 8 != 0)
    {
        DebugStr ("\pscreenRect improperly aligned");
        return;
    }
    
    if ((screenRect->right - screenRect->left) % 32 != 0)
    {
        DebugStr ("\pscreenRect improper width");
        return;
    }   
    
    if ((screenRect->bottom - screenRect->top) % 32 != 0)
    {
        DebugStr ("\pscreenRect improper width");
        return;
    }   
#endif
 
// First, determine the number of tiles in the screen rectangle.
    
    tilesWidth = (screenRect->right - screenRect->left) >> 5;
    tilesHeight = (screenRect->bottom - screenRect->top) >> 5;
 
// We need to determine where we actually need to start drawing, both in screen coordinates and in
// indices into the Grid values.  We also determine if we actually need to perform any clipping.
// 
// If the top/left coordinate in world coordinates is a multiple of 32, then we are exactly aligned to
// the screen rectangle, and no clipping needs to take place.  This is actually the fastest case, because
// we don't call our slower clipping tiles, and we also call the fastest tile blitter.
// 
// If it isn't a multiple of 32, then we're not aligned and we need to draw an extra tile to get the
// full row.  Because we handle the clipped case separately, we actually decrement the number of tiles
// we'll be drawing.
 
    temp = leftGlobal % 32;
    startH = leftGlobal / 32;
    if (temp != 0)
    {
        tilesWidth--;
        horizontalClip = true;
        
        if (leftGlobal < 0)
        {
            startH = (leftGlobal-31) / 32;
            if (temp < 0) temp+=32;
        }
    }
    else
        horizontalClip = false;
 
// However much our start point is offset to the right, we need to offset that far to the left to
// determine our initial drawing point.
    startX = screenRect->left - temp;
    
 
// We do the same for the vertical components.
 
    temp = topGlobal % 32;
    indexV = topGlobal / 32;
    if (temp != 0)
    {
        tilesHeight--;
        verticalClip = true;
        
        if (topGlobal < 0)
        {
            indexV = (topGlobal-31) /32;
            if (temp < 0) temp+= 32;
        }
    }
    else
        verticalClip = false;
    
    drawY =  screenRect->top - temp;
        
    // Calculate our initial destination pointer and our offset between rows.  We also
    // calculate the alignment for the entire grid.
 
    destRowPtr = gDestBaseAddr + drawY * gRowBytes + startX;
    vertOffset = gRowBytes << 5;
    alignment = ((long) destRowPtr) & 0x07;
    
    // handle top clip
    if (verticalClip)
    {
        drawX = startX;
        indexH = startH;
        
        if (horizontalClip)
            temp = tilesWidth+2;
        else
            temp = tilesWidth;
            
        for (hLoop = 0; hLoop < temp; hLoop++)
        {
            UInt32 index;
            
            index = GetGridValue (indexH, indexV);
            fTiles->CopyImageClipped (index, drawY, drawX);
            indexH++;
            drawX +=32;
        }
        
        indexV++;
        drawY += 32;
        destRowPtr += vertOffset;
    }
 
 
    for (vLoop = 0; vLoop < tilesHeight; vLoop++)
    {
        
        drawX = startX;
        indexH = startH;
        destPtr = destRowPtr;
    
        if (horizontalClip)
        {
            UInt32 index;
            
            index = GetGridValue (indexH, indexV);
            fTiles->CopyImageClipped (index, drawY, drawX);
            indexH++;
            drawX +=32;
            destPtr += 32;
        }
        
        for (hLoop = 0; hLoop < tilesWidth; hLoop++)
        {
            UInt32 index;
            
            index = GetGridValue (indexH, indexV);
            
            // *********** NOTE: *************
            // Alignment is actually the same for all tiles in the grid, which means we could
            // optimize this switch outside all the loops.  Only problem with this is that we
            // end up copying a lot of loop code for each alignment.  I had this in the code and
            // took it out for this sample because the function had grown huge, but it is something
            // that can be optimized further.  In a C implementation, you could probably get a function
            // pointer to the appropriate blitter, once, outside the loop and call from inside.
            switch (alignment)
            {
                case 0: fTiles->CopyImageUnclipped0 (index, destPtr); break;
                case 1: fTiles->CopyImageUnclipped1 (index, destPtr); break;
                case 2: fTiles->CopyImageUnclipped2 (index, destPtr); break;
                case 3: fTiles->CopyImageUnclipped3 (index, destPtr); break;
                case 4: fTiles->CopyImageUnclipped4 (index, destPtr); break;
                case 5: fTiles->CopyImageUnclipped5 (index, destPtr); break;
                case 6: fTiles->CopyImageUnclipped6 (index, destPtr); break;
                case 7: fTiles->CopyImageUnclipped7 (index, destPtr); break;
            }
            indexH++;
            drawX +=32;
            destPtr += 32;
        }
        
        if (horizontalClip)
        {
            UInt32 index;
            
            index = GetGridValue (indexH, indexV);
            fTiles->CopyImageClipped (index, drawY, drawX);
        }
        
        indexV++;
        drawY += 32;
        destRowPtr += vertOffset;
    }
    
    // handle bottom clip
    if (verticalClip)
    {
        SInt32 temp;
        
        drawX = startX;
        indexH = startH;
        
        if (horizontalClip)
            temp = tilesWidth+2;
        else
            temp = tilesWidth;
            
        for (hLoop = 0; hLoop < temp; hLoop++)
        {
            UInt32 index;
            
            index = GetGridValue (indexH, indexV);
            fTiles->CopyImageClipped (index, drawY, drawX);
            indexH++;
            drawX +=32;
        }
    }
}