Extreme RAM Saving Techniques
It has been a while since I have written one of my blog entries about the technical challenges of making my RPG "Penult" for the Atari 2600. By far my biggest challenge has been making the game with the extreme memory constraints of the console, so this entry will discuss how I dealt with those issues.
RPGs tend to be very memory-hungry. You need to keep track of a character's name, stats, inventory, experience, location, current state, etc. If there are multiple party members, these need to be tracked for each character. The Atari 2600, on the other hand, only has 128 bytes of RAM. For Penult, I am using a cartridge format that lets me double that, for a total of 256 bytes of RAM, or one quarter of a kilobyte. Over half of this RAM is used to track the tiles currently displayed on the viewport portion of the screen as well as the text characters in the two text lines below the viewport. The visible map is 12 tiles wide by 7 tiles tall, for a total of 84 tiles. The text lines below the viewport are each 24 characters long, or 48 for both. All of these together take up 132 bytes of the 256 bytes available, leaving 124 bytes remaining.
The Stack
On the Atari 2600, stack space comes from the same limited pool of RAM. Every subroutine call takes two bytes to store the return address of that routine on the stack. Due to the extremely limited RAM available, I was careful to never use more than 4 bytes of stack space, which means never having more than two levels of nested subroutines. Allowing 4 bytes for the stack means that the actual amount of RAM available for the game is only 120 bytes.
Hero Name and Gender
When you only have 120 bytes of RAM to work with to make an RPG, even allowing the player name their character seems daunting, as that name would have to be stored in RAM. I considered having the hero already be named, or letting the player choose from a variety of pregenerated names, but these options didn't appeal to me for the style of game I was making. In the end, I allowed players to use up to 6 letters for the hero's name, and I have code to compress this along with the choice of gender down to 4 bytes of RAM.
Companions
With extremely limited RAM, having a party of characters each with their own stats simply isn't feasible. Even a single companion would require at least 16 bytes to keep track of stats, equipment, etc. However, having the hero adventure alone didn't fit the style of game I was trying to make, either.
My solution was to have the hero have a small fey dragon companion that under the hood shared most of the hero's stats, and didn't need any of its own equipment. The dragon e.g. has the same maximum hit points as the hero, and always starts with maximum hit points at the beginning of combat. The hero's level is used for things like attack roll modifiers in combat. Finally, the power of the dragon's abilities is based on the hero's game stats. E.g. if the hero has a high strength, then the dragon's bite will do more damage, or if the hero has a high intelligence, then the dragon's breath will be more damaging, and its heal ability will heal more hit points with each usage. This also has the side effect of having the dragon companion of different heroes have different strengths and weaknesses, and it also means that dragon's effectiveness will automatically increase as the hero becomes more powerful.
Single-bit Variables
A byte has 8 bits, and I keep track of as much as I can in the game with bits instead of bytes. I use 7 bytes to effectively make 56 single-byte variables in the game to save RAM (actually more than this since some bits end up getting reused in different parts of the game).
Temp Variables
Since the Atari 2600 doesn't have screen memory, a character display, etc, 28 bytes of RAM is used to build and display the 96-pixel visible screen kernel that I use for the viewport and text lines. Since these bytes do not have to maintain their values after the screen is drawn, it means they are available in other parts of the code to use as temp variables. Whenever I can, I make heavy use of temp variables in my code to avoid reserving more of the very limited RAM for permanent variables.
Variable Reuse
Also to save RAM, there are many variables that get used for multiple purposes. E.g. variables that are used in combat may be used for other purposes out of combat. This can be tricky to do correctly to avoid situations where both variable values would be needed at the same time, but is vital to avoid running out of RAM.
Combat
Combat can use up a lot of RAM since you need to track the position and hit points of each opponent in addition to the hero and fey dragon companion. For this reason, there are never more than four opponents on the screen. Even enemies that can spawn more of their kind only do so when there are less than four of them on the screen. I also save seven bytes by encoding the X and Y positions of the combatants and any active missile in a single byte instead of using a byte to track each axis.
Static Map
Games like Ultima have cities where people wander around, and outdoor areas where you see monsters roaming the land. Doing this in my game definitely wasn't feasible, since this would require a huge amount of RAM to track all of these NPC and monster positions. Instead, the town maps have NPCs with fixed positions on the map, and the outdoor map doesn't show opponents until you encounter them.
Also for reasons of limited RAM, the game can only track one ship at a time. If one becomes inaccessible and a new one is purchased to replace it, then the old one is lost.
Simplified Inventory
The game keeps track of the hero's melee weapon, ranged weapon (if any), armor, and a few miscellaneous items. If e.g. their armor is upgraded from leather to chain, then it is presumed that the old suit gets donated to that city's defense effort. This greatly streamlines inventory management and vendor interactions, but more importantly it saves a ton of RAM by not having to track unused equipment.
Dungeon State
Dungeons in Penult are 8 levels of 16x16 tiles. I wanted to have treasure chests that could be looted, and the game would keep track of which chests have already been looted and which ones hadn't. As there could be chests in any location on any level, there simply isn't enough RAM to keep track of all of these in a normal fashion. The solution I came up with is in a separate blog entry: The Problem of 2048 Treasure Chests.
While there were sacrifices that needed to be made along the way, all and all I am quite happy with how much of an "Ultima RPG feel" I was able to create with the limited resources I had to work with.
-
4
2 Comments
Recommended Comments