Heroes Of Loot

Java to Genesis conversion.

Day 0

Looked at the original dungeon generation source code and decided to move from Java to c# so I could add it in to my tooling.For now I'll be creating the dungeons on PC side, and perhaps move this over to 68000 later. 
It's going  to be a tricky conversion. I am thinking of refactoring all the Java code first, to make it more data-driven, so that when this works on PC , I can convert it to Genesis much more easily, using the same data. The main advantage of this is to be sure there are no bugs in the conversion, which will be hard to spot later.

Day 1

Started a new code project by copying GunSlugs codebase. Converted the Java dungeon code over, in part, and got that working in the tool, complete with being able to display the results.




Then got this exporting to 68k data tables and importing on to Genesis.
I added a handful of sprites, and also exported a background tile set. This is all using the new Winforms Snake Editor, which is a replacement for my Unity based engine. There were plenty of little bugs to fix getting this export working, like keeping PNG files locked when opening them (couldnt edit them at the same time).

On Genesis, wrote a simple map display - updating the entire screen at once - just to get things working. This is of course way too slow, and will get replaced when more things are decided upon.
Got the enemies spawning. Added some enums to the enemy sub-types to make it more readable. Added basic enemy code to chase player and collide with walls. 

Added bullets and bullet / enemy collision. (using code from GunSlugs, no modification needed)



Day 2

  • Written spawner code which just examines the monster list around the player, and promotes these spawn points to real GameObjects if they're nearby. Currently does ALL monsters per frame, but can easily be split in to multiple frames. This will probably need a despawner, but not sure how the game reacts to things going far away from the player yet.
  • Playing the original a bit more to get a feel for it, and making notes on all Monster types.
  • Added monster vs bullet health / energy code.
  • Added an ItemCollisionMap for pickup type items, for fast collision. Seems that sometimes there are multiple items in the same place, though, which is could make this redundant.
  • Added per-frame label support in the Sprite editor, which is also exported to the Genesis for each sprite, so, we get a bunch of things like SPRITE_MONSTER equ 2, and SPRFRAME_ITEM_CHEST equ 5. This way we can ensure the correct frame is used in code, even if the sprite order is changed.
  • Added in Wall Spikes, basic movement.
  • Changed the Graphics Mode on the Genesis to 256-wide. This helps give the game the chunkier  look of the original, I think. It also reduces the number of sprites we can display to 64 in total. Remains to be seen whether we can use this mode or not.
  • Added placeholders with graphics for some of the bigger enemies.


Day 3

    Added more monster sprites after playing through the game some more. 

    Explaining the sprite tool from yesterday: It now supports naming Sprites and Frames, which then get exported in to 68k assembler files as EQUates.


This produces the following on export:
SPRITE_CAGE equ 12
SPRITE_MEETSPAWN equ 13
SPRITE_MEETBOY equ 14
SPRITE_MEETBOYTINY equ 15
SPRITE2_TOTAL equ 16

;-------------------------------------------------------------

SPRFRAME_PLAYER01_0 equ 0
SPRFRAME_PLAYER01_1 equ 1
SPRFRAME_MONSTERS_SKULL equ 0
SPRFRAME_MONSTERS_1 equ 1
SPRFRAME_MONSTERS_GHOST equ 2
SPRFRAME_MONSTERS_3 equ 3
SPRFRAME_MONSTERS_IMP equ 4
which are then used in code like this:
HolInitSkull_Skull:
ENEMYSCORE 1,HolInitScoreFX_10
GO_SET_HEALTH_DUNGEONLEVEL_ADD 3

SETSPR SPRITE_MONSTERS
move #SPRFRAME_MONSTERS_SKULL,go_frame(a0)

Worked out what 'mPickups' are and added the gfx for those. They are perma items, but not sure what their use is yet. Added code for them. There seems to be one which isn't used.

Noticed that I sometimes get confused about which file I'm in. I have VS and Rider open, editing files in c#, java, and 68000. Sometimes I think I'm in c# when it's the original Java code.

Added Altars. The code seems only to support one type, while there are 4 gfx. Added the gfx, but might be able to remove those later.

Added Avatar placeholders. These seem to be quite a wide range of things, people in cages, springs, statues. I think the common denominator might be that these all have some kind of popup speech thing? This also gets me thinking, how the hell am I going to do a HUD in this, if we're using both playfields for graphics, and already run out of sprites.

All monsters now have at least placeholder graphics and individual code. We're not doing badly for VDP space, but there are a lot more sprites to add, including fx, player stuff, and UI. Here's the current VDP usage:



A lot of the original sprites are based on 12x12s, so there's a lot of 'wasted' space here. If we are running out of space later, one option will be to shrink some of these down to fit in a smaller Sprite size. Conversely, if we are fine for space, we could scale up some of the graphics to fit the frame, if that might make them look better ingame.

Looking at the player weapon code, that is going to have to change from the original. Not going to be finding the nearest enemy to shoot, but proper 8 way shooting ala Robotron. I'm thinking of two fire modes, one being shoot the way you are facing, and the other being shoot the direction you were facing when you first pressed the button. Implemented that, i think it works great, but might take some getting used to?
Wrote proper player movement code - using a pad-to-direction table, 8 directions. Then a lookup table for direction to velocity. Currently this uses a couple of multiplies for speed, but will change that to a further lookup table later when I know what speed range we need.

Split the player graphics up as we only need one player graphic in at once. Same with the player weapons. I only load in the weapon graphics used for the current player. (Though only implemented player 1 so far, the Elf)

Added popups for collecting stuff. Not linked up specific types yet.. more unknowns!

Implemented FlamerTurret baddie, first pass.



Day 4

