Like many of those who program video games, in childhood I often played games for NES. It always amazed me how developers were able to achieve so much with minimal effort, so I spent a lot of time analyzing the internal workings of some games. Today I begin a series of posts in which I will document what I learned from a game programmer’s point of view. I will try to focus on the game systems at the engine level, and not at the hardware level (that is, I will talk about how the game decides what needs to be drawn in the current frame, and not how the sprites work on NES). I will also try to add any bits of information about the games that I find interesting, for example, behavior that is not obvious from the point of view of the player or examples of bugs in the logic of the game.
Introduction to Contra
I will begin the description with the game Contra for NES. In this section I will give a brief overview of the objects and data existing in the game, and in the following sections I will examine in detail each game system used to control the model of the world. The simplified model of the game consists of:
- Player characters
- Bullet players
- Enemies and other objects
- Level data
Characters of players are the most "heavy" objects of the game. As you might expect, there is a huge amount of code that performs solely character processing. Character bullets are handled differently for each type of object in the game for reasons that may be related to performance. Later we will look at how the separation of player’s bullets from other game objects makes various aspects of the game more efficient. The last class of objects are enemies controlled by a simple system of objects. It includes the enemies themselves, their bullets and explosions, as well as a couple of “player-friendly” objects, such as flying capsules with bonuses and bonuses themselves lying on the ground. I will call all these entities “enemies” in order not to use the more general term “objects”. The most important thing is that the game has code that processes them abstractly, unlike player characters and bullets, which are always controlled by specialized code.
The last part of the game simulation is tile-based level data. Contra has standard horizontal levels, vertical levels and levels in pseudo-3D. The levels can only move forward. The game stores a double tile buffer: the first buffer stores collision data and graphics of the current screen, the second creates the next screen. Below we will see how these levels are organized, how they are updated when a player is moved, and how enemies are created based on them.
')
Tile card
Each level in the game is made up of a series of independent screens. The figure below shows a general view of the first level of the game, divided into 13 screens that make up the stage.

Each screen is built from a set of non-intersecting "super-files", each of which occupies 32 Ă— 32 pixels of screen space. The screen always consists of 8 Ă— 7 superTiles. As a result, the final screen size is 256 Ă— 224 pixels. NES native resolution is 256 Ă— 240 pixels, so Contra simply leaves empty screen space, rather than trying to halve the super-files in order to completely fill the screen. This is pretty standard behavior for console games of the time. They were not very worried about what was displayed above and below the screen, because these parts were not always visible on CRT TVs.
The figure below shows the seventh screen of the first level, divided into 56 background supertype files.

Each supertype, in turn, is composed of ordinary tiles of 8 Ă— 8 pixels located in a 4 Ă— 4 grid. It is these regular tiles that NES equipment uses to display background maps, so all games must at some stage break maps into tiles of 8 Ă— 8 pixels.
Collision data
Collision information for each screen is obtained from the final layout of regular tiles, rather than being created as a separate map. The resulting collision map is made up of screen blocks of 16 Ă— 16 pixels. This means that each block of collision data consists of a grid of regular 2 Ă— 2 tiles, but when obtaining a collision type, only the top left regular tile is considered for each block.
Each level defines four ranges of tile indices, and each of the tiles in the range belongs to one of four types of collisions. For example, at the first level, tiles from 0x01 to 0x05 belong to the type "ground". They can land on the fall, but pass through when moving left, up and right. Tiles from 0x06 to 0xF8 belong to the collision type “empty space”. Tiles 0xF9 to 0xFE are of the type “water”, and tile 0xFF belongs to “completely impenetrable” tiles.
The figure below shows a part of the first level of a game with a resulting collision map superimposed on it. Blocks with the collision type “water” are marked blue, and blocks “earth” are marked green. Blank space collision blocks are marked black, and there are no impenetrable blocks in this part of the level.

