Converting Gunslugs to Megadrive / Genesis
Overview
This is all about porting Gunslugs, a PC game written in Java (by OrangePixel ) to SEGA Genesis / Megadrive, written in 68000 assembler.
Original Game (PC) |
Code
Objects / Entities
There are quite a lot of active objects at once in the original game- something lke 100-200 plus bullets and effects. This would not be doable on the Genesis, so instead I generate a list of spawn points for level objects. These are sorted horizontally so that I only need to check one at a time as the screen scrolls. Once a spawnpoint is determined to be just off the right edge of the screen, it is promoted to being an active GameObject, and the spawer code continues looking for the next object in the spawner list.
Early on I knew there would be a problem with this, due to objects disappearing off the left of the screen. In the original these continue to be active. But at the start I didn't know enough about the game to determine what to do about it. In other games I'd unspawn the GameObject and make it active in the spawner list again.
However another issue was that in the original it's possible (I think) to keep a lot of enemies around you by sort of dragging them around the map. For the Genesis this needs to be avoided as it brings back all of the issues that spawning fixes. There would be too many objects to handle at once (needing more CPU and memory) and not enough hardware sprites to display them anyway.
I left this issue in right up to near the end of development, and then, after understanding the gameplay a lot more, decided simply to delete (most) objects if they go too far off the left of the screen.
Multiple Lists
Another optimization which was left until late in development (because of so many unknowns.. ram / CPU usage / gameplay) was splitting up the original single list of entities in to multiple lists. It turns out that some Entities (I'll refer to the objects in the original game as Entities, and the objects in the Genesis version as GameObjects) need to stay active instead of being deleted off the left edge. So I store a different list of GameObjects for these (called 'important' objects).
I store another list for major Entities like enemies, but this list is allowed to fill up, in which case a new one won't be added - so perhaps an enemy wont appear if there is too much on screen already. A third list is used for bullets etc. and finally another one for particle effects like blood and bits of crate, and explosion effects. This uses a circular list so that more recent effects overwrite old ones.
I wasn't able to add these optimizations until i knew how much RAM i had free, a the multiple lists use a little more memory than a single list.
Furthermore, for testing collision, once per frame i build separate pointer lists to these objects so that I can perform much quicker collision detection.
Other Optimizations
There are quite a lot of objects walking around and interacting with the floor and wall tiles at any given time. This was probably the worst performing aspect of the game for quite a while while it was in its naive state. A few little optimizations made this much more bearable.
The original collision map was something like 176x10 tiles. I rounded this up to a nice power of two number to make the lookup much quicker (and also cached lookups)
Further, a lot of objects would need to check if they are off the edge of the map every time they query the collision. Adding a border around the collision map solved this in most cases. I was lucky to have enough RAM available to store this. Two copies were needed (because you can enter buildings and return to where you came from) - and they are alterable during gameplay so I couldn't use the static copies in ROM.
And then there's the normal non game specific things, like making sure everything is in registers rather than RAM, but that's just part of the fun of writing in assembly language.
Graphics
The Genesis has 4 palettes of 16 colors each (15 plus transparent) - and the original has as many colors as it likes. This was perhaps the most difficult and time consuming problem of the whole conversion process, not least because I'm not very good at graphics!
I was lucky to have access to all of the original graphics, but i wasn't really sure what was what, and more importantly what is used where. So, this became very much an iterative process as I discovered what sprites could be displayed at the same time as others.
An early decision I needed to make was whether to use the original sprite sheets directly or to create my own, On the originals the sprites were all condensed closely together with no gaps between them. My tools didn't handle that, because the Genesis sprite sizes are multiples of eight in each axis, ranging from 8x8 up to 32x32. I would have to grab the original spites and add some padding.. but not knowing exactly where the origin of each sprite was. I quickly decided to copy / paste the originals in to my own sprite sheets, for easier management, but fully realizing that later on i might regret this if I had to keep re-doing it multiple times.
An original sprite sheet, using lots of colors. |
So i just added stuff as I went. At this point I had only been able to play through a few levels and didn't really know what was coming down the line.
I was trying to use Aseprite to manage my screens and palettes, but I really couldn't figure it out and didn't like the workflow. I bought Pro Motion NG, which while lacking in some areas, was so much better for me. And as time went on, and after multiple iterations of grabbing sprites, I found a lot of nice tools in Pro Motion to help reduce the palettes to what the Genesis could handle.
One set of sprites in a single 16 color palette. |
I ended up with one palette for the foreground / background tiles, two main palettes for generic enemies, and one replaceable palette depending on which level we are on. I could have made some improvements by knowing what player characters are going to be on each level, and allocating eg. 6 colors for each one. But that was really going to get complicated management-wise so I left it out.
I placed most of the sprites in a single palette on a single bitmap, with some exceptions, as this helped me to manage and understand what sprites used what palettes.
If an artist has a pass over what I have come up with, we might see some improvements before release, especially with far backgrounds, as I had to really simplify those. I believe some of the original tilesets I use have some unused characters in them, taking up some palette entries which could be better used elsewhere.
Graphics memory is in short supply on the Genesis, but really again the problem was not knowing which graphics are needed at any given time. Possibilities to handle this include a streaming system where some graphics characters are overridden in VRAM every frame. But really unless you know the worst case scenario then this can be a hindrance instead of a help. Also this takes up more CPU time, which is in short supply anyway.
My approach was (as usual) just to add things until I run out of space, then work out what to do. Some of the original graphics had flipped versions, so of course they could be removed as the genesis hardware can flip sprites horizontally and vertically for free. Some sprites I would shrink slightly so they would fit in to an 8 pixel boundary, though most of them were drawn that way in the first place.
For large in-game sprites I had to split these in to multiple hardware sprites, which can save a lot of space as you're not storing empty areas, though this is a delicate balance between using too much memory and more sprites (the Genesis has 80 sprites available).
Another optimization was to split large buildings horizontally and use x flipped versions to mirror the building half way. This loses some graphical quality because the shading was different on each side in the originals, but it saved a lot of space.
In the end I have ended up with plenty of space free, so I can gradually add more back in if needed (maybe more rotation frames - some original objects are rotated when rendered, but I can't do that on the Genesis) - and especially there is more space there for more nicely detailed backgrounds.
Bosses
Bosses are usually quite special cases in Genesis games, and this is no exception. The original game could just render as many huge sprites as it wanted, but this is unachievable on the old hardware. I just had to take each boss as it came and work out how best to recreate it. In most cases this was actually pretty simple and involved losing the far background scroll. Instead the boss is rendered on to a tilemap screen and that is scrolled around.
For bosses this game I use a ram tile buffer which gets DMA'd to VRAM each frame. This is just to make it easier for me to code the BLIT and clip functionality. I basically have a 'software sprite' BLITTER to render chunks of background in different places.
I also found it rather nice to use the age-old technique of trailing-edge deletion for moving big sprites around. This means you don't need to clear the background, instead you can just BLIT over the old image in a new location without having to worry about leaving bits behind.
The whole image. Notice that the far background image is not there. |
The boss's base is rendered to the scroll plane where the background used to be. |
World Generation
This is really the only place I used the original source code as-is, after converting to c#. Instead of randomly generating the levels on the Genesis I can pre-generate multiple random versions of each level, compress the data, and store them in ROM. Converting the Java code to assembler for this looked very daunting, as there's a LOT of it!! Call me a cheat. Go on!
Comments
Post a Comment