Working on some editor side stuff. Fixed the palette issue with Player01 - but I really don't know what I'm doing regarding Alpha in Pro Motion NG. I seem to be doing the same thing on different images, but with different results. Anyway, a few 'delete alpha channel', 'use alpha channel' type changes seems to have fixed it for now.
Halved sprite export time, using LockedBitmap. Still loads more I can do there, but a couple of seconds it OK for now.
Generating loot from killed baddies
Added the powerups. Only two in this game, I believe - Runes. 
Ditched the collision map for pickup items (for now) - looping through them instead for player collision. This is because there were multiple items on top of each other, which I didn't realise.
Added the chests. They spawn something random.. which uses a random(100) - a very naughty divu instruction. I should change that to 1024 at some point, that would be close enough.
Generated multiple dungeons with level progression on PC side, and exported those. Re-wrote access to current dungeon using pointers, for a speedup and less mess.
Added key which gets generated after you've killed X number of baddies, and implemented exit door collision.
Fixed some issues with Palette2 png. Alpha wasn't working correctly.




Day 5

Scrolling! So just to get things working from the original map format, for now I've been creating the whole visible map area in ram every frame, then dumping it all to VRAM during the vertical blank. This is not practical in a real game because it takes up pretty much half a 60hz frame to update.

The usual approach therefore is to update scroll edges as they appear on screen, so for 8 pixel per frame scrolling, the maximum amount of tiles to update is nearly 60. This is actually quite fast to render, even with decoding the actual tiles from a meta tile map (in this case the hardware tiles are 8x8s, and the game map is 16x16s).

However, this game has a fixed map size of 32x32 (or 64x64 hardware tiles). The scroll area in VRAM is 64x32 tiles, so I'm trying a slightly different approach of only updating the top or bottom edges rather than the sides as well. Downside being that each edge is longer than would be the case with normal 8 way scrolling. I'm assuming there isn't much in it, speed-wise, and it leaves the code being a lot simpler, with just one edge loop to do.

VERY Naive implementation takes about 12 scans of CPU. Could easily halve this, and maybe quarter it later.

Added player sprite frames, and all 5 player classes. Also more info in the player class lookup table, collated from all over the place. 

Another pass on the normal enemy movement (Skull and its subtypes). Trying to make something similar to the original, but more efficient on the CPU, and trying to stop the clumping quite so much. Fewer sprites on a line = less flicker.

Added some enemies shooting at you.

With the scroll fix, everything is running nice and smooth now at 60fps, but there's a *lot* more to go in, so plenty of optimization left to do. Going to get everything in and working first though, then we can see what we need to do.





Day 6 (Actually a few hours over the weekend)

LIGHT MAP!

The original game uses a 64x64 character map to show areas of the map and hide others. For instance torches on the walls light up the area around them, some monsters emit lights, and the player lights up his surrounding. Luckily there's no line of sight in the original or I might have not even attempted this.

How to do it on the Genesis?

I didn't think that we could possibly do 64x64 map and update it every frame. that's 4k if we use a byte per entry, and just iterating that would be quite slow. So sticking with 32x32 tiles (each tile 16x16) gives us 1kb for each map.

This whole procedure has 3 stages:
1. Clearing out the map so everything is unlit.
2. Rendering the areas around lit objects to the ram map
3. Rendering the the tiles themselves to the VDP

The reason 2 and 3 are split up is because we can't update the whole VDP map at once, that would take up way too much CPU time. We update the light map just like the normal tile map, by updating edges as you scroll around.

Some objects, like torches, are static, so they can be written to another map in RAM, and instead of clearing out the working area, we can copy this area over instead.

So actually, each frame, rendering a light around an object requires the tile data to be written to the map in RAM, and also if that change is on screen, that particular tile needs to be updated in VDP.
Clearly this will cause problems, because there could be a LOT of tiles changing each frame. 

To cater for this, we have another area of RAM which is the Current Render map. We don't draw the light information directly to this, but to a separate area. 
Then, the two buffers are compared. Any changes are then written to the Current Render map, and changed tiles are written to the VDP if they're on screen. 
This sounds like writing directly, but with an extra step. But the reason we do this is so that we can update the Current Render map over time instead of all at once. I tried 32 tiles per frame, which seems to be fine.
This does cause some drawing issues, as you can see a little bit of lag with the lightmap, catching up over time, but I think it's OK.

All this effort gives us something like this.



So to recap. There are 3 RAM buffers, each 1k.
* StaticMap
* CurrentMap
* RenderMap
1. At the start of a level we render the lights in to the static map.
2. (Each Frame) Copy the contents of StaticMap to CurrentMap
3. (Each Frame) Draw dynamic lights in to the CurrentMap.
3. (Each Frame) Compare a section of CurrentMap with RenderMap, and update RenderMap with any changes
4 (Each Frame) in the VBL, take the list of changes from stage 3, and write the tile data for each in to VDP ram.

It works! But it's slow, taking nearly half a frame for all the stages put together. Haven't made any attempt to optimize anything yet though.

Optimizing the buffer copy is easy. Convert to movem.l commands, that's pretty much as fast as we can go.
Lets take a look at the actual 'light sprites'. First thoughts were that the sprites aren't that big, given that each 'pixel entry corresponds to a 16x16 tile. And there are feasibly 'transparent' areas.
I tried creating a list of offsets which we could step through and render each one. This handles transparent areas, as we don't even need any information for these.
eg.
    
LightSrprite_Torch:
    OFFSET -3,-3
    TILE -3,-3,LIGHT_1
    TILE -2,-3,LIGHT_2
    TILE -1,-3,LIGHT_ON
    (etc.)

LIGHT_X are different levels of lighting, so there's sort of falloff from the centre of the light source. The levels are viewed with different dither patterns of graphics, which mask the backgrounds and sprites to a greater or lesser extent.

This all works, and the code is fairly optimal. The 'TILE' macro stores a single word offset from the base pointer (ie. the centre of the sprite) 

The inner loop being something like this:
.loop:
move (a1)+,d0 ; A1 is tile render. d0 is pre calculated offset for the tile
move (a1)+,d1 ; new light value
cmp.b (a0,d0),d1 ; is the new light value less than or equal to the existing one?
ble .skiptile ; if so.. dont update this tile
move.b d1,(a0,d0) ; write the new tile data.
.skiptile:
    
