gbadev.org forum archive

This is a read-only mirror of the content originally found on forum.gbadev.org (now offline), salvaged from Wayback machine copies. A new forum can be found here.

Game Design > Scrolling engine/map storage design

#19925 - abilyk - Wed Apr 28, 2004 2:33 am

I wanted to show off my scrolling engine design. Somebody may find it useful, and I needed a good excuse to write this all out. If anything seems inefficient, I'm open to suggestions.

Most commercial and homebrew games I've seen seem to use the standard suggested method for scrolling:
* Store the level tiles (or metatiles) as a big 2D array
* When you need to scroll outside the current section of the map in VRAM, copy in a new strip of tiles OR completely redraw VRAM with the new current section of the map

I didn't want to do this for a couple reasons. First, it forces your level boundaries to be rectangular. You could get around this by piecing together the level with multiple arrays and carefully determining which section of which array to pull tiles from when scrolling at a boundary of arrays, but this seemed like too much of a hassle.

Secondly, the GBA gives us this nice hardware scrolling. I wanted to take advantage of it and create a clean, logical solution that would help the level designers instead of getting in the way.

The game we're working on is similar to a Castlevania or a Metroid in that it will have some narrow tunnels and columns, as well as large, open areas. I wanted my level designers to be able to create maps using screen-sized chunks (30x20 tiles) as building blocks. Some commercial games I've seen use VRAM-block sized chunks (32x32 tiles), which is especially apparent in tight vertical scrolling areas; the camera will try to center on the character and shift back and forth horizontally because of the extra 2 horizontal tiles in the map. This is something I wanted to avoid.

My scrolling engine uses a 512x256 pixel (64x32 tile) map for each layer. The level map for each layer is made up of contiguous "halfscreens," which are literally half the size of the GBA screen (30x10 tiles). I'm using halfscreens instead of "screens" because the 512x256 size hardware map is too "short" to store multiple rows of screens. The six currently viewable halfscreens are stored in the VRAM map as shown here: http://www.dungeonmonkey.com/gbadev/vram_hs.gif

