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.

Coding > Handling large maps

#139317 - f33ldead - Tue Sep 04, 2007 5:22 pm

Hello,

I'm planning to write a demo in text modes, but I'm concerned about some topics:


  • A background layer's size is not enough for a RPG world map.
    How are maps implemented in usual RPGs then? It looks like by dynamically updating the background, but I'm not sure how to implement this effectively. Any examples will be greatly appreciated!
  • Fonts.
    Do you guys spare a layer for fonts? If not, how would you implement text output?


Thanks!

#139320 - Kyoufu Kawa - Tue Sep 04, 2007 6:01 pm

Larger-than-VRAM maps are indeed loaded on demand, wrapping around the edges. This works since the VRAM map is somewhat larger than the screen and wraps around anyway. Basically, when you've scrolled one tile in a given direction, you draw the strip that would appear next just outside of the screen.

That's not just for RPGs but also for most platformers. In fact, any genre can use it.

I'll leave it to somebody else to explain it in more detail.


Oops, forgot a part. Yes. Some game (engines) use a spare layer for the text output. Some of these merge the text and interface into one layer, some use two seperate layers. Other games use sprites, one for each character. Which does have the added benefit of special effects like wobbles and such.

#139327 - Cydrak - Tue Sep 04, 2007 8:13 pm

It's like you said, you need to "stream" the larger map into the smaller one.

A standard map is 32x32, right? Well, they wrap around, so the way I would think about it, it's really an infinite, repeating "grid" of 32x32 screens. If you ignore the range on the BG scroll, that's the behavior you should get.

Now imagine writing your big worldmap onto the infinite grid:
Code:
// Note: X, Y in tile coordinates
for(y = mapY0; y < mapY1; y++)
    for(x = mapX0; x < mapX1; x++)
        screenMap[x & 31, y & 31] = worldMap[x, y];

This is what you would like to do, but of course it wouldn't work at all, it would stomp right over itself because of the repeat.

However, the actual -screen- is only 30x20 tiles, a "view" onto the larger grid. Since you're probably tracking the scroll already, a small change to the above ought to work:
Code:
for(y = viewY0; y < viewY1; y++)
    for(x = viewX0; x < viewX1; x++)
        screenMap[x & 31, y & 31] = worldMap[x, y];

The trick is just to show the right part at the right time. A lot of games further improve on this, only updating at the scrolled edges:
Code:
void scroll(int dx, int dy) {
    if(dx > 0) {
        for(x = viewX1; x < viewX1+dx; x++)
            for(y = viewY0; y < viewY1; y++)
                screenMap[x & 31, y & 31] = worldMap[x, y];
    } else if(dx < 0) {
        for(x = viewX0+dx+1; x < viewX0; x++)
            for(y = viewY0; y < viewY1; y++)
                screenMap[x & 31, y & 31] = worldMap[x, y];
    }
    viewX0 += dx, viewX1 += dx;
   
    if(dy > 0) {
        for(y = viewY1; y < viewY1+dy; y++)
            for(x = viewX0; x < viewX1; x++)
                screenMap[x & 31, y & 31] = worldMap[x, y];
    } else if(dy < 0) {
        for(y = viewY0+dy+1; y < viewY0; y++)
            for(x = viewX0; x < viewX1; x++)
                screenMap[x & 31, y & 31] = worldMap[x, y];
    }
    viewY0 += dy, viewY1 += dy;
}

I haven't tested that sorta code, I'm sure it could be better, it's just a sketch of how I might approach it. To do pixel scrolling, I guess you'd need to round (viewX0, viewY0) down and (viewX1, viewY1) up to the next tile.

Fonts almost have to go on another layer if you're scrolling (whether it's the map or the dialog). If you just want fixed width, it's just a matter of writing the tilemap. Some people use sprites instead, this is one easier way to do variable width.

Since there is no challenge to doing fixed width, I'll try to explain the other case, which is what I did.