The test and branch is there because lighting is additive. We don't want a semi-lit area to overwrite a fully lit area. This takes 54-56 CPU cycles per tile. 

We know it's additive, and we know how many 'shades' of light there are. So if we are clever with the tiles, we can optimize this a bit. If our light values are not 0,1,2,3,4, but instead 0,1,3,7,15, we can skip the test entirely, and just OR the current value on to the background value

.loop:
move (a1)+,d0 ; A1 is tile render. d0 is pre calculated offset for the tile
move (a1)+,d1 ; new light value
or.b d1,(a0,d0) ; OR the new light value with the previous.
.skiptile:

This is now 34 CPU cycles per tile.

There's not a massive amount we can do to optimize the Update part of the process. It's not too bad though. This has got us in to the realms of actually being possible. But still a lot more needed for this to be viable, given what else is going on per frame.

So, what else could we do?

We're currently using bytes to store each map entry. The 68k really isn't that great at dealing with having to shift bits around. It doesn't have a barrel shifter, so the greater the bit shift, the longer the instruction takes. But now we're ORing light values together, that gives us a great opportunity, but we need to start applying some limitations. 
We could use 4 bits per value, giving us 5 different 'shades' of light, or we could even go to 2 bits, giving us just 3 shades. I decided to go for the latter.

We need to change the Light Sprite format. I will need to export this from a tool at some point, but for testing, I have something like this:

L0_A equ LIGHT_OFF<<6
L0_B equ LIGHT_OFF<<4
L0_C equ LIGHT_OFF<<2
L0_D equ LIGHT_OFF<<0

L1_A equ LIGHT_2<<6
L1_B equ LIGHT_2<<4
L1_C equ LIGHT_2<<2
L1_D equ LIGHT_2<<0

L2_A equ LIGHT_ON<<6
L2_B equ LIGHT_ON<<4
L2_C equ LIGHT_ON<<2
L2_D equ LIGHT_ON<<0

LightWrite_Torch:    
    dc.b    L0_A|L1_B|L1_C|L1_D,L1_A|L1_B|L1_C|L1_D,L0_A|L0_B|L0_C|L0_D
dc.b L1_A|L2_B|L2_C|L2_D,L2_A|L2_B|L2_C|L2_D,L1_A|L0_B|L0_C|L0_D
dc.b L1_A|L2_B|L2_C|L2_D,L2_A|L2_B|L2_C|L2_D,L1_A|L0_B|L0_C|L0_D


So this is using 2 bits per 'pixel', shifted to the correct place in the byte.
Beautifully, as we are now ORing the values, we can have transparency in the sprite format without having to branch around it. 

.loop:    
move.b (a1)+,d0 ; Get 4 light values
or.b d0,(a5)+ ; OR all four at once in to the buffer

Now we're down to 20 CPU cycles for four tiles, or 5 CPU cycles per tile! The overhead for looping is also less as we're not needing to do so much of it.

The data buffers are now 1/4 of the size too, so making copies of those is much faster.

The downside of this is that we have to deal with shifting the 'graphics' to render at multiples of 2 pixels instead of bytes. I'm not going to do this shifting in real time, instead I'll pre-shift the sprites and store multiple copies of them on the tool-side. I might even store pre-clipped versions of each, as they don't take up much space at all. (clipping, yuck!)

This does complicate the actual tile render a little, as we're having to do some shifting, but it's not a lot more work. I simplified the Update loop by just rendering a single (or double) row of tiles each frame. This code is already written as I use it for updating the map edges.

This is getting pretty fast. So much so that I tried changing it to using a 64x64 map instead of a 32x32 map, giving better resolution for the lighting. There's quite a lot more overhead now as we're back up to 1kb per buffer. But we don't need the extra intermediate buffer any more, saving some RAM space.



Notice that the lights are only updated at 4-tile boundaries in the X axis.. this is down to the current lack of pre-shifted graphics.

This is still fairly time consuming. There are more optimizations which can be done, like pre-storing the code for each sprite render, instead of just the data. This will help a lot, as will being more careful with which parts of the map we update by tracking changes. This can be done later in the project, when we have a better view of what actually is needed. Could end up with the worst case scenario being worse than if we didn't do all the extra testing. That's the important part.

The best part about all this is that I suspect it'll all be wasted, and we'll use a standard 'light around the player' map on SCROLL_A, which will look a lot better, and be much quicker! The only reason we have this complicated setup is for showing lights in other parts of the map. Do we really care about that?

Day 7

Consolidated all of the Day6 work and tidied up code, removing older test versions.

Added Unspawning. This unspawns any active objects when they go outside a certain range of the player, helping to keep active GameObjects to a minimum. Previously, you could walk around the whole map, not picking up any treasure, and slowing the processing right down because there were too many objects to process.
Now I unspawn objects and respawn them when you go back to their area, unless they are marked as being permanently dead, like if you've killed a monster or picked up a gem. 

