Lesson 16: Sprites and Sprite Sheets
A sprite is nothing more than an image that can be manipulated around on the computer screen. Sprites usually come accompanied with a background that is either transparent or opaque in color to use as transparency and are typically small in size with a known width and height dimension. More often than not sprites are used for character animation by showing a sequence of sprite cells in series. There are a number of different approaches to loading and displaying sprites. The first approach, as illustrated in the next example program, loads a number of individual images into an array to be displayed in order to create a walking character demonstration.
( This code can be found at .\tutorial\Lesson16\WalkDemo1.bas )
'** Walker Demo 1
'** Load Individual Sprite Images
CONST BRIGHTMAGENTA = _RGB32(255, 0, 255) ' declare colors
CONST WHITE = _RGB32(255, 255, 255)
DIM Frame% ' frame counter
DIM Sprite% ' sprite image counter
DIM Walker&(6) ' sprite images
DIM x1%, y1%, x2%, y2% ' sprite upper left and lower right X,Y coordinates
DIM Dir% ' sprite travel direction
DIM Filename$ ' file name of each sprite image
DIM Path$ ' location of sprites on drive
'** Load each sprite image one at a time
Path$ = ".\tutorial\Lesson16\" ' path to sprites
FOR Sprite% = 1 TO 6 ' load 6 sprites
Filename$ = "walk" + _TRIM$(STR$(Sprite%)) + ".png" ' filename of sprite
Walker&(Sprite%) = _LOADIMAGE(Path$ + Filename$, 32) ' load sprite
_CLEARCOLOR BRIGHTMAGENTA, Walker&(Sprite%) ' set transparent color of sprite
NEXT Sprite%
Frame% = 0 ' reset frame counter
Sprite% = 1 ' reset sprite image counter
Dir% = 1 ' set sprite direction
x1% = 10 ' upper left X coordinate of sprites
y1% = 50 ' upper left Y coordinate of sprites
y2% = y1% + 155 ' lower right Y coordinate of sprites
SCREEN _NEWIMAGE(640, 480, 32) ' enter graphics screen
DO ' begin main loop
_LIMIT 30 ' 30 frames per second
CLS , WHITE ' clear screen in white
LOCATE 2, 21 ' locate text cursor
PRINT " Right/Left arrows to walk, ESC to exit. " ' print directions
IF _KEYDOWN(19712) THEN ' right arrow key pressed?
Dir% = 1 ' yes, set direction heading to right
ELSEIF _KEYDOWN(19200) THEN ' left arrow key pressed?
Dir% = -1 ' yes, set direction heading to left
END IF
IF x1% + Dir% * 3 < 536 AND x1% + Dir% * 3 > 0 THEN ' sprite at edge of screen?
x1% = x1% + 3 * Dir% ' no, update X location of sprite
ELSE ' yes, at edge of screen
Dir% = -Dir% ' change sprite direction
END IF
x2% = x1% + 103 ' calculate lower right hand X coordinate
SELECT CASE Dir% ' which direction is sprite heading?
CASE 1 ' to the right
_PUTIMAGE (x1%, y1%), Walker&(Sprite%) ' display sprite image normal
CASE -1 ' to the left
_PUTIMAGE (x2%, y1%)-(x1%, y2%), Walker&(Sprite%) ' flip sprite image horizontally
END SELECT
Frame% = Frame% + 1 ' increment frame counter
IF Frame% = 30 THEN Frame% = 0 ' reset frame counter after 30 frames
IF Frame% MOD 5 = 0 THEN ' frame even divisible by 5?
Sprite% = Sprite% + 1 ' yes, increment sprite image counter
IF Sprite% = 7 THEN Sprite% = 1 ' keep sprite image counter within limits
END IF
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC key pressed
FOR Sprite% = 1 TO 6 ' cycle through the six sprite images
_FREEIMAGE Walker&(Sprite%) ' free image from memory
NEXT Sprite%
SYSTEM ' return to operating system
Figure 1: Sprite animation
The program above loads 6 individual images, walk1.PNG through walk6.PNG, and saves them to the Walker&() array as seen in lines 17 through 22 of the code. The PNG images do not contain an alpha channel so line 21 identifies the color to use for transparency in each image.
walk1.PNG
walk2.PNG
walk3.PNG
walk4.PNG
walk5.PNG
walk6.PNG
In the main loop of the program the images are simply displayed in sequence as the loop progresses. An integer variable named Sprite% is consistently incremented from 1 to 6 identifying the next sprite image to display. In lines 55 through 58 modulus division is used to increment the Sprite% variable every 5th frame. Since the main loop runs at 30 frames per second the character animation needs to be slowed down otherwise the character would look like The Flash dashing around. This effectively slows the character animation down to 6 frames per second while still allowing the main loop to run at 30 frames per second.
Lines 47 through 52 examines the direction the character is traveling in and either displays the sprite as loaded for right hand motion or flipped horizontally for left hand motion. By using _PUTIMAGE to flip the sprites the program effectively has 12 sprite images it can display instead of the original 6 loaded.
Loading individual images is perfect for games with a small number of sprites which helps to keep the asset file count low. However, for large projects that require many sprites, say for a game like Super Mario, there would be hundreds, perhaps even thousands, of individual image files to keep track of. This is where a sprite sheet can help.
Sprite Sheets
A sprite sheet is a collection of individual sprite images contained in an ordered fashion on a larger image. By placing the individual images onto a larger sheet only one image needs to be loaded from disk. From there the individual images can be parsed out by using the _PUTIMAGE statement. The example program above has been modified to load the walking sprites from a master sprite sheet called walksheet104x156.PNG. The 104x156 in the name of the sprite sheet image file is used to identify the size of the individual sprite images contained on the sheet. This is nomenclature I have developed and used over the years while working with sprite sheets.
( This code can be found at .\tutorial\Lesson16\WalkDemo2.bas )
'** Walker Demo 2
'** Load Sprite Sheet and Parse Images
CONST BRIGHTMAGENTA = _RGB32(255, 0, 255) ' declare colors
CONST WHITE = _RGB32(255, 255, 255)
DIM Frame% ' frame counter
DIM Sprite% ' sprite image counter
DIM WalkerSheet& ' sprite sheet containing walker images
DIM Walker&(6) ' sprite images
DIM x1%, y1%, x2%, y2% ' sprite upper left and lower right X,Y coordinates
DIM Dir% ' sprite travel direction
'** Load sprite sheet and parse out individual images
WalkerSheet& = _LOADIMAGE(".\tutorial\Lesson16\walksheet104x156.png", 32)
_CLEARCOLOR BRIGHTMAGENTA, WalkerSheet&
OR Sprite% = 0 TO 5
Walker&(Sprite% + 1) = _NEWIMAGE(104, 156, 32)
_PUTIMAGE , WalkerSheet&, Walker&(Sprite% + 1), (Sprite% * 104, 0)-(Sprite% * 104 + 103, 155)
NEXT Sprite%
Frame% = 0 ' reset frame counter
Sprite% = 1 ' reset sprite image counter
Dir% = 1 ' set sprite direction
x1% = 10 ' upper left X coordinate of sprites
y1% = 50 ' upper left Y coordinate of sprites
y2% = y1% + 155 ' lower right Y coordinate of sprites
SCREEN _NEWIMAGE(640, 480, 32) ' enter graphics screen
DO ' begin main loop
_LIMIT 30 ' 30 frames per second
CLS , WHITE ' clear screen in white
LOCATE 2, 21 ' locate text cursor
PRINT " Right/Left arrows to walk, ESC to exit. " ' print directions
IF _KEYDOWN(19712) THEN ' right arrow key pressed?
Dir% = 1 ' yes, set direction heading to right
ELSEIF _KEYDOWN(19200) THEN ' left arrow key pressed?
Dir% = -1 ' yes, set direction heading to left
END IF
IF x1% + Dir% * 3 < 536 AND x1% + Dir% * 3 > 0 THEN ' sprite at edge of screen?
x1% = x1% + 3 * Dir% ' no, update X location of sprite
ELSE ' yes, at edge of screen
Dir% = -Dir% ' change sprite direction
END IF
x2% = x1% + 103 ' calculate lower right hand X coordinate
SELECT CASE Dir% ' which direction is sprite heading?
CASE 1 ' to the right
_PUTIMAGE (x1%, y1%), Walker&(Sprite%) ' display sprite image normal
CASE -1 ' to the left
_PUTIMAGE (x2%, y1%)-(x1%, y2%), Walker&(Sprite%) ' flip sprite image horizontally
END SELECT
Frame% = Frame% + 1 ' increment frame counter
IF Frame% = 30 THEN Frame% = 0 ' reset frame counter after 30 frames
IF Frame% MOD 5 = 0 THEN ' frame even divisible by 5?
Sprite% = Sprite% + 1 ' yes, increment sprite image counter
IF Sprite% = 7 THEN Sprite% = 1 ' keep sprite image counter within limits
END IF
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC key pressed
FOR Sprite% = 1 TO 6 ' cycle through the six sprite images
_FREEIMAGE Walker&(Sprite%) ' free image from memory
NEXT Sprite%
SYSTEM ' return to operating system
The sprite sheet used in the example above
Each sprite on the sheet is 104 by 156 pixels in size and by using some simple math and the _PUTIMAGE statement each sprite can be copied and pasted to another internal image within the code. Lines 16 through 21 in the above example does this. Through each loop of the FOR...NEXT statement the location of the next image is calculated and copied into the appropriate Walker&() array index by the _PUTIMAGE statement. This method of loading one master image and then extracting each to a separate image still creates many images that need to be manipulated in code.
There is another way to use sprite sheets where the sprite sheet itself is the only image used. Whenever one of the sprites on the sheet need to be displayed the coordinates on the sheet can be referenced and _PUTIMAGE used to copy and paste directly to the screen. The next example code shows how this can be done.
( This code can be found at .\tutorial\Lesson16\WalkDemo3.bas )
'** Walker Demo 3
'** Load Sprite Sheet and Reference Images on Sheet as Needed
CONST BRIGHTMAGENTA = _RGB32(255, 0, 255) ' declare colors
CONST WHITE = _RGB32(255, 255, 255)
TYPE WALKER ' sprite locations
x1 AS INTEGER ' upper left X
y1 AS INTEGER ' upper left Y
x2 AS INTEGER ' lower right X
y2 AS INTEGER ' lower right Y
END TYPE
DIM Walker(6) AS WALKER ' sprite locations on sprite sheet
DIM Frame% ' frame counter
DIM Sprite% ' sprite image counter
DIM WalkerSheet& ' sprite sheet containing walker images
DIM x1%, y1%, x2%, y2% ' sprite upper left and lower right X,Y coordinates
DIM Dir% ' sprite travel direction
'** Load sprite sheet and record sprite locations
WalkerSheet& = _LOADIMAGE(".\tutorial\Lesson16\walksheet104x156.png", 32)
_CLEARCOLOR BRIGHTMAGENTA, WalkerSheet&
FOR Sprite% = 0 TO 5
Walker(Sprite% + 1).x1 = Sprite% * 104
Walker(Sprite% + 1).y1 = 0
Walker(Sprite% + 1).x2 = Walker(Sprite% + 1).x1 + 103
Walker(Sprite% + 1).y1 = 155
NEXT Sprite%
Frame% = 0 ' reset frame counter
Sprite% = 1 ' reset sprite image counter
Dir% = 1 ' set sprite direction
x1% = 10 ' upper left X coordinate of sprites
y1% = 50 ' upper left Y coordinate of sprites
y2% = y1% + 155 ' lower right Y coordinate of sprites
SCREEN _NEWIMAGE(640, 480, 32) ' enter graphics screen
DO ' begin main loop
_LIMIT 30 ' 30 frames per second
CLS , WHITE ' clear screen in white
LOCATE 2, 21 ' locate text cursor
PRINT " Right/Left arrows to walk, ESC to exit. " ' print directions
IF _KEYDOWN(19712) THEN ' right arrow key pressed?
Dir% = 1 ' yes, set direction heading to right
ELSEIF _KEYDOWN(19200) THEN ' left arrow key pressed?
Dir% = -1 ' yes, set direction heading to left
END IF
IF x1% + Dir% * 3 < 536 AND x1% + Dir% * 3 > 0 THEN ' sprite at edge of screen?
x1% = x1% + 3 * Dir% ' no, update X location of sprite
ELSE ' yes, at edge of screen
Dir% = -Dir% ' change sprite direction
END IF
x2% = x1% + 103 ' calculate lower right hand X coordinate
SELECT CASE Dir% ' which direction is sprite heading?
CASE 1 ' to the right
_PUTIMAGE (x1%, y2%)-(x2%, y1%), WalkerSheet&, ,_
(Walker(Sprite%).x1, Walker(Sprite%).y1)-_
(Walker(Sprite%).x2, Walker(Sprite%).y2) ' copy/paste image from sprite sheet
CASE -1 ' to the left
_PUTIMAGE (x2%, y2%)-(x1%, y1%), WalkerSheet&, ,_
(Walker(Sprite%).x1, Walker(Sprite%).y1)-_
(Walker(Sprite%).x2, Walker(Sprite%).y2) ' copy/paste image from sprite sheet
END SELECT
Frame% = Frame% + 1 ' increment frame counter
IF Frame% = 30 THEN Frame% = 0 ' reset frame counter after 30 frames
IF Frame% MOD 5 = 0 THEN ' frame even divisible by 5?
Sprite% = Sprite% + 1 ' yes, increment sprite image counter
IF Sprite% = 7 THEN Sprite% = 1 ' keep sprite image counter within limits
END IF
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC key pressed
_FREEIMAGE WalkerSheet& ' remove image from memory
SYSTEM ' return to operating system
In this example the Walker() array is used to hold the coordinates of each individual image on the sprite sheet. When a sprite needs to be displayed these coordinates are referenced by _PUTIMAGE instead of using an individual image file. The _PUTIMAGE statements in line 57 and 61 show how this is done. With this example the only image ever loaded and referenced is the sprite sheet.
The method chosen to use when dealing with sprites and sprites sheets is completely up to the programmer. Personally I always lean toward the second example because I prefer to have all my sprites preloaded into RAM as individual images ready for use. However, this method does take a considerable amount of RAM when dealing with hundreds or thousands of sprites. You also need to keep in mind that all of those images need to be removed from RAM before ending the program.
Numbered Sprites
There's a fourth method where images can be referenced from a sprite sheet without even using an array to hold the coordinates. Very large sprite sheets can have their individual sprites referenced by a number that can be used to calculate the x,y location of the sprite within the sheet. Figure 2 below shows a sprite sheet that contains 368 individual sprites.
Figure 2: That's a lot of sprites!
Note: The numbers have been placed on top of the individual sprites for reference only. The original sprite sheet named mario32x32.PNG does not contain the numbers.
Using this image as a guide any of the sprites can be referenced as a number from 1 to 368. The following example code shows how this can be done.
( This code can be found at .\tutorial\Lesson16\NumberedSheet.bas )
'** Sprite demo using a numbered sprite sheet
DIM MarioSheet& ' the mario sprite sheet
DIM x%, y% ' X,Y coordinate of sprite on sheet
DIM Columns% ' number of sprite columns contained on sheet
MarioSheet& = _LOADIMAGE(".\tutorial\Lesson16\mario32x32.png", 32) ' load sprite sheet
Columns% = _WIDTH(MarioSheet&) \ 32 ' calculate number of columns on sheet
SCREEN _NEWIMAGE(320, 200, 32) ' enter graphics screen
DO ' begin main loop
CLS ' clear screen
LOCATE 2, 1 ' position text cursor
LINE INPUT "Enter sprite number (0 to exit)> ", n$ ' print directions
Num% = VAL(n$) ' convert answer to numeric value
IF Num% > 0 THEN ' value greater than 0?
IF Num% MOD Columns% = 0 THEN ' yes, is this sprite in rightmost column?
x% = 32 * (Columns% - 1) ' yes, calculate X coordinate of this sprite
y% = (Num% \ Columns% - 1) * 32 ' calculate Y coordinate of this sprite
ELSE ' no, in column left of rightmost column
x% = (Num% MOD Columns% - 1) * 32 ' calculate X coordinate of this sprite
y% = (Num% \ Columns%) * 32 ' calculate Y coordinate of this sprite
END IF
IF y% > _HEIGHT(MarioSheet&) - 1 THEN ' does sprite row exist?
LOCATE 7, 2 ' no, position text cursor
PRINT "That sprite does not exist!" ' report error to user
ELSE ' yes, row exists
_PUTIMAGE (100, 50), MarioSheet&, , (x%, y%)-(x% + 31, y% + 31) ' copy/paste from sprite sheet
LOCATE 7, 2 ' position text cursor
PRINT "Sprite located at position"; Num% ' print results
LOCATE 8, 2 ' position text cursor
PRINT "Press any key to continue.." ' print directions
END IF
DO: LOOP UNTIL INKEY$ <> "" ' wait for key press
END IF
LOOP UNTIL Num% = 0 ' leave when 0 entered for sprite number
_FREEIMAGE MarioSheet& ' remove sprite sheet from memory
SYSTEM ' return to operating system
Figure 3: Accessing sprites by number
Using code in this manner eliminates the requirements for any sort of an array or secondary images all together. The only image required is the sprite sheet itself. Lines 16 through 22 of the code performs some simple calculations to determine the X and Y coordinate locations of any sprite contained on the sheet.
There is one major drawback to this method however. The sprite sheet has to have sprites that are all the exact same size because the number of sprite columns needs to be known beforehand for the calculations to work. The number of columns is calculated from the width of the sprite sheet divided by the width of an individual sprite as seen in line 8 of the example code.
Sprite sheets are a great way to quickly and easily enter sprites and animations into your games. If you do a Google search for "Sprite Sheet" you'll find thousands of pre-made sheets to choose from or create your own with your favorite graphics program.
Something you may have been wondering up to this point is, "Why is the transparent color used always bright magenta (255, 0, 255)?" Simple, it's a garish eye hurting color that no game programmer in their right mind would ever use in their game. Honestly, that's why. That, and it's easy to remember (255, 0, 255).