issue #10

Power Programming

By Seav

Welcome to the Power Programming series

All right, you know about GET and PUT. Maybe even made a few games with them. And with the introduction of such libraries as Blast!, Dash, and DirectQB, you have powerful graphics routines at your disposal.

 

But, nobody has really addressed the issue of managing all those tiles and sprites you have. Well, fortunately for you, I wrote this article just for that! In this article, you'll learn how to calculate array sizes for your sprites— explanations included so you don't have to memorize stupid formulas that don't help at all. You'll also learn how to store multiple sprites in an array—and how to use two-dimensional arrays for all your same-sized tiles. You'll also know how to save all your sprites into files and simply load them up straight into your PUT arrays! No more hacks of drawing your sprites at the start of your program.

This will be a first in an informal series of articles that deal with power programming.

Array Sizes
How do you calculate what size the array needs to be in order to contain the graphic you intend to capture with GET? If you've ever asked this question, you'll probably get a formula like this for an answer:

ArraySize = (Width * Height + 4) / 2 - 1

But what does it all mean? Well, let's suppose that we have a 16x16 tree sprite to GET to an array. Since we're in Screen 13, each pixel is a byte in size (can take any of the 256 possible colors). So, we would need 16 * 16 = 256 bytes to store the image alone. But because GET needs space to store the sprite's width and height, each of which, being WORD values, occupy two bytes, we need an additional 4 bytes for a total of 256 + 4 = 260 bytes. That's what the plus 4 in the formula means.

If we are dimensioning an INTEGER array (where each element is two bytes in size), we would need an array with 130 elements (260 / 2 = 130), hence the divide by 2 in the formula. Here's the code to dimension the tree sprite array:

DIM Tree(129)

