To round out my discussion of Contra I’m going to cover a few miscellaneous topics that don’t warrant a full writeup on their own. Some of these things are details of the game’s programming and some are just interesting things about the game itself that I never knew before. I’ll start off with a table of contents for this series for easy reference and then dive into the last few details.
Previous Posts about Contra
Contra has a single global 8-bit value that it uses as the source of randomness throughout the game. The way that it updates the random value each frame is kind of interesting in that it doesn’t implement any particular algorithm that you can call on once per frame to get the next number in the sequence. Instead, the next random value is generated by spinning in a tight loop during the time that the game is idle and waiting for the next display frame to begin (while waiting for the vblank interrupt.) During that time, the game continually adds the current frame counter value into the random number over and over again until the video hardware in the NES signals to the game that it’s time to begin processing for the next display frame. One consequence of this approach is that the particular sequence of random numbers you’ll get is heavily dependent on the exact cycle level operation of the CPU and the interaction between the CPU and the video hardware. That means that even if two emulators implement the logic of the CPU instructions perfectly, they will still generate different random sequences if their timing is not exact. You can see an obvious manifestation of this in the attract mode of Contra which plays back a pre-recorded stream of button input and relies on determinism to replay the demo exactly the same each time. Below is a side by side comparison of the same frame of the attract mode running on two different emulators. Notice that the running soldier that has randomly spawned on the lower right part of the screen selected a single spawn when running on Nintendulator and a triple spawn when running on NESten. This is because the sequence of random numbers being generated by the two instances of the game is not the same. Fortunately, the influence of the random numbers is small enough to not completely break the attract mode playback.
Structure of Arrays
Contra stores all of its data about enemies and bullets in structure-of-arrays format instead of the more object oriented array-of-structures format. The reason for this doesn’t have to do with CPU cache utilization or SIMD instructions like you might expect, but is instead a consequence of the NES having 16-bit memory addresses but only 8-bit CPU registers. If you want to access data indirectly through a pointer, you can’t store the address of the object in the pointer and then hard code the offset to the member you want in the load instruction because the address of the object probably won’t fit into a CPU register. Instead, you need to flip everything around and hard code the full 16-bit address of an array of members for multiple objects into your load instruction and then use the register to hold the offset within the array to get to the member that belongs to the object that you’re interested in.
In Contra, everything happen in screen space. The positions of the players and the positions of the enemies and bullets are all stored and manipulated within the coordinate system of the screen (i.e. a position of 0 always means the point in space that is currently at the left side of the screen, no matter where within the level the screen has scrolled to.) This is an optimization that seems a little counter intuitive at first because you would think that storing all of your positions in screen space would make things like scrolling a lot more complicated. Now instead of just changing a single scroll position variable to scroll the screen, you have to manually move the positions of every enemy, player and bullet every frame to simulate scrolling. The reason that this actually ends up being a good thing to do is that it saves a lot more time than it costs each frame. Scrolling is more complicated, but it really just boils down to adding 1 extra number into an object’s position as it updates each frame which is often just 1 extra CPU instruction per game object. The big win is that now all calculations related to the positions of things can be done using 8-bit values (the NES screen is 256×240 pixels so you can locate any position on screen with pixel precision using a single byte for X and Y.) Since the NES only supports 8-bit arithmetic natively this ends up being way faster than trying to emulate 16-bit arithmetic to handle 16-bit world positions. This of course only works for a game like Contra where the relationship between world space and screen space is a simple translation without any scaling or rotation possible (i.e. transforming from world to screen space happens to be commutative with other translations even though applying transforms is not commutative in general.) If the camera could zoom out in the game then things would start moving too fast and if the camera could rotate then things would go in the wrong direction.
There are a number of things in Contra that change according to how many times you’ve played through the game in a single sitting, and which gun you currently have. The following is a complete list of the differences, where FINISHED would be 0 the first time through the game, 1 the second time through, etc. and GUN is 0 for your default weapon, 1 for Fire, 2 for Machine gun or Laser and 3 for Spread:
- Level 8 mid boss and final boss HP = 55 + (16 * FINISHED) + (16 * GUN)
- Level 8 mid boss projectile HP = 2 + FINISHED
- Level 8 mouth things in wall HP = 4 + (2 * FINISHED) + GUN
- Level 8 scorpions HP = 2 + FINISHED + GUN
- Level 8 pods around final boss HP = 24 + (2 * FINISHED) + (2 * GUN)
- Level 6 boss HP = 64 + (8 * GUN)
- Red cannons that comes up out of the ground wait less time to begin shooting at you for greater FINISHED
- Random enemy spawns are more frequent with greater FINISHED and GUN, and when FINISHED is 0 they take care not to spawn right on top of you and only spawn on the right side of the screen in the first level
- Gray turrets in walls rotate to target you faster, and wait less time to target again after shooting bullets with greater GUN
- Soldiers that shoot bullets tend to shoot more bullets per round with greater GUN
- Cannons on screen for level 2 and level 4 bosses, as well as the bosses themselves, wait less time between shooting at you for greater GUN
- Level 8 mid boss shoots 3 projectiles at a time when GUN is 3, but only 2 otherwise
- Speed at which scorpions drop down on you from the ceiling during final boss fight increases proportional to GUN
Contra is one of my favorite games on the NES and as a game programmer it was a lot of fun to dive in and figure out exactly how it works. I’m always amazed at how much game developers of the time were able to get out of hardware that is ridiculously slow and limited compared to today’s consoles. If you have any feedback about this series of posts please let me know so I can come up with a plan for if/when/what I’ll cover next. You can reach me on twitter @allan_blomquist or comment below.
(Prev – This is part 7 of a 7 part series)