Mode 3 Scrolling Game Engine Demo (WIP)

Use this forum to share and discuss Uzebox games and demos.
User avatar
Artcfox
Posts: 1382
Joined: Thu Jun 04, 2015 5:35 pm
Contact:

Re: Mode 3 Scrolling Game Engine Demo (WIP)

Post by Artcfox »

I think it's the little details that matter.

While the unicorn is alive, I want it to be drawn on top of all the other sprites, so for instance the fart spray gets drawn on top of the enemies when attacking them, and well it's the main character so it is important for it to be seen. However when it dies, I want all of the other sprites to be drawn on top of the unicorn, so if it gets killed by a falling spike, the spike is not completely hidden, and instead it looks like the unicorn is being stabbed by the spike. (Or maybe I want killer butterflies to munch on its corpse.)

To keep the unicorn drawn on top of the rest of the sprites, I made sure to use the highest numbered sprites, and then I assigned sprites to the active enemies starting at sprite 0, and ending before the dedicated unicorn sprites.

Getting those to swap places at runtime, and work correctly was a bit tricky to pull off, and I wanted to verify that there weren't any glitches during that transition. It turned out there was a glitch where the unicorn was drawn twice on top of each other, once upside down and again right side up for a single frame (because I forgot to turn off the old sprites during the switchover) but I couldn't actually see it while playing the game because it happened only for a single frame.

Unfortunately single-stepping in cuzebox doesn't always advance by a single video frame, so the best way I found to truly see anything frame by frame at 60fps is to tell cuzebox to record a video and then convert it to a series of PNG files and then step through those with an image viewer program, because that allows you to go back and forward in time easily.

