We are a 3 person team. Here are our games so far:

Retro Game Internals: Contra Conclusion

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

Random Numbers

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.

prng_difference

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.

Screen Space

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.

Subtle Behavior

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

Conclusion

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)

Tags: , , ,

16 Responses to “Retro Game Internals: Contra Conclusion”

  1. Sweet! This was awesome, as always.

    I reckon they figured that entropy wasn’t all that important with their random number generator. XD Not that I blame them.

  2. Cristian says:

    Contra is one of my favourite game, and your in deep dive is really a gem! I indeed learnt something.
    Thank Allan!

  3. Joakim says:

    Very nice series. I always find it interesting reading about how they solved technical issues back in the day,

  4. @binxalot says:

    Thanks for writing this series, I saw this posted on making of games on reddit and saved it. Easily one of my favorite games for the NES and one of the few games I owned. It’s funny that I found this, only a two weeks ago I had this booted up on nester to do a playthough. I never knew the game got harder after beating it and playing through it again, it was always such a challenge to beat it on the first run I usually called it quits afterwards.

    For the bosses, are there any special code snippets from any of the special boss enemies that are interesting? Like for instance the code for the flying enemies with wings from the end of level 2? or the red and blue follow bullets? Any unused sprites that didn’t make it in to the game? Thanks again for this write up it was really great.

    • Allan Blomquist says:

      I did originally want to have some stuff about enemy behavior but I wasn’t able to find any particularly interesting examples. The behaviors in the game are all fairly basic and straight forward. Nothing is very smart and so most of the code related to enemy behavior is just book keeping around very simple state machines. Maybe I’ll try to dig out the most interesting example I can find if I get some time to work on more of these posts.

  5. requiredname says:

    Reddit programming brought me here. Sweet series, though it would have been nice to see the annotated disasm instead of just words.

  6. John Evans says:

    I can’t help but wonder, is there some limit to the “FINISHED” variable? What would happen if you monkeyed around with it, maybe in an emulator? Is it an 8-bit variable that can overflow? Hm…

    • Allan Blomquist says:

      Yup, the number of times you’ve finished the game is stored as an 8bit value so it will wrap back to 0 eventually. Also, none of the calculations that use it guard against overflow so enemies can actually end up with fewer hit points that normal or even 0 HP.

  7. Timothy says:

    Awesome series. It’s fascinating to get a peek into how these games get made. Easily one of my top 5 retro games.

  8. Nombo says:

    I love this game so much and I’m in awe of people such as yourself who can go ‘under the hood’ like this.

    I have a question that I’m wondering if you would be able to answer; maybe you’ve come across it in your probing of the game:

    It concerns the “R” power-ups. They are hard to understand completely, as their effects are so subtle.

    Like, they increase, the amount of bullets that you can fire, or perhaps more accurately, the power-up decreases the amount of time you have to wait before getting off another round.

    When they are accumulated, their effect seems to increase. But with some weapons it’s not very apparent at all – to the point that I sometimes wonder if they actually ARE accumulating. I feel like there’s some mystery here. Maybe it’s dependent of the weapon?

    But what I’d love to know even more is this:

    Say you have the standard gun. Then, you get an “R”. Then, you get a spread shot. At this point, is the spread shot “R”-ified?

    I can’t figure it out. The subtlety of the effect of “R” in general contributes to this uncertainty, and also, the effect of “R” is most apparent in the ‘Base’ levels, so there’s never really the opportunity to figure this out once and for all.

    I’d love to hear your thoughts on this.

    • Allan Blomquist says:

      The R modifier makes it so that the bullets you shoot move faster. Any other effect that it seems to have is just a consequence of the fact that the bullets are moving faster (and so will exit the screen sooner, allowing you to shoot again sooner, etc.) When you get an R, the effect is turned on and collecting more R power-ups will not increase the effect – it is either on or off. When you pick up a new weapon that is different from the weapon you had before, R is always turned off. When you pick up a weapon and it is the same as the weapon you already have, R is not affected so you will still have it if you had it before. Also, the L weapon is ALWAYS shot as if you had R so there is basically no effect for that weapon.

      • Nombo says:

        Thanks for the reply, and for explaining it so well.

        All these years I thought they were accumulating/compounding! The spread shot particularly.

        Thanks again. You’ve really cleared this up, nicely.

  9. Retro-Ed says:

    Don’t forget the 30 lives

  10. VBKesha says:

    Very cool and intresting!
    Great work!
    It would be interesting to hear what tools and techniques were used to analyze the game code.
    And very intresting know how working Snake Rattle and Roll!

  11. Slormer says:

    Awesome write up.

    I found myself rereading all of this again as a result of the Contra speedrunning community looking into why the Stage 8 “ceiling scorpion” doesn’t make contact with the player if it falls on them when they only have the peashooter.

    Lots of the mechanics and quirks you’ve consolidated here are really useful for routing the quickest and safest ways throughout the stages. Thanks!

  12. AWJ says:

    I’ve been reverse engineering Life Force and it’s remarkable how very similar it is to Contra. It uses the same object model where players and player projectiles are managed by specialized code but other objects are abstracted; the same system of tables of precalculated hitboxes for every combination of enemy size and player/projectile type; the same pseudorandom number generation, etc. I guess Konami had a repertoire of standard techniques for the large number of shooting games they released on underpowered 8-bit hardware.