I'm working on the DS, so I went all out and grabbed a screen's worth of 16 color tiles, treating them as a sorta bitmap. (On the GBA you'd prolly use a smaller region.) My fonts are just linear 16 color bitmaps with characters sliced out of them, and I use something like this:
Code:
void printChar(u32* tiles, int tileHeight, int& drawX, int& drawY, const Char& char)
{
    for(int charX = 0; charX < char.width) {
        writeX = drawX + charX - char.xAnchor;
       
        if(writeX < minX) {
            charX += 8 - (writeX & 7);
            continue;
        }
        if(writeX >= maxX)
            break;
       
        // Chop off pixels to the right of the character
        rightClip = charX + 8 - char.width;
       
        // Chop off pixels to the left of this tile
        charShift = 4*(writeX & 7);
        charMask = 0xffffffff >> (rightClip > 0 ? 4*rightClip : 0);
        charMask <<= charShift;
       
        charXOffset = char.nibbleOffset + charX;
        const u32* charData = (u32*)(char.fontData + char.charXOffset/2
                            - bitmapWidth/2*char.yBaseline);
       
        align = (unsigned)charData & 3;
        alignMask = 0xffffffff >> (8*align);
               
        u32* tileColumn = tiles + tileHeight*(writeX & ~7)
                                + (drawY - char.yBaseline);
       
        // I didn't do Y clipping here, there's two ways that make sense.
        // One is to discard out of bounds pixels. The other is to wrap
        //   around the layer edges, which is good if you're scrolling.
        // Both would probably move this loop into a function.
       
        for(int charY = 1; charY < char.rowHeight; charY++)
        {
            yOffset = bitmapWidth/8*charY;
           
            // Fetch 8 pixels of font--possibly a 9th if odd X offset
            // The misaligned access here is purposeful,
            //   and it does the bytewise shifting for us.
            charCol0 = charData[yOffset+0];
            charCol1 = charData[yOffset+1];
            charPixels = charCol0 & alignMask | charCol1 & ~alignMask;
            if(charXOffset & 1)
                charPixels = charCol1<<28 | charPixels>>4;
           
            // Move them to match the character's position over the tile
            charPixels <<= charShift;
           
            // Find all the nonzero font pixels and generate a mask
            charAlpha = charPixels;
            charAlpha = (charAlpha | charAlpha>>2) & 0x33333333;
            charAlpha = (charAlpha | charAlpha>>1) & 0x11111111;
            charAlpha = (charAlpha<<4) - charAlpha;
            charAlpha &= charMask;
           
            // Alpha blit 8 pixels to the screen
            tilePixels = tileColumn[charY];
            tileColumn[charY] = (tilePixels & ~charAlpha) | (charPixels & charAlpha);
        }
        // Move to next column, aligning write position
        //   if the first was a partial tile
        charX += 8 - (writeX & 7);
    }
    drawX += char.xAdvance;
}

Note that there's no restriction on character size, and letters can quite happily overlap, e.g. stylish cursive or hanging italics. They are placed so (xAnchor, yBaseline) is over the cursor, and the next character is printed xAdvance pixels to the right. nibbleOffset is the top left of the full character box. How you get those per-character measurements is up to you. I was lazy, I actually embed them as marker pixels, right in the bitmap, and parse them to create the font in memory.

Okay, so this is probably overkill. ^_^ But not only does it skirt around VRAM 16-bitness, there's only three reads and a write per 8 pixels... if you're not afraid of bit mashing, it should give you cool ideas... (One thing I added was priority masking--I can have an outlined font with tight spacing, where the colored outlines merge together, but won't step on the letters themselves.)

I'm pretty sleepy, so apologies in advance if this turns out more confusing than anything else... >_>

#140126 - f33ldead - Thu Sep 13, 2007 7:44 pm

Thanks for the ideas. I've also seen a more detailed text at Cearn's site, so I've settled it completely. However, about the scrolling issue... Cydrak, thanks but that's nothing like what's in my mind. Such memory copy is much of a overhead, because it's not a linear copy so that we can't DMA it out or memcpy() in a simple, single fast call.

I had this idea. My actual map, which is something big, will be stored somewhere in the memory, whose nametable is chopped into 32x32 mini-maps. However, I'll use a 128x128 map in VRAM, and load mini-maps into the correct "slots"

Code:
0 1
2 3


Assume the character is moving to, say, right. When 31th column appears on the screen, I'll load the current mini-map to 0th slot, and "neighboring" nametable to the 1st slot, so it'll go smoothly. If he's moving down, I'll load current to 0, and "neighbor" to 1, so on. The advantage of doing so is, nametables are linear, and can be DMAed (or copied by some other simple function).

Any comments, ideas?

However, I wonder how commercial RPGs handle the situation.

edit: By the way, I hope Cearn's mirach will break a huge map into 32x32 mini-map chunks. It apparently works correctly for 128x128 maps, but I'm not sure if it will for a map, say, 256x512. I'll test it as I get home.
_________________
It's real, spooky action at distance!

#140162 - ThousandKnives - Fri Sep 14, 2007 12:26 am

How you store map data depends somewhat on whether you are coding for GBA or DS (your post doesn't specify), since the DS's screen width is equal to the width of a map block. Therefore, unless someone knows a really neat technique, it is not possible to do sub-tile horizontal scrolling of a map on DS without expanding your map RAM to 64x32. Vertical scrolling is easy since there is a large buffer in relation to the screen size, but since the DS screen is 32 tiles wide, you need a map buffer width of 33 tiles to do horizontal scrolling at a sub-tile level, since as the screen is scrolling there are two different slivers visible at either side of the screen adding up to 33 discrete columns that need to be displayed.

Otherwise, as pointed out, the technique is pretty straightforward: keep the full map in memory and copy as needed to the map buffer. You can optimize this as you like but in the end a 32x32 map section of map is only 1024 bytes to copy, which is not a major deal. Storing map data in blocks like the tiles themselves is certainly a good idea but even that hardly seems necessary.

#140211 - keldon - Fri Sep 14, 2007 7:23 am

Large map scrolling concept by Jason tells you how it generally works. Don't worry about it being for GBDK, the concept works for tile modes in general.

#140378 - f33ldead - Sat Sep 15, 2007 10:42 pm

keldon wrote:
http://www.devrs.com/gb/files/mapscroll.txt


Have you read the previous posts?
_________________
It's real, spooky action at distance!

#140405 - DiscoStew - Sun Sep 16, 2007 4:37 am

Although the DS has a width of 32 tiles, if you really didn't care about have all 32 horizontal tiles show, you could set up windowing to blacken out the sides, maybe 4 to 8 pixels in length for each side, allowing behind-the-curtain updating of the tiles if you didn't want to resort to expanding the layer size.

I've had an old project on my GBA that dealt with large maps using a custom compressed format that split maps into 64x64 metatile sections (1 metatile = 2x2 defined tiles). If I can find it, perhaps I'll post it (after cleaning it up and possibly optimizing it after the lessons learned with working on the DS).
_________________
DS - It's all about DiscoStew

#140438 - f33ldead - Sun Sep 16, 2007 12:46 pm

BTW, I forgot to mention, mine is a GBA program.

DiscoStew wrote:
've had an old project on my GBA that dealt with large maps using a custom compressed format that split maps into 64x64 metatile sections (1 metatile = 2x2 defined tiles). If I can find it, perhaps I'll post it (after cleaning it up and possibly optimizing it after the lessons learned with working on the DS).


I'll be looking for it :)
_________________
It's real, spooky action at distance!

#140746 - Miked0801 - Wed Sep 19, 2007 6:06 pm

For our edge scrolling in DS (bitmap), we just make sure to sync BG movement and tile loading during VBlank. No edge room sucks, bit is solvable.

For Char scrolling, we used 512x256 to make the loading available outside of interrupt time.