The enemies
On each screen there is a list of enemies that you need to create in the world when the player scrolls through it. The list is sorted according to the distance you need to scroll the screen to create an enemy. For example, the first enemy on the first level has a scrolling (scrolling) value of 16, so it will be created when the player starts the game and advances the screen forward by 16 pixels. Since the list is sorted by scroll distance, and the screen can only be scrolled in one direction, there is always only one new “candidate” for creation.
The second part of the data in each list item is the type of enemy being created, as well as the number of enemies of this type being created. Then for each of these numbers of enemies there is a general meaning of the creation context and a position value for creating an enemy on the screen. The creation context does not matter for the system of creating enemies, but is later interpreted by the own logic of updating the enemy. It is used, for example, to configure which bonus falls from a flying capsule, and to control certain aspects of the behavior of fleeing soldiers. The value of the position of creating an enemy gives only half the information needed to place a new enemy on the screen. For levels with horizontal scrolling, it gives the position of the enemy along the Y axis, and the position along the X axis will always be at the extreme right side of the screen. If the level is scrolled vertically, the value of the creation position is the position along the X axis, and the Y position is always at the top of the screen. Enemies, who seem to be created behind the part of the screen going back / down, actually have built-in logic, the first action that moves them to the opposite side of the screen.
The list of enemies of variable length is completed with a byte with a magic value, after which the game waits until the player scrolls to the next screen to start searching for new created enemies from the list of enemies of this screen. The enemies themselves are engaged in the destruction of enemies after scrolling and disappearing them from the screen, there is no system that does this automatically.
Random creation of enemies
In addition to the enemies created in certain places of the level, there are also enemies that appear a little random. These enemies are fleeing soldiers, often created at the edges of the screen during the game. The system that controls these randomly created enemies is surprisingly complex, despite its seemingly simple task. The picture below shows the very enemies. All the soldiers running around the screen were created by chance, while the player stood motionless.