The white boxes are the halfscreens, the greyed-out part of the map is unused. The engine stores the indices of the current halfscreens. When the user tries to scroll into the greyed-out area, the engine updates the current indices array and overwrites the VRAM map with the halfscreens specified by the current indices. Here's a logical representation of a halfscreen's map data (blue are used tiles, grey is unused garbage data, black is no data at all -- there's no reason to store or copy the last 2 tiles worth of garbage data): http://www.dungeonmonkey.com/gbadev/hs_l.gif

And here are the first two rows of a halfscreen's map data as stored in memory: http://www.dungeonmonkey.com/gbadev/hs_m.gif

Halfscreens are DMA'd to VRAM at the starting addresses specified by the red Xs on the VRAM map above. Doing it this way, we can use the same halfscreen map structure and keep the unused garbage data on the outside edges of the VRAM map.

* A halfscreen's map data is 636 Bytes (318 tile entries * 2 bytes per entry).
* To update a layer, 6 halfscreens must be copied to VRAM, which is 3816 Bytes (636 * 6).
* Though unlikely, if all 4 layers are used and must be updated at the same time, 15264 Bytes must be transferred (3816 * 4).
* The DMA transfer rating for EWRAM -> VRAM transfers is equal to 2 cycles per Byte transferred (source: CowBite Virtual Hardware Spec), making the above max transfer (15264 Bytes) 30528 cycles long.
* There are 83776 cycles during VBlank (source: GBATEK). The transfers used by this engine during any given frame would take at maximum 36% of VBlank's cycles, and would leave at minimum 53248 VBlank cycles for other operations.
* These calculations ignore the cycles used to set up each transfer. There are only 24 transfers maximum, so this overhead should be negligible.


Here's my halfscreen struct:

Code:
 typedef struct Halfscreen  // 16 Bytes
{
   u16* map;    // pointer to tile map or metatile map data
   u16  left;   // index of left neighbor
   u16  right;  // index of right neighbor
   u16  up;     // index of upper neighbor
   u16  down;   // index of lower neighbor
   u16  event;  // index of level event triggered by attempting to scroll from this halfscreen
   u8   lock;   // bits 0-3 represent the state of each halfscreen edge: unlocked (0) or locked (1)
   u8   type;   // tile map (0), 8-bit entry metatile map (1), 16-bit entry metatile map (2)
}Halfscreen;


I'll handwave over the event variable, since it's specific to my game's needs, and the comments there explain most of it, but let me explain the lock variable. Each of the first 4 bits specifies whether a given edge of the screen (left, right, up, down) is locked or unlocked. If an edge is locked, the engine will not allow you to scroll in that direction.

This feature serves two purposes. First, it stops the camera from showing secret areas to the player. In the Metroid games, for example, you can walk through certain walls. The camera will not scroll far enough to give away the secret, however, until Samus begins to walk through the wall. The lock variable allows me to easily do the same thing, keeping the camera from scrolling in a certain direction until an event is triggered that unlocks it.

Secondly, because of this, I don't have to test for a NULL neighbor when attempting to scroll the map. Normally, I would want to test each neighbor index for a NULL value, which would specify that the map boundary has been reached, so it wouldn't try to compute and display further. Instead, the map editor will automatically lock each edge on a map boundary. I don't have to test to see if a neighbor exists or not; I can't scroll that direction anyway.

You'll notice mention of metatiles in the comments of the struct. Levels will initially be stored in ROM, and halfscreen map data will not be stored as an array of tile entries but as an array of metatile entries (15 x 5 = 75 entries per metatile map):

Code:
typedef struct Metatile  // 8 Bytes
{
   u16 tile_map_entry[4];  // array of 4 16-bit tile map entries
}Metatile;


Each layer will have its own metatile bank. If there are less than 256 unique metatiles in a bank, that layer's metatile maps will use 1-Byte entries. If there are more than 256, the maps must use 2-Byte entries. When the player enters a new level, the level data and halfscreen structs will be copied to EWRAM. The system will then generate the level's tilemaps in EWRAM using the information stored in the metatile maps and metatile banks.

I've covered a lot, but I may have left out some important details. I'm curious to hear any questions or comments regarding this. Thanks!

#19934 - tepples - Wed Apr 28, 2004 5:54 am

abilyk wrote:
I didn't want to do this for a couple reasons. First, it forces your level boundaries to be rectangular. You could get around this by piecing together the level with multiple arrays and carefully determining which section of which array to pull tiles from when scrolling at a boundary of arrays, but this seemed like too much of a hassle.

If Metroid does this, then it can't be too bad. Or you can just have a big 2D array and give one tile "out of bounds" semantics such that the camera will never move to overlap it.

Quote:
Some commercial games I've seen use VRAM-block sized chunks (32x32 tiles), which is especially apparent in tight vertical scrolling areas; the camera will try to center on the character and shift back and forth horizontally because of the extra 2 horizontal tiles in the map. This is something I wanted to avoid.

That's easy to avoid: don't display the rightmost 2 tiles of a screen unless there's a screen defined to the right of it, that is, unless right is unlocked. In other words, to avoid the annoying Super Mario Advance scrolling artifacts, treat the camera's window as 256 pixels wide.

Quote:
There are 83776 cycles during VBlank (source: GBATEK). The transfers used by this engine during any given frame would take at maximum 36% of VBlank's cycles, and would leave at minimum 53248 VBlank cycles for other operations.

In addition, remember that VRAM is dual-ported with less than one wait state. If you update only the top visible halfscreens during vblank, you can probably update the rest during draw without artifacts.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.

#19940 - LOst? - Wed Apr 28, 2004 3:20 pm

Metroid Zero Mission uses some interesting techniques that I can't really understand.
Sometimes the camera following Samus and stop when it reaches an edge. It will swap maps during a room switch (going through a hatch). Now at some places the camera slows down and I have no idea why or how it is done. Also at some places the screen stops scrolling till Samus has reached the edge of the camera, then the camera will move making Samus stand in the middle of the screen, and if Samus walks one tile, the screen will go making an edge where she just stood. It's a cool effect but how to program this and how to make it trigger, have no idea how it works

#19942 - niltsair - Wed Apr 28, 2004 3:37 pm

One problem I can see with your approach, is that it eats a whole lots of Tiles.

If your game is using the same tiles over and over, this shouldn't be a problem, but if you wanted to display an intricated BG having no repeting Tiles in some instance, then you would soon run out of tiles, especially if you're using the 4 layers.
30x10x4(HalfScreen Map) Tiles --->1200Tiles per layer.
I'm multiplying by 4 since the player could be right at the corner of 4 Halfscreen map, needing you have 4 halfscreen loaded

While if you're using the method of only showing the current screen and leaving the rest of the map empty, this give you :
31x21Tiles ---> 651Tiles per layer. **(edited)** :P
_________________
-Inside every large program is a small program struggling to get out. (Hoare's Law of Large Programs)
-The man who can smile when things go wrong has thought of someone he can blame it on. (Nixon's Theorem)


Last edited by niltsair on Wed Apr 28, 2004 5:12 pm; edited 1 time in total

#19944 - Miked0801 - Wed Apr 28, 2004 5:10 pm

*edit* ;)

#19966 - abilyk - Thu Apr 29, 2004 12:42 am

Niltsair, if I understand you correctly, I don't think that issue would be a problem.

Assume I had a large map, which spanned many halfscreens, in which all tiles referenced were unique. Despite the fact that a given layer's VRAM map would reference more than the standard 31x21 tiles as in your example 2, only a maximum of 31x21 tiles could be displayed onscreen at any given time, and I would only need to store 651 tiles per layer -- the number of visible tiles. I could copy new tiles into VRAM before scrolling into an undisplayed section of the currently loaded map.

If I misunderstood your argument, I apologize. Even if so, for me at least, it's a moot point. Since we're using text backgrounds, we can flip tiles, essentially turning each unique tile into four. I don't expect to have more than 1200 unique tiles per level. ;)

#19970 - niltsair - Thu Apr 29, 2004 12:57 am

So you would load Tiles on a on-screen basis. Then, what's the point of using your 1/2 screen maps method? Like Tepples mentionned, you could just block scrolling to stop from seeing too much of offmap data. You seem to basicly do a scrolling based on halfscreen size chunks, instead of Tile size chunks. Is it only to solve scrolling problem?

Another thing, you're using 512x256 instead of 256x256, which mean you lose 2x as more Tiles for each map.
32x32Maps : 256Colors-->Lose use of 32Tiles per layer
32x32Maps : 016Colors-->Lose use of 64Tiles per layer
64x32Maps : 256Colors-->Lose use of 64Tiles per layer
64x32Maps : 016Colors-->Lose use of 128Tiles per layer
(This text has been proof read with a calculator ;) )

Again, if you do not think to use that much different Tiles, the problem is mooth, minus the overhead incured by taking care of this map method.
_________________
-Inside every large program is a small program struggling to get out. (Hoare's Law of Large Programs)
-The man who can smile when things go wrong has thought of someone he can blame it on. (Nixon's Theorem)

#19972 - abilyk - Thu Apr 29, 2004 1:19 am

niltsair wrote:
So you would load Tiles on a on-screen basis. Then, what's the point of using your 1/2 screen maps method? Like Tepples mentionned, you could just block scrolling to stop from seeing too much of offmap data.


Well, I won't. It was just an example of how to fix the problem you described while still using my system, but I agree, if I needed to update my tileset consistently during scrolling, another method would be more efficient.

The main reasons I plan to use this method over another are 1) that I think it's a cleaner, more logical solution for both the level designer and the level editor programmer and 2) that I wanted to efficiently use the available RAM.

Let me explain #2. Like Tepples said, I could use a 2D array for each level and just use a special tile to designate unused areas. But consider the situation in which a level might have a very long, narrow horizontal passageway followed a tall elevator shaft. Note that I want the level to scroll continuously, no map swaps during a fadeout. The width and height of the level would both be quite large, but the vast majority of level's map array would be filled with the unused tile. If the level is wide and tall enough, the map array would not be able to fit into EWRAM. This was my main concern.

#19975 - niltsair - Thu Apr 29, 2004 1:49 am

Makes sens but for one point
Quote:
If the level is wide and tall enough, the map array would not be able to fit into EWRAM.
Did you really meant in EWRam or you meant in Rom? If you did, why woul you need to load the map in EWRam first? You need to decompress it?

This make sens if you have a lot of narrow corridors and don't want to store the map as a big gigantic rectangle, saving Rom space. Or you could use some compression scheme.

On the top of my head, here's one :
Store your world on a per line basis.
Each line would be defined by a vector data pair (NbRepeated,TileID).

Thus a long serie of empty Map entries could be greatly reduced.
_________________
-Inside every large program is a small program struggling to get out. (Hoare's Law of Large Programs)
-The man who can smile when things go wrong has thought of someone he can blame it on. (Nixon's Theorem)

#19977 - abilyk - Thu Apr 29, 2004 2:21 am

niltsair wrote:
Makes sens but for one point
Quote:
If the level is wide and tall enough, the map array would not be able to fit into EWRAM.
Did you really meant in EWRam or you meant in Rom? If you did, why woul you need to load the map in EWRam first? You need to decompress it?


Compressed in a manner of speaking, yes. The plan is to initially store the maps in ROM not as maps of tile entries, but as maps of metatile entries, as briefly explained in the first post. These metatile maps will use a bit less than 1/4 or 1/8 (dependent on number of metatiles in a layer's metatile bank) the memory needed by the corresponding tile maps. After the game is further along in development, if I find I'm running out of memory (we'll be using a 4MB cart, the smallest available), I'll compress the metatile maps with one of the popular compression schemes.

So yes, the metatile maps in ROM will possibly need to be decompressed, and then used to generate the corresponding tile maps in EWRAM.

#19978 - niltsair - Thu Apr 29, 2004 2:36 am

Ok, it all make sens now :) Good luck with your project.
_________________
-Inside every large program is a small program struggling to get out. (Hoare's Law of Large Programs)
-The man who can smile when things go wrong has thought of someone he can blame it on. (Nixon's Theorem)

#19983 - abilyk - Thu Apr 29, 2004 6:04 am

Thanks, and thanks for your input. :) I'd been anxious to get some feedback on this to see if there were any logic holes I'd overlooked.