After fixing my glitch, you can now see the transition for the Z-order swap happens at the exact instant the unicorn dies. (Again, the hitbox for enemies is a bit smaller than the actual object to make things feel more fair, which is why the unicorn doesn't die until the spike is well into it.)

spike-cropped.gif
spike-cropped.gif (765.02 KiB) Viewed 1111 times

The above animated GIF created by recording the inputs in cuzebox, then playing the input recording back while telling cuzebox to record a video, then running the following commands to convert the video recording to a series of PNG files (takes up a lot of space!) and then making an animated gif of the frames I wanted, and then cropping that animated gif:

Code: Select all

ffmpeg -i capture.mp4 -vf fps=60 out%04d.png
convert -delay 33 -loop 0 out0{356..385}.png spike.gif
convert spike.gif -coalesce -repage 0x0 -crop 256x256+275+134 +repage output.gif
User avatar
Artcfox
Posts: 1382
Joined: Thu Jun 04, 2015 5:35 pm
Contact:

Re: Mode 3 Scrolling Game Engine Demo (WIP)

Post by Artcfox »

What are people's opinions on sprite flickering?

I was always opposed to sprite flickering, but felt that I needed to implement it just as a safeguard to prevent the blitter from totally starving the CPU in the worst case scenarios. It just doesn't look quite as good as not flickering, though when it comes time to design the actual game levels, I can space things out to avoid it, and I don't have to make the levels CPU/engine torture tests.
User avatar
uze6666
Site Admin
Posts: 4801
Joined: Tue Aug 12, 2008 9:13 pm
Location: Montreal, Canada
Contact:

Re: Mode 3 Scrolling Game Engine Demo (WIP)

Post by uze6666 »

Awesome work again Matt! You guys rocks keeping the scene alive with news games and demos regularly. Can't wait to see the finished game.
What are people's opinions on sprite flickering?
I was never too keen of it either. But I don't think it's a deal breaker if used sparingly. I grew with the NES so I'd say it even adds a bit to the retro vibe. :)
User avatar
Artcfox
Posts: 1382
Joined: Thu Jun 04, 2015 5:35 pm
Contact:

Re: Mode 3 Scrolling Game Engine Demo (WIP)

Post by Artcfox »

uze6666 wrote: Sat Jan 21, 2023 12:47 am Awesome work again Matt! You guys rocks keeping the scene alive with news games and demos regularly. Can't wait to see the finished game.
What are people's opinions on sprite flickering?
I was never too keen of it either. But I don't think it's a deal breaker if used sparingly. I grew with the NES so I'd say it even adds a bit to the retro vibe. :)
Thanks Alec! Yeah, I hope to only use it sparingly as well, though I did discover that it also cuts way down on the CPU usage, so I might have to use it a little bit more. I'm so happy that I've finally learned enough about the platform to create the type of game that I've been wanting to, ever since I discovered the Uzebox back in 2015.

The funny part was when I was researching different sprite flickering techniques, I had to wade through tons of results of people wanting to emulate sprite flicker in newer OpenGL games. :cry: Those posts make me want to reply with: "Hey, you should just make a Uzebox game, where the sprite flicker is real!"

My first implementation was the naive iterate through the list forwards, and then if you didn't get to draw everything, iterate through the list backwards on the next frame. It reserves sprites at either the top or the bottom of the list to draw the unicorn without flickering (depending on whether it is alive or dead and if I want it to be drawn on top of other things, or to have other things drawn on top of it).

Then I improved it a bit so it looks at the positions of each entity and player, to calculate the number of RAM tiles each needs (ignoring the RAM tile savings if multiple sprites overlap). That way it should always reserve enough RAM tiles for the unicorn even when it is the last thing to be drawn.

This works well, and I can have a lot of sprites on the screen, but when it always flickers the same two sprites and not the rest, it is a bit jarring.

My latest thought (which I haven't tried yet) is on any frame where I needed to toggle the iteration order, mix up the ENTITY array the same way a double egg beater would. Divide the active part of the list in half, and do a left circular rotation of the left half of the active part of the list, and a right circular rotation of the right half of the active part of the list. Normally only the sprites at the extreme ends of the list are the ones flickering every other frame @ 60 fps, but with this double circular mixer technique in addition to changing the iteration order every other frame, the sprites on both ends of the list will be rotated to the center of the list, while the center of the list is being rotated toward the extreme ends of the list, so any flickering should be evenly spread out among all of the non-main character sprites. (And if you really like to see flickering, with this double mixer technique it should be possible to flicker more than what the naive iterate forwards/iterate backwards technique allows. Not that I would do that in a game, but I would definitely do it as a torture test to see what my game engine could handle.)
User avatar
Artcfox
Posts: 1382
Joined: Thu Jun 04, 2015 5:35 pm
Contact:

Re: Mode 3 Scrolling Game Engine Demo (WIP)

Post by Artcfox »

So I got around to implementing different swizzling algorithms for sprite flickering that happen after the reversal of the list:
Due to how complicated the z-ordering stuff was, I went with a super naive approach, of just using memmove on the "in-use" portion of the entity array (12 bytes per entry).

This one just does a single circular shift:

Code: Select all

    if (l->numSpawned > 2) {
      ENTITY tmp = l->entity[0];
      memmove(&l->entity[0], &l->entity[1], sizeof(ENTITY) * l->numSpawned - 1);
      l->entity[l->numSpawned - 1] = tmp;
    }
And this one does the double mixer technique:

Code: Select all

    if (l->numSpawned >= 4) {
      ENTITY tmp;

      uint8_t llo = 0;
      uint8_t lhi = (l->numSpawned - 1) / 2;
      tmp = l->entity[lhi];
      memmove(&l->entity[llo + 1], &l->entity[llo], sizeof(ENTITY) * (lhi - llo));
      l->entity[llo] = tmp;

      uint8_t rlo = lhi + 1;
      uint8_t rhi = l->numSpawned - 1;
      tmp = l->entity[rlo];
      memmove(&l->entity[rlo], &l->entity[rlo + 1], sizeof(ENTITY) * (rhi - rlo));
      l->entity[rhi] = tmp;
    }
I still don't like the flickering. It might be that I have to remove my RAM tile estimation system, because it flickers before it needs to because it doesn't account for overlapping blits, and instead just after calling ProcessSprites() look at free_tile_index to know whether or not to reverse the list:

Code: Select all

extern uint8_t free_tile_index;
if (free_tile_index == RAM_TILES_COUNT) {
  // You used up every last RAM tile last frame, so you should at least iterate through the list in the opposite order in the next frame!
}
This might be the solution I'm after, to only flicker if we absolutely, absolutely need to. And maybe combining that with one of the above swizzling techniques, would be the golden ticket?

(I also fixed up the DissolvingTile_add function, so if you run out of space on the list of active dissolving tiles, it will automatically make the next tile in line to go solid expire immediately, so you won't ever be left with a glitched tile(s) that you can't fall through. That was a fun one to figure out, because the expiration times are for each dissolving transition in the sequence, not necessarily the reset transition, and further complicated by the fact that if you drop down onto a dissolving platform, you might land across a tile boundary, and have two tiles in the list with the same expiration time, and in order not to have any glitches if you reclaim one of those, you have to reclaim the other.)
User avatar
Artcfox
Posts: 1382
Joined: Thu Jun 04, 2015 5:35 pm
Contact:

Re: Mode 3 Scrolling Game Engine Demo (WIP)

Post by Artcfox »

The sprite flickering is very much improved using the free_tile_index variable to decide if we need to flicker or not!

What would be even better is if we could randomize the RAM tiles that are used for each individual 8x8 sprite. Even if it was just a static randomized LUT in PROGMEM, or RAM if it needed to be faster. That way instead of an entire 8x8 sprite flickering on a frame, odds are that only a small portion of it would flicker. To me, that would be the ultimate solution, because it is imperceptible when just a single corner of an 8x8 sprite is gone for 1/60th of a second, and it's back the next frame.
User avatar
Artcfox
Posts: 1382
Joined: Thu Jun 04, 2015 5:35 pm
Contact:

Re: Mode 3 Scrolling Game Engine Demo (WIP)

Post by Artcfox »

I am okay with this level of sprite flickering (see attached .uze file). Please share your feedback! (disable frame merging in cuzebox)

I had to give up on the ram tile estimation system, and for now I ditched the different Z-order for the unicorn when it is alive or dead, since I want to just let the kernel use the max RAM tiles possible, and only flicker if it runs out. To easily ensure that happens, I have to draw the unicorn first, and then the rest of the entities, and just let the kernel automatically limit the number of RAM tiles for the entities.

If you go to the bottom right corner of the level, and jump up enough to get the top butterfly on the screen at the same time as the bottom butterfly and the firebar, make sure you are misaligned, then jump (but end the jump before the bottom scrolls out of view) and fart at the same time, it can run out of RAM tiles and will need to flicker. It only reverses the iteration direction when the number of free_ram_tiles == RAM_TILES_COUNT after ProcessSprites() is called, so if you aren't exceeding the number of RAM tiles and two sprites are overlapping, it shouldn't change their Z-order every other frame.

If you are running my debug-enhancements branch of cuzebox: https://github.com/artcfox/cuzebox/tree ... hancements, outputs to the whisper port will also go to the console (I wouldn't be able to debug without that and the input recording I added!) and you can see when the sprite flickering happens because it will output @ symbols for each frame that it swaps the iteration order. There are a bunch of other debug options I can turn on, but the only other one that is enabled in this build is DEBUG_PRINT_FATAL_ERRORS. I had to play back an input recording frame-by-frame in order to see where the flickering is happening, because it seems to just be parts of 8x8 tiles that flicker, which makes me happy.

I also added a dissolving cloud torture test (more dissolving tiles than there is memory allocated for the dissolves, so it will have to reclaim memory from the proper undissolved tiles as needed).

I still have a long, long way to go though. I need to implement spawn eviction in case something wants to spawn, but you are out of spawn memory. Typically things despawn after you've traveled a certain distance away from them, but it would be nice if spawn eviction also happened because the level has to be designed very carefully for a purely distance-based despawn to work well, especially while also trying to max out the number of enemies on screen.

I would also like to make it so the falling spikes use tiles when resting at the top, and sprites once in motion. That should help reduce the number of sprites on the screen at any given time, and it will ensure that they never flicker while at rest, which looks very bad. And I have a long list of other ideas that I want to try out.

Here is what the tileset and map looks like:
tileset.png
tileset.png (10.42 KiB) Viewed 1034 times
Everything just comes alive on its own (after enabling the DEBUG option that uses the whisper console to automatically build two tiny files with PROGMEM data in them for the collectables). No level editor needed yet, just a drawing in GIMP.
Attachments
unicorn3.uze
sprite flickering, more enemies, and a freaking 6-long firebar!
(43.63 KiB) Downloaded 38 times
User avatar
Artcfox
Posts: 1382
Joined: Thu Jun 04, 2015 5:35 pm
Contact:

Re: Mode 3 Scrolling Game Engine Demo (WIP)

Post by Artcfox »

It was surprisingly easy to make the falling spikes tile-based when resting at the top, without modifying the main behavior at all, so it should be possible to make whatever I want to fall either tile-based when resting at the top, or totally sprite-based. Anvils come to mind...

I'm wondering if it would be possible now to have an entire row of falling spikes, since they only use 2 RAM tiles each when falling. They each have configurable trigger zones, so it's probably possible to time the last one to always nail you unless you break stride before you get there. :lol:

Also, my kids prefer a 6-long firebar where the fireballs are closer together. The farther apart ones just use more RAM tiles, so that's why I made them so far apart in the other build.

Attached is a build with tile-based falling spikes with the "normal" spacing for the firebar, placed right in the middle of all the action, so you can get even more things on the screen at once and actually see a little bit of flicker if you try. I still think this amount of flicker is acceptable, but I would love to hear what other people think.

Here is the sweet spot that I think causes the most flicker (bonus if you can get there fast enough with a falling spike still on the ground on the platform above, since it will still be sprite-based when it has fallen):
sweet-spot-to-cause-most-flicker.png
sweet-spot-to-cause-most-flicker.png (68.23 KiB) Viewed 1015 times
Attachments
unicorn4.uze
tile-based when resting at the top falling spikes, kid-approved 6-long firebar placed in the center of all the action to cause more flicker to see if it is still acceptable to people
(44.08 KiB) Downloaded 41 times
User avatar
Artcfox
Posts: 1382
Joined: Thu Jun 04, 2015 5:35 pm
Contact:

Re: Mode 3 Scrolling Game Engine Demo (WIP)

Post by Artcfox »

So I solved the Z-order problem, but it requires another small kernel modification to gain access to the BlitSprite function while keeping SPRITES_AUTO_PROCESS enabled. I just had to comment out the line that makes it static.

My goals are:
  • The main character must be drawn every frame in its entirety, without running out of RAM tiles
  • The entities should be drawn until every last RAM tile is exhausted, except for the ones used by the main character
  • Sometimes I want the main character drawn on the bottom of the Z-order, and sometimes I want it drawn at the top of the Z-order
My (first) solution is:
In order to ensure that we never run out of RAM tiles while drawing the main character, its sprite assignment should begin at sprite[0]. This will cause its Z-order to always be on the bottom (which is actually what I want when the main character is dead). To achieve the best possible sprite-flickering for the entities, they should all be drawn next until all RAM tiles have been exhausted. Then after calling ProcessSprites() which allocates RAM tiles as it blits everything, I can check to see if my main character is still alive, calculate how many sprites it is using, and manually call BlitSprite again for the main character sprites.

When the call to ProcessSprites happened earlier, it automatically reserved those RAM tiles, and since I didn't move anything, I know that calling BlitSprite again will not require any additional RAM tiles, it will only bring the Z-order of my main character to the top.

And here is what my main loop looks like now:

Code: Select all

  for (;;) {
    ProcessSprites();
    if (!p.dead) {
      uint8_t max = (p.attacking) ? 6 : 4;
      for (uint8_t i = 0; i < max; ++i)
        BlitSprite(sprites[i].flags,
                   sprites[i].tileIndex,
                   sprites[i].x,
                   sprites[i].y);
    }
    WaitVsync(1);
    bool usedAllRamTilesLastFrame = (free_tile_index == RAM_TILES_COUNT);
    RestoreBackground();
    ReadControllers();

    Player_input(&p);
    p.update(&p, &c, &dt);
    Camera_update(&c, &p);
    DissolvingTile_update(&dt, &c);
    p.render(&p, &c);
    Entity_updateAll(&l, &c, &p, usedAllRamTilesLastFrame);
    AnimateCollectables(&c, &l);
  }
A future enhancement would be to make a new function like BlitSprite, call it ReserveRamTilesForSprite(), and have it do the exact same thing as BlitSprite does, except have it skip the actually blitting part, so you don't have to pay the time cost of blitting all of the main character's sprites twice. Or maybe there would be a way to just use the real BlitSprite with a sprite flag set or something to tell it to skip the actual blitting?

Edit:
  • There probably is not a sprite flag free, but perhaps using a certain invalid combination of existing sprite flags could be used to trigger this?
  • Maybe I should just ditch sprite auto processing, and do it all myself? I already have the X, Y, tileIndex, and flags stored or calculated at sprite assignment time, so it would save me a whopping 96 bytes of RAM just to ditch the sprites[] array. I would have to implement my own MapSprite and MoveSprite function, but since there are totally blank tiles within the rectangular maps of my main character, implementing those myself would mean using fewer RAM tiles for the unicorn...
User avatar
Artcfox
Posts: 1382
Joined: Thu Jun 04, 2015 5:35 pm
Contact:

Re: Mode 3 Scrolling Game Engine Demo (WIP)

Post by Artcfox »

Well, I took the plunge and set:

Code: Select all

KERNEL_OPTIONS += -DSPRITES_AUTO_PROCESS=0
in my Makefile, and dealt with the flood of compile errors, because tons of things that I relied on were now gone.

No more:
  • sprites[] array (saving me 96 bytes of precious RAM!)
  • MapSprite2() function
  • MoveSprite() function
  • ProcessSprites() function
I just wanted to get it back to a state where it would compile, so the first thing I did was make my own empty functions called MapSprite2 and MoveSprite. I've play tested this so much that I can play it with an invisible unicorn, that doesn't matter for now. Then all of the places where I filled in the sprites[] array, I replaced with a call to BlitSprite, and I commented out the call to ProcessSprites() since it wasn't needed anymore. And it compiled! And when I ran it, all of the enemies moved around, and I could move my invisible unicorn all around the level collecting things and dying, albeit invisibly.

Then I had to work on writing a better replacement for MapSprite2 and MoveSprite. I always thought it was silly they were two separate functions, so I first decided to combine them and replace the sprite assignment with a call to BlitSprite. Then I realized I had 3 unused parameters, so I deleted them and rearranged the rest so it has the same function signature as BlitSprite, except instead of passing a tileIndex as the second parameter, you pass a pointer to a mega sprite map.

Sometimes a rectangular sprite map will contain an 8x8 section that is blank. This is super wasteful, because it both costs CPU time to blit them, AND they waste precious RAM tiles for absolutely nothing at all. There are two of them in the walk/run animations of my gassy unicorn:
blank-tiles-in-map.png
blank-tiles-in-map.png (11.26 KiB) Viewed 1003 times
I make sure that the first tile in my spritemap is all transparent, and I checked for that in my new BlitSpriteMap function, so I can skip the blit if possible.

Code: Select all

#define SPRITE_BLANK 0
void BlitSpriteMap(uint8_t spriteFlags, const char *map, uint8_t map_x, uint8_t map_y)
{
  uint8_t mapWidth = pgm_read_byte(&map[0]);
  uint8_t mapHeight = pgm_read_byte(&map[1]);
  int8_t x, y, dx, dy, t;

  if (spriteFlags & SPRITE_FLIP_X) {
    x = mapWidth - 1;
    dx = -1;
  } else {
    x = 0;
    dx = 1;
  }

  if (spriteFlags & SPRITE_FLIP_Y) {
    y = mapHeight - 1;
    dy = -1;
  } else {
    y = 0;
    dy = 1;
  }

  for (uint8_t cy = 0; cy < mapHeight; cy++) {
    for( uint8_t cx = 0; cx < mapWidth; cx++) {
      t = pgm_read_byte(&map[y * mapWidth + x + 2]);
      if (t != SPRITE_BLANK)
        BlitSprite(spriteFlags,
                   t,
                   map_x + (TILE_WIDTH  * cx),
                   map_y + (TILE_HEIGHT * cy));
      x += dx;
    }
    y += dy;
    x = (spriteFlags & SPRITE_FLIP_X) ? mapWidth - 1 : 0;
  }
}
After replacing all of the calls to MapSprite2/MoveSprite with calls to this new BlitSpriteMap function, it compiled and runs!!!

The totally crazy thing about this now is there are no more sprites, only blits, so all of my code that made sure I wasn't exceeding MAX_SPRITES is gone now, and all the places where I had to set the sprite flags so the sprites turned off, gone! So much code just vanished. I had to add an extra call to RestoreBackground in the beginning of my popup menu, and move the call to AnimateCollectables so it happens before any of the blitting starts.

I don't want to just gloss over that last point, because that is one of the really important things you need to follow when setting SPRITES_AUTO_PROCESS=0 and switching over from having a sprites[] array to just blitting:

Once you start blitting to VRAM, you can't change VRAM until you call RestoreBackground() again!

I was very fortunate that the only thing I needed to do was to move the call to AnimateCollectables up a bit.

I also made one other function that I call after RestoreBackground before blitting any sprites. That sets the same variables that the old ProcessSprites() did before it called BlitSprite:

Code: Select all

// Call this after calling RestoreBackground, but before calling BlitSprite
static inline void ResetUserRamTilesCountAndFreeTileIndex(void) {
    extern uint8_t user_ram_tiles_c;
    extern uint8_t user_ram_tiles_c_tmp;
    extern uint8_t free_tile_index;
    user_ram_tiles_c = user_ram_tiles_c_tmp;
    free_tile_index = user_ram_tiles_c;
}
Now if I want to change the Z-order, I can just call my p.render() function again after I blit all of the entities, though that does waste clock cycles, so I'm still investigating whether or not a modified version of BlitSprite that only copies the background tile and reserves RAM tiles is the best solution.

This is quite awesome, and I am so grateful that Jubatian rewrote mode 3 to make this all possible. I'm just sorry it took me this long to find the time to start wrapping my head around these fancy enhancements. The code size and memory usage both dropped quite dramatically, and this should allow for some fancy effects once I realize its full potential. Blitting single pixels on top of my sprites comes to mind. I always wanted to make the unicorn stick its tongue out a bit while eating the lollipops, but didn't want to waste PROGMEM space for that.
Post Reply