- Introduction to Sprite Sheets
- Using Zwoptex
- The SpriteSheet Class
- PackedSpriteSheet Class
- Summary
- Exercise
The SpriteSheet Class
Having looked at the basics of a sprite sheet, we can now look at our implementation of the SpriteSheet class. In Xcode, open the CH06_SLQTSOR project and look inside the Game Engine group. You will see a new group called Sprite Sheet, inside of which are the SpriteSheet classes header and implementation files.
Initialization
Inside the SpriteSheet.m file, you find the following class methods:
- spriteSheetForImageNamed:spriteSize:spacing:margin: imageFilter
- spriteSheetForImage:sheetKey:spriteSize:spacing:margin:
These methods are used to create new sprite sheets either from an image file or from an Image instance that has already been created. Notice that both of these are class methods. This means you don't need an instance of the SpriteSheet class to access them. Having also defined a static NSDictionary within the class, you can use these class methods to access the dictionary information that only has a single instance.
The idea is that a sprite sheet is cached when it is created. Whenever a new sprite sheet that either uses the same image file or key is requested, a reference to the sprite sheet already created is returned. This helps with performance when you have a large number of entities that share the same sprite sheet (for example, the Door class, which you will see soon).
These class methods still make use of the standard initializer methods; they just cache the sprite sheet returned by these methods for later use. Listing 6.1 shows the spriteSheetForImageNamed:spriteSize:spacing:margin:imageFilter: method.
Listing 6.1. The spriteSheetForImageNamed:spriteSize:spacing:margin:imageFilter: Method
static
NSMutableDictionary *cachedSpriteSheets =nil
; + (SpriteSheet
*)spriteSheetForImageNamed:(NSString
*)aImageName spriteSize:(CGSize
)aSpriteSize spacing:(NSUInteger
)aSpacing margin:(NSUInteger
)aMargin imageFilter:(GLenum
)aFilter {SpriteSheet
*cachedSpriteSheet;if
(!cachedSpriteSheets
)cachedSpriteSheets
= [[NSMutableDictionary
alloc
]init
];if
(cachedSpriteSheet = [cachedSpriteSheets
objectForKey
:aImageName])return
cachedSpriteSheet; cachedSpriteSheet = [[SpriteSheet
alloc
]initWithImageNamed
:aImageNamespriteSize
:aSpriteSizespacing
:aSpacingmargin
:aMarginimageFilter
:aFilter]; [cachedSpriteSheets
setObject
:cachedSpriteSheetforKey
:aImageName]; [cachedSpriteSheetrelease
];return
cachedSpriteSheet; }
The first line in Listing 6.1 defines a static NSMutableDictionary. This creates a single instance of NSMutableDictionary that the class methods use to cache the sprite sheets. This dictionary has been defined at the class level, which means that only a single copy of this dictionary will exist, regardless of how many SpriteSheet instances are created. This provides us with a single cache of the sprite sheets.
The rest of the class simply checks to see if an entry already exists in the dictionary for an image name passed in (using spriteSheetForImageNamed). If the other method passes in a ready-made image, the sheetKey provided is used.
If no match is found, a new sprite sheet is created and added to the dictionary. Otherwise, the matching entry from the dictionary is passed back to the caller.
The initializer used when an image name is provided is shown in Listing 6.2.
Listing 6.2. SpriteSheet initWithImageNamed:spriteSize:spacing:margin:imageFilter Method
- (id
)initWithImageNamed:(NSString
*)aImageFileName spriteSize:(CGSize
)aSpriteSize spacing:(NSUInteger
)aSpacing margin:(NSUInteger
)aMargin imageFilter:(GLenum
)aFilter {if
(self
= [super
init
]) {NSString
*fileName = [[aImageFileNamelastPathComponent
]stringByDeletingPathExtension
];self
.image
= [[Image
alloc
]initWithImageNamed
:filenamefilter
:aFilter];spriteSize
= aSpriteSize;spacing
= aSpacing;margin
=0
; [self
cacheSprites
]; }return self
; }
The start of the initializer method is standard, and we have seen it many times already. The first interesting action comes when we create an image instance of the image used as the sprite sheet.
We are using the Image class that we created in the last chapter, passing in the image name that has been provided along with the image filter.
Next, the sprite's size, spacing, and margin are defined. At this point, we branch off and call a private method, called cacheSprites, which caches the information for each sprite in this sprite sheet. Calculating this information only once is important to help performance. This information should never change during the lifetime of a sprite sheet, so there is no need to calculate each time we request a particular sprite.
We examine the cacheSprites method in a moment; first, there is another initializer method to look at, as shown in Listing 6.3.
Listing 6.3. SpriteSheet initWithImage:spriteSize:spacing:margin Method
- (id
)initWithImage:(Image
*)aImage spriteSize:(CGSize
)aSpriteSize spacing:(NSUInteger
)aSpacing margin:(NSUInteger
)aMargin{if
(self
= [super
init
]) {self
.image
= aImage;spriteSize
= aSpriteSize;spacing
= aSpacing;margin
= aMargin; [self
cacheSprites
]; }return self
; }
The previous initializer took the name of an image file and created the image as part of creating the sprite sheet. This second initializer takes an image that's already been created. Not only is it useful to create a sprite sheet using an image instance that already exists, but it is also the method that's used when we create a sprite sheet from an image held in a complex (or packed) sprite sheet.
The only difference in this initializer from the last is that we set the sprite sheet's image to reference the Image instance that has been passed in. This method still calls the cacheSprites method, and that's the next method we discuss.
The cacheSprites method (shown in Listing 6.4) is a private method, as we only use it internally in the SpriteSheet class.
Listing 6.4. SpriteSheet cacheSprites Method
- (void
)cacheSprites {horizSpriteCount
= ((image
.imageSize
.width
+spacing
) +margin
) / ((spriteSize
.width
+spacing
) +margin
);vertSpriteCount
= ((image
.imageSize
.height
+spacing
) +margin
) / ((spriteSize
.height
+spacing
) +margin
);cachedSprites
= [[NSMutableArray
alloc
]init
];CGPoint
textureOffset;for
(uint
row=0
; row <vertSpriteCount
; row++) {for
(uint
column=0
; column <horizSpriteCount
; column++) {CGPoint
texturePoint =CGPointMake
((column * (spriteSize
.width
+spacing
) +margin
), (row * (spriteSize
.height
+spacing
) +margin
)); textureOffset.x
=image
.textureOffset
.x
*image
.fullTextureSize
.width
+ texturePoint.x
; textureOffset.y
=image
.textureOffset
.y
*image
.fullTextureSize
.height
+ texturePoint.y
;CGRect
tileImageRect =CGRectMake
(textureOffset.x
, textureOffset.y
,spriteSize
.width
,spriteSize
.height
);Image
*tileImage = [[image
subImageInRect
:tileImageRect]retain
]; [cachedSprites
addObject
:tileImage]; [tileImagerelease
]; } } }
The first two calculations work out how many sprites there are in the sprite image, and a new NSMutableArray is created. This array holds Image instances created for each image in the sprite sheet. Again, creating the images at this stage and caching them improves performance. This is not an activity you want to be performing in the middle of game play.
With the array created, we then loop through each row and column, creating a new image for each sprite. We use the information we have about the sprite sheet, such as size, spacing, and margin, to calculate where within the sprite sheet image each sprite will be. With this information, we are now able to use the subImageInRect method of the Image class to create a new image that represents just the sub-image defined.
Retrieving Sprites
Having set up the sprites on the sprite sheet, the next key activity is to retrieve sprites. We have already discussed that one of the key tasks of the SpriteSheet class is to return an Image class instance configured to render a single sprite from the sprite sheet, based on the grid location of the sprite.
The spriteImageAtCoords: method shown in Listing 6.5 implements the core mechanism for being able to retrieve a sprite.
Listing 6.5. SpriteSheet spriteImageAtCoords: Method
- (Image
*)spriteImageAtCoords:(CGPoint
)aPoint {if
(aPoint.x
>horizSpriteCount
-1
|| aPoint.y
<0
|| aPoint.y
>vertSpriteCount
-1
|| aPoint.y
<0
)return nil
;int
index = (horizSpriteCount
* aPoint.y
) + aPoint.x
;return
[cachedSprites
objectAtIndex
:index]; }
The first check we carry out in this class is on the coordinates that are being passed in. This method takes the coordinates for the sprite in a CGPoint variable. CGPoint has an x and y value that can be used to specify the grid coordinates in the sprite sheet.
When we know that the coordinates are within the sprite sheet, we use the coordinates of the sprite to calculate its location within the NSMutableArray. It's then a simple task of retrieving the image from that index and passing it back to the caller
That's it for this class. It's not that long or complex, but it does provide an important building block within our game engine.