From Thousands to Hundreds: Exploring Memory Limitations in Retro Consoles
The memory capacity of a game cartridge has more to do with the amount of graphics it can handle or store. It affects the overall performance of the game, and what is possible for the game to do in terms of mechanics. This blog will offer a look at how these limitations can be overcome with creativity and planning.
Breaking Down Storage Capacity
Let's start with the fact that even the simplest game for a modern computer often requires several megabytes on a hard disk. Let's make sure.
This is Exolon, a classic game for the ZX Spectrum, a personal 8-bit computer. Its size is only 43 kilobytes.
And this is a modern remake of the same game for the PC (from 2005), and it occupies 26 megabytes.
Can't you just feel the difference? The PC version is almost 600 times larger. And it is clear that the PC can afford to use high-resolution graphics.
We, however, do not have such luxuries with retro consoles.
Let's remember the differences between a modern PC, the ZX Spectrum, and classic consoles. A modern computer typically measures memory in gigabytes. The ZX Spectrum can address 64 kilobytes, 16 of which are reserved for the Spectrum’s ROM which contains a Basic language interpreter and some system subroutines. We'll subtract the screen size from here (16 kilobytes) and we will have an honest 32 kilobytes of RAM that we can use.
Our NES, unfortunately, has only two kilobytes of RAM. This means that we are forced to program our game and game elements in a way that is always mindful of this budget.
One challenge that requires such an approach is that of enemies that will automatically “resurrect.” Everyone has witnessed it. When we play some retro games, like Super Mario Bros 2 for example, we defeat an enemy, proceed to scroll to the next screen, double-back, and *poof*, the enemy has returned.
This is done in part because in order to track each of these enemies as “defeated,” we would have to spend some of our precious two kilobytes. But if we did that, we would not be able to show other, no less interesting features of the game, as the next screens in the level.
This is not necessarily a bad thing. Sometimes it is very useful for items and points farming, but there are also games where we would not like to meet these bad guys again.
Other games keep the state of enemies and many other things, and of course, programmers have spent some amount of RAM for this.
Even a relatively simple scenario like resurrecting enemies requires careful decision-making, as it can impact the rest of the game’s overall design choices.
Sometimes, our problems are more complex, and yet we are up against the same restrictions.
Icing Our Meat
Let's look at this picture.
This is one of the levels for The Meating. Here are 233 ice blocks that can be destroyed by the player once, and each of these destroyed blocks should be stored in order not to show them again. And what if we had not 3 screens, but 6, like this?
How about 9 screens? What if the level designer decides to fill all the screens with ice blocks? I think some game designers could do that. In this case, we will need to store information about 2160 blocks (16x * 15y * 9 screens). We need to store three parameters of each block:
- Screen number
- X position
- Y position
2160 * 3 = 6480 bytes total.
But we have only two kilobytes.
Fortunately, there are some clever tricks for overcoming this challenge.
Option 1. Packing to the Bits
Let's suppose the number of screens of the level is six.
Since the maximum screen index is five (zero is also a value), we need three bits (101 in binary format) to store this value.
We'll move on. The maximum value takes X: 16 in decimal (1111 in the binary).
Since the maximum values of X and Y are 1111 and 1110 respectively, we can try to pack them into one byte: 11111110.
So, to store X and Y, we will need 1 byte for both of them, since they are 4 bits each (1 byte = 8 bits). Plus screen number: 00000101 11111110 it’s two bytes for each ice block total. In sum, it is still quite a lot:
16 * 15 * 6 * 2 = 2880 bytes.
Well, let's remove the screen number, perhaps, we do not really need it. So now we need 1 byte for each ice block. This is 1440 bytes total.
Not bad. And if we programmed for the ZX Spectrum, we probably would have done so. But with NES we still cannot afford it, if we want a game with interesting mechanics and effects.
Option 2. Advanced Packing
Perhaps it is time to take a chance and try to pack one block to one bit.
The size of our map has 16x15 metatiles, so we can fit 16 blocks to one row. This is 16 bits (or two bytes) by one row or 30 bytes for one screen of ice blocks. Our level has six screens, which means that we will spend only 180 bytes to store info about 1440 blocks.
It's simple. An array of 180 bytes is divided into sections of 30 bytes for each screen. The first 30 bytes are screen 1 data, the next 30 bytes are screen 2 data, the next are screen 3 data, and so on. Therefore, to get access to the first row of screen 5, you need to calculate the offset by the formula: offset = (screen number-1) * 30 and we get 120. For the offset of the last, sixth screen: 150. If you add 30 here (data size for one screen), you get 150 + 30 = 180. End of the array.
Whole array map with offsets to desired screen data.
We will describe the state of the block by the value of one bit (1 or 0): 1 if the block was destroyed by the player, and 0 if the player did not interact with the block.
This compact way of organizing our data allows us to make the best use of our 2 kilobytes of RAM, and still achieve an effect that does not suffer from “resurrecting enemy syndrome.” Case in point, watch how our ice blocks behave now:
Got it? Now you're on the right path to mastering memory limitations through creativity and programming power!
Alex Tokmakov is a retro dev and meat enthusiast. He is the beefy programmer bringing this ghost minotaur game to life.