The heart of the system of random creation of enemies is the timer, counting over and over from some initial value to zero. Every time you reach zero, there is the likelihood of creating an enemy. Each level has an interval used by the timer, and at some levels the random creation of enemies is disabled by assigning a zero value to the timer (for example, at the base level and at the last level). Above the default interval, two more modifications are made to achieve the necessary finite interval determined by the level. First, the interval is reduced by about 20% after each passing game. This means that after the game sends you back to the first level after passing, random enemies are created more often, and so on, while you go through the game again and again. Another factor reducing the timer interval is the weapon used by the player. Weapons are classified according to how the game perceives them: P is considered the worst, F is slightly better, M and L are considered equally good, and S (of course) is the best. The timer interval of the random creation of enemies is reduced by about 3% for each point (0-3) of the player's current weapon. The figure above shows the fourth passage of the game, the player in the hands of the Spread gun (S), but the screenshot does not give an idea of ​​the number of opponents, constantly running out on the screen.
The speed of decreasing the timer counter is also variable and depends on whether the screen is currently scrolling. If we assume that the rate of decrease in a stationary state is 1.0, then when running forward it is 0.75. This leads to the fact that less random enemies are generated when moving. I think the developers would like to compensate for the fact that the player will have to face the enemies created by scrolling during the scrolling.
When the timer reaches zero, there is a likelihood of creating an enemy. The side from which the enemy will appear is usually completely random. With one specific exception: when you are at the first level, play the game for the first time and on the current screen less than 30 random enemies were created, then the enemies will always be created on the right. This allows newcomers to the game to get used to it. The position along the Y axis of the creation of a new enemy is selected in one of three different ways, depending on how many frames have passed since the launch of the game. One quarter of the time, the game is looking for a platform above the screen to create an enemy. One quarter of the time she starts from the bottom of the screen and searches up. The remaining half of the time the game tries to use the player’s random current position along the Y axis as the initial Y coordinate to search up. However, in this logic there is a “bug”, which leads to the fact that the search begins at the very top of the screen half the time, only if only one player is alive in the game. This “bug” doesn’t really interfere with anything; its only effect is that enemies can be created at the bottom of the screen more often than they should.
After the desired position to create an enemy is selected, several more checks are performed to determine if creation should be carried out. If the selected position is too close to the top or bottom of the screen, the creation is canceled (the exception is the level with a waterfall, where the creation of enemies at the top of the screen is allowed). If you are on the very first screen of any level, the creation of enemies is canceled. Another tightly defined check - if the player is on the last few screens of the Snowfield level, the creation of opponents on the left side of the screen is always canceled. This check is triggered when the player reaches the most recent snow-covered platform level, when trees are visible below, bombs are thrown at the player and one enemy shoots him with a stationary cannon. Why such a specific test is needed is not known to anyone. Perhaps one of the developers decided that it would be too difficult to cope with so many dangers at the same time (however, almost the same situation occurs at the very beginning of this level, but the creation of enemies on the left is allowed there).
Another hard-coded rule has been added to simplify the first walkthrough of the game a bit. If there were less than thirty randomly created enemies on the current screen, the player passes the game for the first time and he is too close to the side of the screen from which the enemy should have appeared, then such an enemy creation is canceled. This protects against stupid deaths when confronted with enemies that have arisen right in front of the player.
If all these checks are passed, then there is one more check before the creation of the enemy. Each game screen has a unique value that controls various aspects of opponents randomly created on this screen. Each individual screen can control the added likelihood of canceling an enemy. Screens can either always allow you to create enemies, or permanently prohibit, accidentally prohibit the creation of 50% of the time or 75% of the time. And when this last test is passed, it is time to create an enemy.
There are two types of process for creating enemies. If the player is not on a level with a waterfall and the screen is currently scrolling, then there is a 25 percent chance of creating a group of three running soldiers. These soldiers are set up in such a way that they never shoot. Perhaps this is another "bug" or made specifically, but to adjust the behavior of each of the soldiers, the game uses uninitialized memory. Behavior determines how they will act when they reach the end of the platform on which they are running (whether they should jump or can turn back). If it was a “bug,” and not just an attempt to create random-looking behavior, it is rather inconsequential.
Another type of creation process is obtained if the conditions for the first type are not satisfied - the creation of a single running soldier whose behavior is adjusted a little differently. In this case, a random number generator is used to randomly adjust the soldier's jumping ability. For such soldiers, on the basis of another screen value, the possibility of shooting is also chosen. The idea behind this process is that each screen selects one of a small set of pre-selected behavior groups. In each of the groups there is a different combination of the absence of shooting, shooting while standing and shooting while lying down. Then, for each soldier created, a corresponding shooting opportunity is selected at random from a selected group level. However, there is another “bug” in the game: on the one screen of the Hangar level, the eighth group of behaviors is defined, although there are only seven groups. This leads to the fact that the game assigns to the soldiers on this screen a “trash” value of behavior (the actual value is obtained from the part of the pointers to the lists of screen enemies we talked about above). For this reason, various non-lethal side effects occur. Basically, the soldiers instantly change direction and run away from the screen.
Enemies inside bases
Static enemies on bases
Enemies on pseudo-three-dimensional levels of "bases" in Contra are created on a slightly different system. In addition, base levels are divided into screens, each of which also receives its own list of created enemies, but the information in the list is different.