We used 129 in the DIM statement bacause there is already an element 0 (the reason why we're subtracting 1 in the formula).

If the sprite's size in bytes were an odd value, we round up. For example, if we have a 15x15 sprite, the sprite would occupy 15 * 15 + 4 = 229 bytes which will occupy 229 / 2 = 114.5 or 115 INTEGER array elements. Thus, we dimension the array this way:

DIM Tree(114)

Storing Multiple Sprites In An Array
Have you ever seen code that looks like this?

DEFINT A-Z
DIM Player1(129), Player1Mask(129), Player2(129)
DIM Player2Mask(129), Tree(129), Grass(129), Rock(129)

The coder is obviously using one array per sprite. It would be better to store everything in as few arrays as possible so that everything would become clearer. Besides, this is especially suited for tile engines where each tile is the same size. This is also done so that we can store numerous sprites in one file using BSAVE and BLOAD.

The solution lies in the GET syntax. We can indicate in which element of the array to start storing the sprite by simply adding subscripts to the array name. For instance this code actually stores the sprite in the first element, 0.

GET (0, 0)-(15, 15), Tree

So it is the same thing as writing

GET (0, 0)-(15, 15), Tree(0)

If we dimension an array to have 260 elements (520 bytes), we can store two 16x16 sprites in it!

DIM TwoSprites(259)
GET (0, 16)-(15, 31), TwoSprites(0)
GET (0, 32)-(15, 47), TwoSprites(130)

You'll notice that we stored the second sprite starting in element 130 since the first sprite already occupies elements 0 to 129 (130 elements, 260 bytes). The second sprite would then occupy elements 130 to 259.

You can extend this concept to store as many sprites as you want in one array. However, the array cannot exceed 64K in size (good for 252 16x16 sprites) unless you use the /AH switch in QB4.5 and the $DYNAMIC metacommand. Also, be careful that you don't overlap sprites in the array. You can leave gaps, but never overlap them.

An Application: Tile Engine Drawing Routine
Storing multiple sprites in one array is especially useful in tile engines where every tile has the same size. If your map is stored in an array, you can use the tile numbers to calculate in which element of the sprite array that tile is located:

For example, we have a Map() array that contains the tile numbers of each part of the map:

  • 0 = Grass
  • 1 = Tree
  • 2 = Vertical path
  • 3 = Horizontal path
  • 4 = Rock
  • 5 = Water
  • etc.

So if Map(13, 14) contains a 4, then there's a rock at that position.

If we organize the sprite array so that the first tile is grass, the second tile is a tree, etc., we can simplify drawing the tiles: (Assume that we have 16x16 tiles)

' Draw a 10x10 portion of the map
' from (1,1) to (10,10)
'
FOR Y = 1 to 10
   FOR X = 1 to 10
     ' Calculate position on screen to place the tile
      XScreen = (X - 1) * 16
      YScreen = (Y - 1) * 16

      ' Calculate where the tile is stored
      Element = Map(X, Y) * 130

      ' Draw the tile on the screen
      PUT (XScreen, YScreen), Tiles(Element)
   NEXT
NEXT

It's very simple, isn't it?

If we're going to use two-dimensional arrays, accessing the tiles will become much, much easier. How do we do it? Instead of dimensioning a one-dimensional array, use a two-dimensional array where the size of the first dimension is the size of the sprite and the second dimension is the number of tiles. For example,

DIM Tiles(129, 19)

This statement will dimension an array that can hold 20 16x16 sprites (figure out why). To store the sprites, just use this

GET (...), Tiles(0, TileNumber)

where TileNumber is the number of the tile you're GETting. PUTing them works the same way.

This method of using two-dimensional array works because the first dimension elements, (0, x), (1, x), (2, x), etc., are contiguous in memory (i.e., they are next to each other).

THE GET FORMAT
I've said earlier that GET needs 4 bytes to store the sprite's width and height. Well, it puts this information at the front, before the image data. So, if you stored the sprite in an INTEGER array, the first element contains the sprite's width in bits (or pixels * 8). The next element contains the sprite's height in pixels. After that comes the sprite data (which are pixel color values). (SCREEN 13 only)

  Offset   Length   Description
  (bytes)  (bytes)
  -------  -------  ------------------
    00     02 WORD  width in bits
    02     02 WORD  height in pixels
    04     XX       sprite image data
The sprite data is arranged much like what you would expect. The pixels representing the top row in the sprite are first, followed by the pixels in the next row and so forth, down to the bottom row. Knowing the GET format is essential if you're planning on making your own sprite routines which are compatible to BASIC's GET and PUT routines.

BSAVE AND BLOAD
If you're the type of programmer who draws his sprites at the start of the program and GETS them, then it would be useful to know that you can do this once during development and load the sprites straight to the arrays in the actual program. You do this by using two of BASIC's useful commands: BSAVE and BLOAD.

BSAVE and BLOAD are two memory statements whose purpose is to transfer data between RAM and files. BSAVE lets you store parts of the memory to a file and BLOAD gets from the file and dumps it to the memory. To be able to fully use these commands, you first need to know all about Segments and Offsets, so if you don't know anything about these terms, don't read on until you do. (See the first part of Petter's ASM article in issue 4 for a nice introduction to Segments and Offsets)

Syntax:
   BSAVE filespec, offset, length
   BLOAD filespec, [offset]

   filespec Filename (path optional) of the file to save to or load from.
   offset Offset into the default data segment to save from or load to. This value is an unsigned integer (0 to 65535).
   length Length in bytes of the memory image to save. This value is an unsigned integer (0 to 65535).

An example, to quickly save the screen (in SCREEN 13) to a file, you can use the code below. When you run the code, you will now have a file, SCREEN.GFX, in the default directory containing the memory image of the screen. (The palette is not saved, BTW.)

DEF SEG = &HA000
BSAVE "screen.gfx", 0, 64000
DEF SEG

The Screen 13 pixels are stored in memory at the address A000:0000 and is 64000 bytes long (320 * 200 = 64000). The segment A000 is a hex value and thus, the value in the DEF SEG statement contains a prefix (&H) to indicate that it is in hexadecimal. DEF SEG is a command that changes the default data segment for use with such commands as PEEK, POKE, BSAVE, BLOAD, and CALL ABSOLUTE. It has this syntax:

DEF SEG [=segment]

It is usually a good idea to restore the original default segment by executing another DEF SEG without attributes. You can omit this if you know what you are doing.

There is a quirk with BSAVE that you should know. If you specify a file without an extension, BASIC automatically assumes a .BAS extension, which I would say is fairly stupid. Also, the if the file you specify in BSAVE exists, it is completely replaced, so beware!

To load the screen back use this code:

DEF SEG = &HA000
BLOAD "screen.gfx", 0
DEF SEG

If you omit the offset in the BLOAD statement, BASIC will assume the segment and offset you got the image from (with BSAVE). Omitting the offset is not recommended unless the location will not change. Since the video RAM is more or less fixed in location (it has to be), you can simply use this statement instead of the code above:

BLOAD "screen.gfx"

VARSEG And VARPTR
Since BASIC arrays are stored in memory, you can use BSAVE and BLOAD to save or load your graphics to the arrays. The only thing we need to know is where in memory the arrays are located. We can find out using these two functions, VARSEG and VARPTR.
VARSEG is a function that returns the segment in which the specified variable resides. Likewise, VARPTR returns the offset. Syntax

VARSEG(variablename)
VARPTR(variablename)

Example

Number% = 10
DEF SEG = VARSEG(Number%)
BSAVE "number.tmp", VARPTR(Number%), 2
DEF SEG

The example above stores the contents of the variable, Number%, into the file NUMBER.TMP.

You can specify any variable as the variablename. This includes array elements and record names. Some more examples using VARSEG:

DIM Map(1 to 10, 1 to 10)

' Get the segment of the first element of Map
Segment = VARSEG(Map(1, 1))

' Get the segment of the (3,4) element of Map
Segment = VARSEG(Map(3, 4))

TYPE RGB
R AS INTEGER
G AS INTEGER
B AS INTEGER
END TYPE

DIM Palette(255) as RGB

' Get the segment of the red attribute of color 100
Segment = VARSEG(Palette(100).R)

Since many variables change locations often, it is recommended that you use the values returned by VARSEG and VARPTR immediately.

Note: If you want to get the offset of a dynamic-length string variable, use SADD instead of VARPTR since using VARPTR returns the offset of the string's descriptor, not the string itself.

Saving Those Sprites
We have 20 tiles stored in one big array and we wish to save it to a file so that we can just load it when the game starts. We do this:

DIM Tiles(129, 19)
' 20 16x16 tiles (5200 bytes)
.
. (draw and GET the tiles)
.
DEF SEG = VARSEG(Tiles(0))
BSAVE "maptiles.gfx", VARPTR(Tiles(0)), 5200
DEF SEG

To load them back in the actual game, use this:

DIM Tiles(129, 19)
DEF SEG = VARSEG(Tiles(0))
BLOAD "maptiles.gfx", VARPTR(Tiles(0))
DEF SEG

You'll notice that we specified the offset in the BLOAD statement since it is not guaranteed that the array will be in the exact same location it was when we saved it.

The BSAVE File Format
The BSAVE statement stores the memory image in a special file format with a 7-byte header. As such, you cannot just BLOAD any file. (This information is presented for those who are interested.)

  BSAVE FILE HEADER

  Offset   Length   Description
  (bytes)  (bytes)
  -------  -------  -------------------
    00     01 BYTE  Special BSAVE value
    01     02 WORD  Original segment 
    03     02 WORD  Original offset
    05     02 WORD  Length of memory image 
The original segment:offset is the memory address the memory image was taken from. Immediately after the header comes the memory image.

SPECIAL WARNING
Since we are using BSAVE and BLOAD, the potential for disaster is great. You might accidentally lose a file with BSAVE or crash the computer using BLOAD. Always take care when you're using these two statements. Make sure that you don't interchange BLOAD with BSAVE, and VARSEG with VARPTR and always check that you have your DEF SEGs straightened up.

Conclusion
I hope that this article enlightened you with how to organize your graphics in your arrays and files. Look forward for more articles from me. For instance, in my next article, I will present better alternatives to BSAVE/BLOAD and TIMER. Happy programming, for now!

 
Pester Seav to tell you the secret of the elusive BEXIT, BSAVEAS and BPRINTPREVIEW at this address.

 

Back to Top