Engine fixes


Shrine Maiden Shizuka is a metroidvania that targets the SEGA Genesis. Programming for this kind of old hardware is difficult as there are a lot of technical limitations that you need to be aware of. Even when using a framework like SGDK, you need to learn the hardware specificities.

The Genesis has its own way to allow scrolling in your game. The Video Display Processor (VDP) has two main plans, PLAN_A and PLAN_B that are usually used for foreground and background. These plans are limited in size, and when your level is larger than the plan, you have to load the next tiles in the VRAM as your character nagivate in the level.

In the case of a metroidvania, there are 4 kind of maps that I'm trying to support:

  • Small maps that fit in the screen
  • Long maps with height equal to the height of the screen
  • High maps, with the width equal to the width of the screen
  • Large maps, with width and height being multiple of the size of the screen

DMA buffering

Today I implemented a faster scrolling method that queues tile updates to the DMA (Direct Memory Access) instead of using DMA_queueDma instead of VDP_setMapEx. The logic of updating columns and rows on the fly still applies:

Before:

if (morphing_x && camera_next_x < 0 && camera_next_x > -terrain->width + SCREEN_WIDTH) {
  u16 x = -camera_tmp_x / 8 + (morphing_x < 0 ? 41 : -1);
  VDP_setMapEx(PLAN_A, terrain->fg->map, TILE_ATTR_FULL(PAL1, FALSE, FALSE, FALSE, ind), x & 63, 0, x, 0, 1, 28);
}

After:

if (morphing_x != 0 && camera_next_x < 0 && camera_next_x > -terrain->width + SCREEN_WIDTH) {
  u16 x = -camera_x / 8 + (morphing_x < 0 ? 41 : -1);
  redrawForegroundColumn(terrain->fg->map, x, ind);
}

And here redrawForegroundColumn will look like:

void redrawForegroundColumn(u16 columnToUpdate) {
    // Calculate where in the tilemap the new row's tiles are located.
    const u16* mapDataAddr = fgMap.tilemap + fgRowOffsets[fgCameraTileY] + columnToUpdate;
    u16 columnBufferIdx = fgCameraTileY;
    u16 baseTile = TILE_ATTR_FULL(PAL_FG, 0, 0, 0, fgTilesetStartIdx);
    // Copy the tiles into the buffer.
    u16 i;
    for (i = VDP_PLANE_TILE_HEIGHT; i != 0; i--)
    {
        columnBufferIdx &= 0x1F;  // columnBufferIdx MOD 32 (VDP_PLANE_TILE_HEIGHT)
        fgColumnBuffer[columnBufferIdx] = baseTile + *mapDataAddr;
        columnBufferIdx++;
        mapDataAddr += fgMap.w;
    }
    // Queue copying the buffer into VRAM.
    DMA_queueDma(DMA_VRAM, (u32) fgColumnBuffer, PLANE_FG + ((columnToUpdate & VDP_PLANE_TILE_WIDTH_MINUS_ONE) << 1), VDP_PLANE_TILE_HEIGHT, VDP_PLANE_TILE_WIDTH_TIMES_TWO);
}

It's way more low level code than calling SGDK's VDP_setMapEx but doing it using DMA has some benefits. As @kcowolf explained:

While the VDP is drawing one frame you're buffering the tiles to be updated before the next frame, then during the vertical blanking interval you can DMA the new data into VRAM.

A full demo of this technique in SGDK is available here https://github.com/kcowolf/GenScrollingMapDemo

This change had the nice effect of fixing 8 direction scrolling. Which means we can now have larger maps in the game.
Before this change, I could only do maps that are either the width of the screen, either the height of the screen.

Fade in and fade out properly

Another thing I change was calling VDP_fadeOut and VDP_fadeIn at the right moment when switching to the next map.

You have to call VDP_fadeOut when the sprites are still there, so they will be part of the fade out animation. Then, while the screen is black, it's time to do all the changes like freeing old sprites, old map, loading the new map, inserting the new enemies, position the camera and load the tiles in the VDP and then only you call VDP_fadeIn to display.

This is an example of how I am implementing this:

VDP_fadeOut(0, (4 * 16) - 1, 10, FALSE);
fix32 old_y = self->y;
fix32 old_yspeed = self->yspeed;
fix32 old_xspeed = self->xspeed;
u8 old_lookleft = self->look_left;
self->on_collide = NULL;
terrain = e2->transit();
refreshRowOffsets(terrain);
player->x = FIX32(0);
player->y = old_y + e2->yoffset;
player->yspeed = old_yspeed;
player->xspeed = old_xspeed;
player->look_left = old_lookleft;
camera_x = 0;
camera_x_offset = 0;
camera_y = e2->yoffset;
refreshScroll();
redrawScreen(terrain->fg->map, ind);
VDP_fadeIn(0, (4 * 16) - 1, palette, 10, TRUE);

As you can see, everything happens before the two fading calls. The transitions from one screen to another are now smooth.

Using SPR_reset()

The sprite engine of SGDK can be initialized one time at the beginning of your game using SPR_init(), then you can use SPR_reset() when loading a new level.

This ensures that all the former sprite resources are released and that they won't be drawn on your new screen.

Also, as the sprite engine uses the same video RAM, be careful with what you free before calling SPR_reset(). By changing the order of instructions when switching to a new level I could fix a recurrent memory access crash in the game.

Files

rom.bin 384 kB
Dec 01, 2019

Get Shrine Maiden Shizuka (SEGA Mega Drive / Genesis)

Leave a comment

Log in with itch.io to leave a comment.