The screens at the base levels do not scroll, so each enemy in the list of enemies on the screen is created when the player moves to the screen. Each list of enemies begins with the number of targets that must be destroyed to turn off the electrical barrier and pass the screen. In fact, there is a part of unused logic in the game, which immediately removes the barrier in the absence of targets, despite the fact that there are no screens with this configuration in the game. The rest of the list consists of a set of records for each enemy you create. The first piece of data in each record reports the X and Y coordinates of the location of the enemy’s creation on the screen. The second part of the data specifies the type of enemy being created, and the third, last, part is the incomprehensible context value of the creation, just as in the lists of enemies for other types of levels.
At the moment of hitting the new screen, all enemies from the list of enemies of the screen are created, and this list is no longer read. These enemies are mostly static objects on the far side of the screen, such as targets and cannons firing at the player. All other enemies that run into the screen during the game are not created from the list of enemies on the screen. They are controlled by another system.
Cyclic enemies on bases
One type of object from the list of enemies of each screen at the base levels is in fact not a visible enemy, but an entity that controls all enemies running onto the screen from two sides. This parent object handles its own separate list of enemies for each screen, telling you when to create enemies and what type. Such lists of enemies also contain sets of records, one for each enemy created. The first part of the data in each record determines the type of enemy being created and sets the context value of the creation for this enemy. The second part of the data contains the delay time before creating the next enemy. Also associated with each record is a flag indicating whether the current record is the last one for the screen. After processing the last entry, the “parent” object returns to the first entry in the list and begins to create enemies using the same pattern again and again. After this pattern was repeated seven times on one screen, the “parent” destroys itself, and the creation of enemies ceases. This event starts shooting targets on the wall at the player.
Enemies with bonuses
On some level-base screens, there are red jumping enemies who give a bonus when killing. The appearance of such enemies is mainly determined also by the secondary list of the “parent” object, but with additional rules: only one red enemy with a bonus can be created on each screen and the creation of this enemy is prohibited during the first pass of the secondary list. The context of the creation of a jumping enemy in the list of enemies of the "parent" determines whether there is a bonus in it, and if so, indicates the type of bonus falling to the player after killing the enemy. The rule of one enemy on the screen applies to the creation of the enemy, but not the killing, so if the player misses the red enemy, then he will not appear on the screen anymore.
Collision Detection in Contra
Collision detection
In the game Contra, two types of collisions can occur: object-object and object-level collisions. Each of them is processed by completely different code and is recognized by completely different data, therefore, in essence, they are two separate systems.
Object object collisions
At the beginning of this article, I mentioned that the game tracks three types of objects: players, player bullets, and enemies. Collision detection is one of those areas in which this separation is used. Instead of checking collisions between all objects, the game checks only collisions between groups reacting to each other. Players never interact with other players or their bullets, and enemies never interact with other enemies, so the only important conflicts are between players and enemies, as well as between player bullets and enemies.
Taking into account the fact that enemies are always involved in important object-object collisions, collision recognition is implemented as part of updating the state of all enemies. In each frame, the game bypasses the cycle of each active enemy and executes its update logic. After updating all the enemies in the system enters the collision. She checks the collisions of enemies with the players, and then collisions with the bullets of the players. Each enemy has a pair of flags that can be used to transmit information about which types of collisions are involved for this enemy. That is why some enemies, such as towers, can be shot (collisions with players' bullets are allowed), but they are not able to kill the player upon contact (conflicts with players are prohibited). In addition to these flags, no special logic is used to narrow down the list of potential collisions of the enemy. Each enemy is simply checked for collision with each player and each bullet.
Checking the collision between a specific enemy and a specific player is based on a “point-to-rectangle” check. A point representing the current position of the player is checked along with a rectangle representing the enemy’s current hitbox. If at the time of the test the point is inside the rectangle, then a collision has occurred. At first glance, it looks a bit strange. It is necessary for a player to die from a shot both in the head and in the leg, but the current position of the player is represented by just one dot (which is usually quite close to the center of the player's sprite), which is checked for a collision with the enemy hitbox. For the system to work, the enemies hitboxes become not just borders around the enemies' sprites, but rather a representation of where a collision would occur if we performed the standard rectangle-rectangle test. In other words, the enemies hitboxes represent the space in which the player’s position must be in order for the player's sprite to overlap with the enemy's sprite.

This process is in action shown in the figure above. On each of the three screenshots, the position of the player is represented by a single green pixel in the center of the green circle. The pink box shows the hitbox used by the enemy’s white bullet to check for collisions with the player’s position. Note that the bullet of the enemy uses different hitboxes depending on the player’s actions. This is how the game changes the player's vulnerabilities instead of processing his hitbox. Each enemy has its own hitbox version for a player in the water, a jumping player, a player on the ground, a standing / running player, and player bullets. The type of a bullet of a player is not taken into account when selecting a hitbox, so there is no difference between different weapons in terms of bullet collisions.
Object level collisions
Collisions between objects and the level itself are handled differently by the system. Collisions with a level are checked only when they are necessary. If a player needs to know if he has fallen off the edge of the platform, then the code updating the running player each frame requests collisions with the player’s legs until he finds that there is nothing under them. Most enemies do not need to know about conflicts with the level, and those who need them also check them if necessary at the right time. The only type of query supported is point checking and level collision maps, which we talked about above. The collision request returns a collision code for the tile under the point being checked (empty, impenetrable, water or platform), and then the caller, based on the results, performs the necessary actions.
3D collisions
Collisions on pseudo-three-level bases work in much the same way as on 2D-levels, but with certain restrictions, in order to preserve the effect of fake 3D. Enemies cannot collide with the player if their Y position in the screen space is too small (for example, too close to the top of the screen, that is, they are “in the depths” of the screen). This works because it is known that players at these levels are always at the bottom of the screen. Players' bullets have a timer, and they can collide with most enemies only if they were on the screen long enough to reach the “far” part of the screen. For enemies that can be destroyed in any position along the Z axis in the room (for example, for rolling grenades that need to be blown up before they roll and kill the player) the bullet timer is not used. Instead, it checks the flag that indicates the position of the player "standing / lying." If the bullet was fired when it was lying, then the usual two-dimensional collision detection is performed. This only works because recognition is performed between two objects, both of which are on the ground, where the position along the Y axis directly overlaps the position along the Z axis (for example, if it looks like two objects collide in 2D, then we understand that they are collide in 3D, but only along the surface of the earth). Due to these limitations, ordinary flat 2D recognition can be used to recognize collisions between objects in a pseudo-three-dimensional environment.
Player management
Player physics
The code controlling the low-level movement of players in Contra is very simple compared to what is possible in modern physics engines. At the same time, I always liked super-responsive management in old games compared to games, where there is more physically realistic management. The data associated with low-level motion in Contra is, in fact, only a position in 2D and speed. Both values ​​are in 8.8 fixed-point format. The units in them are pixels and pixels / frame, respectively.
In each frame, the game determines what speed the player should have and simply adds this speed to the player’s position. The horizontal speed at the beginning of the frame is always reset to zero, so there is no constant acceleration in the direction of the X axis (therefore, the physics of motion on ice in Contra cannot be realized). The enemy characters in the game do not even have their own allocated memory to track speed, because many of them do not move. Those enemies that usually move each frame “manually” add a constant value to their current position. In those rare cases when the enemy needs a more complex movement, it implements its own version of what they need to do and do not use the physics code shared with the players' characters.In general, there is no constantly working physical system in the game that controls the movement of players. Instead, each frame, depending on the player’s actions, performs certain calculations related to the player’s movement. For example, when a player runs along the ground, the control code sets the player’s horizontal speed and checks for collisions at the character’s feet to see if he shouldn’t start falling. However, the player’s vertical speed is not updated and is not even added to the player’s Y-axis position. Also, collisions are not checked above the player’s head, because it’s known that the character doesn’t move up during the run. This approach differs from the more general approach to physics simulation, when gravity constantly pulls the player to the ground, and the reaction constantly returns the player back to resolve the collision.Similarly, collisions while jumping, while the player moves up, are checked only over his head. When the peak of the jump is reached and the player starts moving down, the code switches to checking for collisions under the player. Also during the jump, vertical acceleration is applied by manually adding a fixed value to the player's speed along the Y axis in each frame. There is no common acceleration variable always added to the player's speed, acceleration has a non-zero value only when jumping. To calculate the player’s movement, no unnecessary collision checks and mathematical calculations are performed. Each frame performs only the most necessary calculations, and because of this there is a feeling of very responsive control without strange behavior.When the peak of the jump is reached and the player starts moving down, the code switches to checking for collisions under the player. Also during the jump, vertical acceleration is applied by manually adding a fixed value to the player's speed along the Y axis in each frame. There is no common acceleration variable always added to the player's speed, acceleration has a non-zero value only when jumping. To calculate the player’s movement, no unnecessary collision checks and mathematical calculations are performed. Each frame performs only the most necessary calculations, and because of this there is a feeling of very responsive control without strange behavior.When the peak of the jump is reached and the player starts moving down, the code switches to checking for collisions under the player. Also during the jump, vertical acceleration is applied by manually adding a fixed value to the player's speed along the Y axis in each frame. There is no common acceleration variable always added to the player's speed, acceleration has a non-zero value only when jumping. To calculate the player’s movement, no unnecessary collision checks and mathematical calculations are performed. Each frame performs only the most necessary calculations, and because of this there is a feeling of very responsive control without strange behavior.There is no common variable of acceleration, always added to the speed of the player, the acceleration has a non-zero value only when jumping. To calculate the player’s movement, no unnecessary collision checks and mathematical calculations are performed. Each frame performs only the most necessary calculations, and because of this there is a feeling of very responsive control without strange behavior.There is no common variable of acceleration, always added to the speed of the player, the acceleration has a non-zero value only when jumping. To calculate the player’s movement, no unnecessary collision checks and mathematical calculations are performed. Each frame performs only the most necessary calculations, and because of this there is a feeling of very responsive control without strange behavior.At the next, higher level of control, a set of possible player states is usually determined over a low level of physics, and then each frame is updated by the player according to the current state. In Contra, this system is actually implemented, although a single value is not used for the current state of the player. Instead, players have several groups of flags denoting the current state of the player. They can be divided into groups of jump flags, fall flags, and flags in the water. When a player is on the ground, all of these flags are cleared. If he presses A to jump, the jump flag is set, and several other flags related to the jump are updated according to the direction of movement and direction of the character during the jump. A similar set of flags exists to drop. This statein which the player moves when falling from the edge of the platform or jumping down through the ground. The flags of being in water are used only on the first level, when a player enters the water and starts to swim. These flags are never used at the same time (for example, the jump and fall flags are never set at the same time), so I don’t understand why they didn’t use a single state variable instead. Usually, control functions start by testing various flags, and then, depending on the flags set, go to the desired parts of the code.why not use a single state variable instead. Usually, control functions start by testing various flags, and then, depending on the flags set, go to the desired parts of the code.why not use a single state variable instead. Usually, control functions start by testing various flags, and then, depending on the flags set, go to the desired parts of the code.Details on the management
Running in Contra turns on / off instantly, unlike some other games, such as Super Mario Bros., which add momentum to a player’s character. As I mentioned earlier, as a result, the horizontal speed of the player at the beginning of each frame is reset to zero, and then updated to the desired value depending on the player’s actions. If a character runs along the ground, and the player stops pressing the “left” or “right” button, the horizontal speed remains zero and the character instantly stops.Behavior when jumping and falling is a little different: starting to move forward or backward, you will continue to move in the same direction until you either come across something or press the backward button to move in the other direction. Starting to move horizontally in the air, it is impossible to stop moving horizontally until you land. This is similar to a spin jump from Metroid and is different from behavior in games like Mega Man, where you can stop moving in the air if you stop pressing the direction button. Jump behavior is implemented through a pair of flags that are part of the jump flags described above. When you press left or right during a jump, the corresponding flag jumps left / right is turned on, but when you release the buttons, the flags are not turned off.The horizontal speed in a jump is determined by the current state of the flags, and not directly by the input of the buttons.In addition, when you press the "down-jump" Contra allows you to jump down through the ground on the lower platforms. The same mechanics are used in games such as the Chip 'n Dale Rescue Rangers on the NES, but not in other games with similar "one-sided" platforms (which can be flown through with a jump upwards, but stop on them when falling down), such as Super Mario Bros. 2. In Contra, when a player presses A while simultaneously holding down, the character’s fall flag is set instead of the jump flag. To allow it to pass through the ground, the game records the space on the screen, which is 20 pixels (a little more than the full collision tile) below the player’s current position on the Y axis. Then, when the player falls, a collision check, which is usually performed at the player’s feet in a fall state, is skippedwhile the current position of the player on the y-axis is above this recorded value.Other topics
To conclude with a discussion of Contra, I will consider other topics that are not worth a detailed analysis. Some of these topics relate to the details of game programming, others are simply interesting facts about the game itself, which I did not know before.Random numbers
As a source of random numbers throughout the game, a single global eight-bit value is used. The update method in each frame of a random value is quite interesting in that it does not use any particular algorithm that can be called each frame and obtain the next value of the sequence. Instead, the next random value is obtained by going through a small loop while the game is in standby mode for the start of the next frame (waiting for the vblank interrupt). At this time, the game constantly adds the value of the current frame counter to a random number, again and again, until the NES video equipment gives the game a signal that it is time to process the next displayed frame. One of the consequences of this approach:the resulting sequence of random numbers strongly depends on the exact execution level of the CPU cycles and the interaction between the CPU and video equipment. This means that even if two emulators ideally implement the logic of the CPU instructions, they will still generate different random sequences if their timings are not accurate. Obvious evidence of this can be seen in the Contra demo mode, which uses a recorded stream of pressed buttons. The demo must be deterministic and play the same way every time. Below is a comparison of one frame of the demo mode running on two different emulators. Notice that for a running soldier, created randomly in the lower right corner of the screen, a single creation was chosen on the Nintendulator, and a triple on NESten. It happened because a sequence of random numbersgenerated by two instances of the game, is not the same. Fortunately, the influence of random numbers is small enough and cannot completely disrupt playback in demo mode.
Array structure
Contra stores all enemy and bullet data in an array structure format, rather than in a more object-oriented structure array format. The reason for this has nothing to do with using the CPU cache or SIMD instructions, as you might expect. This is simply a consequence of the fact that NES has 16-bit memory addressing, but the CPU registers are 8-bit. If you need to access data indirectly through the pointer, then you cannot store the address of the object in the pointer and rigidly set the offset to the desired item in the load command, because the address of the object may not fit into the CPU register. Instead, you need to turn everything around and hard set the 16-bit address of the array of elements for several objects in the load command. After that, you must use a register to store the offset inside the array to get the element related to the desired object.In Contra, all actions are performed in screen space. The positions of players, enemies, and bullets are stored and processed in the coordinate system of the screen (for example, position 0 always means a point in space located on the left side of the screen, regardless of where the screen is scrolled to the level). At first, this optimization seems counterintuitive, because storing all the positions in the screen space will make such scrolling much more difficult. In this case, to simulate scrolling, instead of changing the only variable position of scrolling, you need to manually move the position of each enemy, player, and bullet each frame. However, in the end, this solution turns out to be good, because it allows you to save much more per frame than to spend. Scrolling is harder to implement.but in fact it all comes down to adding just one number to the position of the object when it is updated frame by frame. And often it just adds one extra CPU command for each object. The big gain is that now all calculations related to the positions of objects can be performed through eight-bit values ​​(the NES screen has a resolution of 256 × 240, so you can determine any position on the screen with pixel accuracy with just one byte for X and Y). Since NES natively supports only eight-bit arithmetic, as a result, these calculations are obtained much faster than emulation of 16-bit arithmetic to control 16-bit positions in the world. Of course, this only works for games like Contra,in which the relationship between the space of the world and the space of the screen is a simple translation without scaling and rotation (that is, the transformation from the space of the world into the space of the screen turns out to be commutative with other translations, even though the application of transformations in general is not a commutative operation). If the camera could move away, the objects in the game would start moving too fast, and if the camera turned, the objects would move in the wrong direction.then objects would move in the wrong direction.then objects would move in the wrong direction.Hidden behavior
Some parameters Contra changes depending on how many times a player has completed the game at a time, and on what kind of weapon he has in his hands. Below is a complete list of differences, where FINISHED will be 0 during the first pass, 1 during the second pass, and so on. GUN is 0 for the default weapon, 1 for the flamethrower (Fire, F), 2 for the machine gun or laser (Machine gun, M and Laser, L) and 3 for Spread (S):- The health (HP) of the intermediate boss and the last boss of level 8 = 55 + (16 * FINISHED) + (16 * GUN)
- HP intermediate boss shells level 8 = 2 + FINISHED
- HP shooting mouths in the walls of level 8 = 4 + (2 * FINISHED) + GUN
- HP level 8 scorpions = 2 + FINISHED + GUN
- HP cocoons around the last boss level 8 = 24 + (2 * FINISHED) + (2 * GUN)
- HP boss level 6 = 64 + (8 * GUN)
- - FINISHED
- FINISHED GUN, FINISHED 0, ,
- GUN
- GUN,
- GUN, 2 4,
- GUN 3, 8 ,
- The speed of falling on a scorpion player from the ceiling in combat with the last boss increases in proportion to the value of GUN
Conclusion
Contra is one of my favorite games at NES, and as a game programmer I really enjoyed learning the details of her work. It always amazed me how game developers of that time could extract so much from the ridiculously weak and limited hardware compared to modern consoles. If you have something to say about this article, contact me so that I decide whether to continue, and if so, which games to consider further. You can find me on Twitter: @allan_blomquist .