There are perhaps 200-300 objects on later levels (maybe more, I'm not sure) . I could spacially partition the object list at some point, but currently I'm looping through, checking 16 spawn points per frame. This seems to work fine at the moment, but might need to be adjusted later, along with the spawn radius.

Overall this is now much quicker than it was previously. The game update doesn't seem to be going over about half a frame currently.



Day 8

Big tidy up. Removed lots of GunSlugs code now that this game is not reliant on that any more. Moved Collision Map to RAM, so that it can be updated ingame. Freed up lots of RAM use from previous game. Now have about 16k free.
Added the export of Compound Sprites from the editor. I would love to go in and write more editor code for this, but we only have a few Compound Sprites in this game. I'll wait until I do a game with more before working on that.
Getting on with implementing various creatures:
* Fixed timing on Wall Spikes, and wrting to and clearing collision map
* Added Floor Spikes. The original uses 3 separate GameObjects for a group of spikes, but I've turned this in to one, and added frames which make it look like it's more independent. Should randomly flip the frames as well, for variety.
* Added basic movement for Cyclops, Minotaur, and Summoner. And added their special skills. Not sure what sprite Summoner spawns. I think I've got some of the monsters mixed up.
* Coded up MeetSpawn and MeetBoys.

Day 9

Added collision editor to sprite frames.Using that now ingame, so can adjust collision in the editor and export and run. Sprite export is taking too long though.. something silly going on there. I think it's calculating something for each separate sprite which should be cached.
Sprite export down to ~300ms. Yes. It wasn't using the cached version of the palettes. No need to multithread this yet, anyway.

Implemented ROCKO's jumping. Just wanna post this up to record how beautiful state handling is in 68000: This makes Rocko pause, jump, land, and kill stuff. Simple looping state machine.
;--------------------------
.initstate_pause:
move.l #.updatestate_pause,go_updatefunc(a0)
move #ROCKO_PAUSETIME,go_timer(a0)
move #SPRFRAME_ROCKO_PAUSE,go_frame(a0)
rts
.updatestate_pause:
sub #1,go_timer(a0)
bgt .r
;--------------------------
.initstate_jump:
move #SPRFRAME_ROCKO_JUMP,go_frame(a0)
move.l #.updatestate_jump,go_updatefunc(a0)
move go_y(a0),go_starty(a0)
move.l #ROCKO_JUMPVEL,go_dy(a0)
rts
.updatestate_jump:
ADD_DY_GRAV
move go_starty(a0),d0
cmp go_y(a0),d0
bgt .r
;--------------------------
.initstate_destroy:
move #SPRFRAME_ROCKO_LAND,go_frame(a0)
move.l #.updatestate_destroy,go_updatefunc(a0)
move #ROCKO_DESTROYTIME,go_timer(a0)
move.l #HolRenderLightRing,go_renderfunc_after_main_render(a0)
GO_SET_SCREENSHAKE ROCKO_SCREENSHAKE_TIME
rts
.updatestate_destroy:
bsr DoPlayerDamageInRange
sub #1,go_timer(a0)
bgt .r
move.l #0,go_renderfunc_after_main_render(a0)
bra .initstate_pause
.r rts
;--------------------------

go_updatefunc is a pointer to the Update function which gets called each frame. Modifying that pointer keeps track of state in a neat and tidy way, in my opinion.

Added writing map change to VDP when Rocko lands.. Moved scroll map to RAM for this purpose. Very similar to the light map code.

Fixed a bug which caused half the enemies not to generate! It's a bit harder now.

Added a basic HUD, and coded up the death / progress code. Can now play the game properly.

Finally, wrote the tool code to export pre-shifted versions of the Light sprites. Now they follow the player round nicely, less imagination needed to see what it could be like!




RECAP

I played through the original game a few times to see what's left to do:

  • People in cages
  • Different room types
  • Cloak of invisibility
  • Quests
  • Mushroom monsters
  • [Done] XP / levels
  • XP graphics
  • Different background tiles
  • Regeneration potion
  • Inventory
  • Secret areas
  • Switches
  • Lens of secrets
  • Title / Char select / Game over screens
  • Intermission screens
  • PLUS LOTS MORE

Further possible Megadrive enhancements needed:

  • UI - software render in to large sprites instead of using lots of smaller ones
  • Torches - update those using a new tile graphic and DMA the animation per frame
  • Potions etc. pickups. Place those on a scroll layer, using a map. Try either foreground or light layer. This would save a lot of sprites and CPU time. Also allowing tile based animation.
  • Lots more optimizations to be done. Start with scroll.

Day 10

  • Collated all the data I could find for all the players' weapons, and found all the original graphics, and implemented them.
  • Added cages for avatars, random avatars, and avatar collision. Also writing / clearing collision map.
  • Spent some time tidying up the CompoundSprite editor. There was no need for this, as it's got hardly any use in this game, but while I was in there, I added a lot of new features, and some parity with the normal Sprite Editor. 
  • Added a bit more niceness to Sprite Editor - better Collision Rect editing (also added to CompSprs), other things like viewing multiple frames at the same time, etc.
  • Implemented export for other dungeon types - Quests, Shops, CoinRooms, Arenas, ItemRooms.
  • Implemented 5 different normal dungeon backgrounds (shown at random) and special ones for Quests and Shops
  • Partially got switches working, writing to the map when trod upon. However I think I'll need these to be always active GOs, which I don't want. Will need to store some state, and for this I need to change the JAVA generation code. Will tackle it later.
  • Generated secret rooms. No code to activate them yet

Day 11

Optimized background scroll update using lookup table. Much simpler now. Still doing two rows at a time though, so could be twice the speed if we're never moving more than 8px a frame.

Changed the way torches are done. Previously these were sprites, which had to be updated each frame. Actually, they were spawned on and off, and to keep them visible I would have had to make the sprites AlwaysActive, which is even more wasteful.
Changed this so it instead overwrites a tile in vram for the torch animation. 
The downsides are that 1. All the torches animate at exactly the same time, 2. Takes up a bit of time to DMA these across every frame. 3. Need to add these torch anims to the tileset, taking up some colors.
The upsides are that the torches are always visible, there's no limit to the number of them, we need fewer GameObjects, therefore freeing up RAM and CPU time. This is an overall win.

Fixed a bug with caged guys respawning.. it was not marking their spawn list address as no-respawn if you scrolled away from them after rescuing them, and before they were deleted.

Tower works now.. Appears when all switches are activated.. gives you XP for a bit, then dies.

Write Quest System stuff ingame. Hugely simplified the system compared with the original, or it would have been quite painful to implement. Each GO has a go_quest_item variable, and on death or collection it calls the Quest system which compares that with the global current quest item.

Added global control for regeneration and cloak.

Cant quite work out how the cup of life works.. something happens when you have it near the fountain thingy. specialItemBar in original JAVA code. Basically adds health though. Implemented a basic version of it.

Lens of secret: So, normally, secret rooms show themselves when you get close by to a secret area 'door' - and with the lens of secret they appear all the time. Not sure how to do this on Genesis, it could be quite tricky. Will ponder!

Resurrection Ring: that is like a continue. Implemented it so that if you die with the ring, you spawn a tombstone, lose the ring, and go back to full health

Split up the scroll update in to upper and lower halves of the 2x2 tiles. Pretty much doubled the speed of the function. Could save another couple of scans perhaps if pre-calc all the address offsets at start of game. Yep, well maybe a scan. But good savings if it's in VBL

Removed a load of unused things from the GO structure. Saved nearly 7k. 22k free RAM at the moment.

Fixed a bug with Lightning Rune not damaging the correct objects

Added in first pass Title and CharSelect screens. Added a single Sprite sheet png for all UI in Pal1.
IS_FINAL_GAME now takes you through the correct sequence and selects the right character. 




Day 12

Optimized the Lightmap scan 'render' function so that each 4-tile cycle now takes 86 cycles instead of 248 cycles. This is still the same speed if run in scree-time, because the VDP FIFO is slow enough to make up the cycle count. However shifting the scroll functionality to VBL gives us a massive speedup. Maybe saved 20 scanlines of overall time.
Did this by generating a 256-entry lookup table for each byte value, storing 4 tilemap words for each entry, which can just be dumped in to the VDP data register.
Starting to have some confidence that this will run at 60fps even with the lightmap.

Secret Rooms done. I thought this would be quite tricky, and it was. Slightly changed the way the original works, so simplify it.
Changed the dungeon generation for this. Added new versions of items - ITEM_SECRET. These get generated, made invisible, and on update test a global flag for the visibility of the secret room. When it's been found they revert to the normal code for the items. This saves having to put the extra code in EVERY item.
Removed one of the Secret doors, and just used the entrance door to test whether you've found the room or not. 
I don't update any graphics for the secret rooms, just let them go through the wall. I don't have any priority setting for the background tiles, so the player is just going to walk over the wall.

Working out a plan for different GameObject lists. Still using the ones from Gunslugs. Separated off Treasure in to its own list, as i think that these can probably be allowed to not spawn occasionally. If it's a problem I can put important ones in to a separate list so they will always be activated. 
27k free RAM now. (I'm making sure there is plenty of RAM available so that I can save the entire state when entering a shop or other room) It's going to be quite tight.

Thinking about this, it seems much simpler to have separate screens for the Shop and Quest rooms, rather than having an actual dungeon system for them. All they do is very simple user interaction.

Therefore I added a system for having OverlayScreens in the middle of gameplay. This is for use with the Inventory screen as well as the shop and quest. I store another sprite list just for overlay screens,so I don't need to store copies of the normal dungeon data now. 
Also converted the pre-game state code to use the new UI sprite list.

Added in temporary shop and quest screens, and hooked them up ingame to the shop and quest doors.

Day 13

Added in sprites for different doors, and standardized door collision code, now that I understand what the door types are.
Fixed up all the logic for the shop screen. Just needs to look a bit nicer!
Separated out the 'collect' function for all items, so to re-use between the shop and collecting things in-game.

Playing with some ideas for simplifying some code conversion. Some of the Java logic is convoluted, and hard to convert to asm safely. Trying out RPN parser ideas:
;if (getRandom(48)>24 && ((activePlayer.playerSkillLevel>4 && myWorld.dungeonLevel>4)|| myWorld.dungeonLevel>12) ) myWorld.currentQuestItem=15; // cyclops
Logic1:
PUSH P_VAR_GREATER g_hol_dungeonLevel,4        
AND P_VAR_GREATER g_hol_playerSkillLevel,4
OR P_VAR_GREATER g_hol_dungeonLevel,12
AND P_RANDOM 128
TRUE
RESULT 15
END
Something along these lines, perhaps, though it still gets complex.

Added basic Quest generation, and displaying that in the Quest screen.

Added better shop and quest screens using the original tile maps. 

EverDrive has arrived, so time to try this out on a real device. Ah, but I have no micro SD card. How can this be?

Day 14

    Fixed some palette issues with the UI sprites. Still something I don't understand about Pro Motion though. This screen is not showing the purple background, and I don't know why. Fixed it by taking a copy of a working sprite screen, and copy pasting the UI sprites in to it.

I admit I've been putting it off a bit, but time to work on the final monsters behaviour now. There are some monsters I don't have generated, I think that's to do with the dungeon skill level or something.

All monsters now in, I believe, and working. Removed the 'delayed' monsters from the original, which means there are even more monsters active in this than in the Java version!

Later levels are going way over the 80 sprite limit, and also dropping to 30fps. Just too much stuff going on. Added a counter for number of objects active of different types, so i can easily limit the amount. This works well for now. Might have to make that adjustable depending on dungeon level. I hope not as that would be very hard to balance.

Thinking again about other optimizations. I'd really love to put 'items' on tiles. This would save a lot of CPU and sprites. I think I might be able to add them in to the dungeon tile sets. Problem really is a lack of colors. Can't use a separate palette for them if they're in the tile set as the floor colors won't match the background tileset.

Fixed a bug with the scroll area not being initialized properly if starting near the bottom of the map.
Fixed scroll not being reset properly when coming out of Shop screen (etc.) - it was stupidly centering the scroll on the player's start position, not the player's actual position.

Looking at sprite list optimization. Next step is to look at cached sprites again. I do have the memory to store these now.

Day 15

Sprite optimization pass 1 done: 

My 'standard' sprite system is slightly slow just to make it simple and organized. 



Sprites are grouped first by Type, and then by Frame. Each Type contains any number of Frames. For instance we have Monster type, and in that we have frames for each of the different monsters. Both Types and Frames are able to be labeled, and use directly in the assembler code.

In-game, sprites are allowed to be uploaded to VRAM on a Type basis, so I can't just use individual frames from a Type. (I could add this, but it's not needed yet)

Each Scene in the game can choose what sprites are used, and where they sit in VRAM, so it's impossible to store that data offline. When setting up a scene, positions in VRAM are saved for each Type, in a global table.

When the game itself uses a Sprite Type / Frame, it needs to do a couple of lookups and shifts. The render function also needs to lookup a frame in a table, which can slow the render code down a bit. This is a tradeoff so that we can easily use Frames in the update functionality, for instance an animation which uses frames 0,1,2,1, repeating.

Caching frames makes the render part a lot faster for each sprite, but makes it less flexible. Changing sprite attributes etc. at runtime is much harder to do with cached sprites. It's not as fast as I would like, as we still have to calculate scroll offsets, etc. Currently a cached sprite takes 124 cycles vs 192 cycles for non cached. But there's also the overhead of testing for cached sprites. 

This also takes quite a lot of RAM up to cache frames. But we're ok for RAM now, so this is an overall win.

Other stuff:
Added totalKills and totalLoot counters for display in the inventory and game over screens.
Combined cages and trapped people in to single sprite, saves some CPU and hardware sprite count at the expense of VDP space
Added blue flame animation when rescuing someone, and they leave a chest behind.
Added simple XP orb. The original has hundreds of these, but i don't think I will!
Added all items to inventory screen.
Displaying XP, Dungeon Level, Loot, and Monsters Killed on Inventory screen.

HUD.
Been thinking about how to do the HUD for a while. I wasn't sure how many sprites we'd be needing for the actual gameplay, but I now know that whatever the number, it's not enough. Only 64 sprites available, so the HUD has to be characters. I'm using the Window, and hopefully only 2-3 lines will be enough at the top, as don't want to waste too much screen space. 
Side window would probably not be so bad actually, but might not be enough space to print up all the info needed. It's worth a try though.
Written a new number renderer for hud tiles. Finally got round to writing one which doesn't need leading zeroes, I've been so lazy about that!.

Day 16

Nothing much interesting today. Just getting things done.
  • Updated FM music playback (which now works on real hardware)
  • Purple monsters shooting through walls.
  • Cloak of invisibility graphics and monster behaviour.
  • Added a text queue system for HUD
  • Added in all the popup messages I can think of. Eg. You Found a Fire Rune
  • Added display for current Quest. Made a bunch of HUD icons for the current quest objective.
  • Added Game Over screen.
  • Added in special MeatBoy graphics for those spawned from MeetSpawns. This had been confusing me as the same named object does different things.
  • Added mini meetboys when you kill a non-spawned meetboy
  • Added minutes / seconds Quest Timer
  • Popup for Locked Door
  • Fixed the graphics for Wall Sprites so they appear not to go through the backgrounds now.
  • Added FX for player bullets hitting walls
  • Fixed Lightning Rune so it's more like the original. Still might have to flicker it a bit as it uses four sprites.
  • Added Level Up effect for player.
  • Sorted FX layer so it appears in front of everything else
  • Added a CompSpr anim for the orbs. Need to refine this, but it's proof of concept.
  • Removed old sample based music.
  • Plus lots of little things.

Day 17

I wrote onion-skinning code for the Compound Sprite Animator tool, and made a lot of quality of life changes to the tool, as this is the first time I've really used it.
Added some more Orb animations. I do think this looks rather good now, for very little CPU cost.

But mostly this was a day of optimizations.
  • Not showing a second background layer in the shop and quest screens (added a blank tile function)
  • Sprite code: Removed sprite bin sort code, which was there from Gunslugs, and not needed here. That shaved off 3-4 scans or so
  • Micro optimizations for the draw itself. Freed up a register, so removed two Swap instructions from each sprite. This tiny thing saves a coulple of scans on a busy level.
  • HUD takes about 8 scans to update. Not done it yet, but obvious improvement is just to split it up over multiple frames.
  • Collision: Again, swapped out the optimizations from Gunslugs to something more appropriate for this game. No need to create collision lists now, as the GameObjects are pretty much separated in to useful lists now anyway. Removed a lot of complexity, saved CPU and even saved some RAM which stored the pointer lists.
  • Had a go at speeding up the GameObject update. There appears to be less I can do with this.. the chase player code has to be there. I split up the chase code so that it only needs to calculate the player offset once every four frames. I spread this out based on the current frame timer. I could not see any difference in the actual AI from playing the game, but it's saved a few scans of CPU.
  • Split the player bullets in to their own GameObject list.
I was concerned to see that >64 sprites are being used all the time on later levels. Wrote some code to filter the different GameObject lists so I could track down what's going on. This is unsustainable so it needs a fix.
Turns out that (playing level 30) there are sometimes over 32 Treasure GameObjects active at any one time. (I set a max limit of 32 of these, and that's overflowing). That's also 32 sprites out of 64 available. 
I've mentioned this before in the blog, but i had to bite the bullet and convert them to background tiles.
I can't fit the colours for this in to the background tiles sets themselves, so using one of the sprite palettes for it. This means that the treasure won't really match the background floor tiles very well. I made a new set of graphics for these, so they fill up as much of an 8x8 cell as possible, to hide any mismatch with the backgrounds. 
(possibly, could store one set of sprites for each background tileset, to better match the colours. We'll see)
So, now, instead of rendering sprites for each active Treasure item, it writes in to the background tile map in VDP, and when you collect the Treasure it writes back the original scroll tile. I'm doing this on an 8x8 tile basis instead of the 16x16 tilemap which the game uses.
For spawned treasure, like that when you kill monsters, I'm still using sprites. 
This is all sort of hacked in, in case I need to revert it. Will make the code much cleaner when it's all finalised.

Day 18

Tackling the random generation of Monsters now. Up to this point I was generating them in my Editor tool, but that could never work for the final game because what's in the level depends on the current state of the game / character.
What I'm doing now in-game is generating a list, the same format as that previously generated offline, based on the original Java code. This is quite tricky, there are lots of clauses all over the place, and I'm sure I have made some mistakes. However it's working quite well, and seems to be similar to playing the original.
I still have some bugs with things appearing in the wrong place, though.

I optimized the way we find space to add enemies. The original does random searches through the 32x32 map until it finds something. I am wary of this, it's potentially dangerous code if your random number function favours speed over accuracy! (I don't trust it). 
Instead I make a list of all the free areas in the map (just walking through all the cells, and adding the XY position of each space to a list) . Then I shuffle this list, which gives me all the free cells in a random order. So when adding a new monster I can just use the next position in this list, incrementing the current pointer.

We're still using the 'keep iterating with random values until we find what we want' approach in other areas of the generation, which is why it's still slow. I think it can take up to half a second between levels. This isn't terrible, but a different approach would make it instant, and more reliable. I will ponder this!

In other news, I converted 95% of all GameObjects over to using cached sprite frames. There is an issue with some monsters flipped frames which I haven't decided how to approach, but other than that we're seeing a big speedup in sprite rendering times.

Added XP and health bar code to the HUD. Using a pre-generated list of offsets for each one, it's nice and fast, apart from the divide needed to calculate the values!

Removed a few unused bits of code, now that I understand what the game's doing. Removed some sprites as well, freeing up a little space.

Here's the current state of it. There's quite a lot going on now.




Day 19

Bits and pieces day.
Fixed a bug with the clipping of the light map sprites which was writing in to random memory.
Fixed a bug which deleted a sprite list when returning from Shop / Quest screens
Split the HUD code up in to four chunks, called over a series of frames. This saves a few scans of CPU time.
Spent some time fixing an image exporter problem in the Editor tool, when an image is re-used. The caching code has gone wrong somewhere. I will fix it in the next project. For now, it's truly hacked.
Created a font for the HUD, with black background.
Created new graphics for the XP bar.
Finished off the HUD code, I think it's all there now. Might want to move things around a bit though.

The main event today was writing all the quest code to match the Java version. This is tricky code to get right so I expect there are some bugs in it. Anyway it's all in, and seems to work.

Fixed XP increases to match the original. 
Fixed the shop so that it gives you a random item.

Day 20

Even more bits and pieces.
  • Fixed problem with big monsters not appearing on later levels.
  • Put Flamer Turret in to Palette2, as it has more colours available.
  • Went through all sprites and fixed their origins.
  • Fixed all collision boxes on sprites
  • Fixed missing exit doors.
  • Added collision to enemy bullets. Naive system as there aren't that many at once. Might push enemy bullets in to their own list if that improves performance later.
  • Fixed palette on Game Over screen.
  • Updated Inventory screen so that it shows only the items you're carrying. Added numbers to some items.
  • Re-converted all the 5 Tile Maps, hopefully better colour usage. Added animated torches to all Tile Maps.
  • Fixed monsters' collision so they don't walk through hidden areas.
  • Fixed Character Select screen and converted to 256 wide.
  • Added bullet shooting speed per-player class.
  • Fixed bug to allow the correct amount of monsters per level.
  • Fixed missing door graphics on generated maps.
  • Added some PSG sound effects.
  • Removed wall spikes from early levels.
  • Fixed Hidden Items so they use the same system as normal items, saving 9 sprites from being used when near a secret area.

Day 21 - Beta1

  • Fixed some issues with the Quest display on the HUD. Was getting some random characters. Turns out I wasn't clearing out the collectedCount between quests. 
  • Added fade in and out of Shop, Quest, and Inventory screens.
  • Access inventory by pressing START whilst playing
  • Characters were starting at one level higher than they should be. Weird indexing from 1 showing its ugly head.
  • Added some colour to the ingame gems.
  • Added generic code to reset the GO lists, and calling that now in the UI screens rather than generating the lists again.
  • Fixed some graphical issues with some sprites - missing black edges.
  • Fixed Hidden Treasure appearing for one frame when generated.
  • Unspawner wasn't working correctly - for some time, surprisingly. It was using the spawner current index rather than the unspawner current index
  • Finally added a variable width number renderer!
  • Stopped player bullets colliding with the wall when first shot.
  • Remapped all the Monster subtypes so they are now indices rather than magic numbers. Now the Spawner code can do a constant time lookup rather than searching through a list each time. This is now much faster in worst case scenarios.
  • Reduced the amount spawned / unspawned each frame. It doesn't seem to make any difference
And finally- fixed the one horrible bug which was worrying me. Some levels had monsters spawning in walls, and on levels after that, nothing was being generated, everything was broken!
Turns out I had a Clear function in the level generation code, but I wasn't calling it! So some weird stuff was happening over time. So glad the fix was this simple, as this was quite a scary bug.

Day 22 - Beta 2

  • Fixed flipped cached sprites. This adds a little CPU at render time, but no more at update time. Nothing to worry about though.
  • Actually awarding Quest Items now.
  • Added screen shake. Made a lookup table for this, which is 8 levels of pre-limited random numbers, so can fade off the shake without adding any CPU penalty.
  • Show the number of special items on Inventory screen.
  • Fixed background for Character Select screen.
  • Fixed issue with number of *updated* sprites going over 64 without adding any extra CPU time. Allocated a slightly bigger buffer so that we can overwrite multi-sprites without incurring a penalty.
  • Added high score
  • Implemented Save Game
  • Implemented Medallion and Fire Ring
  • Changed Score and Highscore to BCD, which is much faster to render with larger numbers. Now supports 8 digits.
  • Added more per-class stuff which was missing. Damage dealt with weapons, etc.
  • Added switches back in.
  • Added a bit of error / memory checking on dungeon generation.
Started some general refactoring. Fixed some annoying Font render nonsense which has been there since much earlier projects!

Adding other Dungeon Room types: Items. Coins, Arenas. I'm storing off 3 different pre-generated levels, and adding the monsters in Genesis-side.

Day 23

  Sprite sorting. 
First of all, I only really need to y-sort one of the lists of GameObjects, namely the one with monsters and player. 
The others generally don't have much overlap, and they're drawn as layers:
1. Treasure / Pickups. Always at the bottom.
2. Player bullets.
3. Enemies / Player etc. - this is the only one I need to sort.
4. FX.

Doing a full sort each frame is way too expensive. It's also complicated to keep sorting over time when objects are being enabled and disabled frequently, as in this game. 

So, decided to keep a pointer list of ALL of the objects in the list, not just active ones, and handle the worst case scenario. I'm still sorting inactive objects, but this means I don't need to do any active tests per object, and can keep track of objects over time without adding / removing them from a list. I am pretty sure this is the quicker approach, especially when a high number of objects are active.

First pass. A naive sort.
    move.l (a4),a0                  ; first obj pointer
move.l 4(a4),a1 ; second obj pointer
move go_y(a0),d0
cmp go_y(a1),d0 ; compare y pos
bge .next
move.l a1,(a4) ; if swapped, swap the pointers around
move.l a0,4(a4)
.next:
lea 4(a4),a4 ; next pointer in list
dbra d7,.lp
 
This takes ~80 cycles per loop for no swap, and ~110 cycles per loop when pointers are swapped. 
Measuring it in game, it takes maybe 10-12 scan lines, or 1/20th of the frame time.

CPU is tight, so a little more optimization:
    move.l (a4)+,a0
move go_y(a0),d0
.objectloop:
move.l (a4)+,a1
move go_y(a1),d1
cmp d1,d0
blt .swap ; branch on swap, as no-branch is fewer cycles.
.noswap: ; move the 'current' pointer to the 'previous' pointer, and same with the y pos, so we don't need to fetch em again
move.l a1,a0
move.w d1,d0
dbra d7,.objectloop
rts
.swap: ; if swapped, we don't need to exchange current pointers, or positions, as they're already swapped, I think
move.l a1,-8(a4)
move.l a0,-4(a4)
dbra d7,.objectloop
.exit:
This takes more like ~58 / ~84 cycles per loop. I'm only reading the data for each entry once, and swapping the registers every update. Could go down some more with a little unrolling, as we wouldn't need to swap the registers. It's a little more complex to code though, so I'll stick with this for now.

So, much better timing in-game, but I tried just sorting half the list every other frame. Almost halves the CPU time per frame, and I can't actually see any difference at runtime, as it's all so hectic anyway.

When objects first appear they will likely take a few frames to start sorting properly, but for active objects, there's barely any sorting delay as they dance around each other. Nothing I've noticed, at least.
Down to maybe 5-6 scans of CPU per frame now for the sort. It's worth it as it does look a lot better in game now.

Other stuff today:
For special rooms I have made a different Spawner pass. I generate all the sprites at the start of the level, and disable spawning and unspawning. There's no light overlay in the special rooms, so without this you can see the creatures spawning in and out.
Special rooms are now all working too. But they're not yet linked up in the overall flow.
Added HUD text for all the special pickups.

Played the original game, and FINALLY found a Cup Of Life, so now I know what it does.

Day 24 - Beta3

  • Implemented Cup of Life / Cup stuff.
  • Fixed sorting on Well / Tower
  • Started Cutscene system
  • Fixed collision writing to colmap on a few objects.
Cutscenes:
    In the original these are handled with large lists of if /elses which would be dangerous to convert, so I'm again using a Macro'd command list. I've done this before in other games, but implemented them here in a slightly different way, which I think is a bit neater.

This is a simple example. Four characters are generated, and they move on to the screen.  (there is no Y movement in the cutscenes, so Move commands are always X only.
Cutscene_World2:    ; easiest job
CS_AddObjAt Cutscene_Avatar1,-40,100
CS_AddObjAt Cutscene_Avatar2,-30,100
CS_AddObjAt Cutscene_Avatar3,-20,100
CS_AddObjAt Cutscene_Avatar4,-10,100
CS_Move CS_AVATAR3,100
CS_Move CS_AVATAR2,100
CS_Move CS_AVATAR1,100
CS_Move CS_AVATAR0,100

CS_WaitTime 20
CS_Speech 0,AnimSpeech10
CS_Speech 1,AnimSpeech11
CS_WaitTime 60
CS_Speech 0,AnimSpeech12
CS_WaitTime 20
CS_Speech 0,AnimSpeech13
CS_WaitTime 20
CS_Speech 0,AnimSpeech14
CS_WaitTime 80
CS_Speech 0,AnimSpeech15
CS_Stop
These are macros, which wrap a bunch of command declarations. Here's an example - AddObjAt

CS_AddObjAt_GOInitFunc equ 4
CS_AddObjAt_x equ 8
CS_AddObjAt_y equ 10
CS_AddObjAt_commandsize equ 12
CS_AddObjAt macro \1,\2,\3
dc.l CSCommand_AddObjectAt
dc.l \1 ; GO init
dc.w \2,\3 ;x,y
endm

CSCommand_AddObjectAt:
move.l #'addo',DebugArea
move.l CS_AddObjAt_GOInitFunc(a4),a1
jsr GOManager_SpawnGameObject_UIScreen
move.w CS_AddObjAt_x(a4),go_x(a1)
move.w CS_AddObjAt_y(a4),go_y(a1)
add.l #CS_AddObjAt_commandsize,a4
rts
(there is more code there, but cut off for clarity) So each command is defined as a function address, and various parameters. I did this in Rocket Panda with index lists instead, but this seems a bit simpler. Also, all the functionality is grouped together in the source file.
A4 is the pointer to the current command, throughout the command handling process.

Day 25

Cutscenes needed some indication of who's speaking, so I made a sprite-based text renderer. which shows the text coming from the speaker, before settling in to place.

Day 26 - Beta4

Fixed some SRAM issues (wrong header!) 

Wrote a tool to pad out the size of the rom, and fix header issues. All works lovely now on a variety of real hardware.

It's pretty much complete now. There are I think some bugs with the dungeon generation, but they may be in the original game as well, I'm not sure. Leaving it alone now whilst waiting on music.


Day 27 - 3-11-2023

Seemingly fixed all issues with later levels crashing. It was a simple off by one error in the spawning system.
Fixed a bug (thanks Reemus) with choosing which dungeon is generated.
Stopped bullets colliding with the background on their first frame, as this was stopping the ability to shoot things like cages if you were standing next to them.



Comments

Popular posts from this blog

Converting Gunslugs to Megadrive / Genesis