
Many of us grew up on "Tanchiki", "Mario" and other imperishable masterpieces since the dawn of the gaming industry. Sometimes it is pleasant to remember how we spent days on end hanging out with friends at TV screens, changing joysticks like gloves. But time does not stand still, and some interests are replaced by others. However, sometimes the love of good old toys does not fade.
I attribute myself to people like this, and my interest in old games went in the direction of reverse engineering, which led me to the IT-sphere, where I ended up with ends.
I want to tell you about the iron monsters from the famous Battle City game (in common “Tanchiki”) from the equally famous Nintendo Entertainment System console (NES for short, in Russia its Chinese clone “Dendy”) is under the hood. At one time, this information seemed rather curious to me - I hope, it will seem the same to you.
Prehistory
Several years ago, studying various graphic libraries, I set a goal to write an exact clone of “Tanchiki”. It so happened that just at that moment a friend of mine was engaged in creating a rather interesting
remake by modifying the code of the original game, so I decided to consult with him (by the way, thank you very much for his advice and assistance in preparing the article). Then I was struck by what methods implemented some functionality. So I decided, years later, to share this knowledge with the public: after all, this is part of the history of gamedev, which may seem interesting both to lovers of the classics and developers of modern games.
I would like to say a special thank you to the person under the nickname
Griever for his efforts in decompiling the game. First of all, thanks to the source code he received, it became possible to grasp the essence in such detail.
')
Level maps
Maps in Battle City are made up of 8x8 pixel tiles. The whole background, seen on the screen, is a map - hypothetically, even with the counter of lives, you can interact, but in fact this, of course, will not succeed.
However, in the serialized form, the levels are stored more compactly: for this, tiles are arranged in blocks of 2x2 tiles. In total there are 16 types of blocks, which are listed in the table below. Only the main part of the map (13x14 blocks) is stored, on which tanks drive - it makes no sense to keep static walls and supporting information. 4 bits are used to record one block, so the entire card occupies 91 bytes.
Block | Code | Block | Code | Block | Code | Block | Code |
 | 0 |  | one |  | 2 |  | 3 |
 | four |  | five |  | 6 |  | 7 |
 | eight |  | 9 |  | A |  | B |
 | C |  | D |  | E |  | F |
Unlike blocks, the variations of the tiles themselves are much larger, namely, 256, i.e. exactly as much as one character generator page - a video memory section, where each tile corresponds to an index from 0 to 255. Tiles are used both to create a level environment and to display information about the number of enemy tanks, lives, etc. But directly in the elements of their level, only 22 are used - 6 of them are used to form the blocks listed, the remaining 15 are additional tiles of brick blocks, it is worth discussing them in more detail.

If the map consists of 8x8 pixel tiles, then how is it possible to destroy brick mini blocks 4x4 pixels in size? The fact is that in fact there are no “bricks” of this size, instead there are 16 types of ordinary tiles - one for each state. Those. when hit by a 4x4 block, in reality a whole tile is replaced, and the brick itself goes into another state, depending on where the projectile hit.
Therefore, by the way, when destroying a part of a brick block, a tank cannot enter the vacated space - the tile itself was not destroyed.
The location of the respawn points and the eagle on the map are fixed, the objects will appear in their places regardless of what is in the place of their appearance. This happens if in the built-in level editor to “brick up” these areas.
If it is interesting to someone to work on mapping, then for these purposes there is a good editor
Quarrel , which works directly with the image of the game.
Random Number Generation
The task of generating random values for a long time did not give rest to humanity. It is impossible to obtain a truly random value programmatically, so the task was often reduced to generating pseudo-random values with a distribution close to what is characteristic of random ones.
For a computer with a fully deterministic behavior, generating pseudo-random numbers is even more challenging. For example, each time you start, exerting the same impact (pressing the same buttons at the same time), we will get the same course of the game - even the gameplay records are based on this principle, where the buttons pressed at that moment are stored for each frame . But for us, this is not so critical: it is important to get as “random” values as possible within a single session.
Consider how things are going with Battle City. Here, to generate a pseudo-random value, several “random” entities are used - this is the previous random value, the seconds counter and bytes of the so-called alternating bytes. page zero — the first 256 bytes of RAM that the NES processor can address faster and easier than the rest. That is why the zero page contains the most frequently used, and as a result, the most frequently changed variables, which ensures a more even distribution.
As a result, the final value of the number is influenced by many factors: the buttons pressed by the player, the coordinates of all the tanks and bullets on the screen, the number of points of both players, the state of the set of timers (such as bonus) and even the state of the sound channels! Of course, this is not a complete list.
The expression itself, by which the next random number is calculated, is quite simple. I will not go into details, just show how it would look in C:
uint8 seed = 0; uint8 index = 0; uint8 rand() { seed = (seed << 3) - seed + secondsCounter + zeroPage[++index]; return seed; }
As can be seen from the code, we can at any time obtain a pseudo-random value in the range from 0 to 255. Moreover, its distribution is quite adequate, as can be seen from the imperishable gameplay.
Bonuses
The game has exactly seven types of bonuses - a helmet, a clock, a shovel, a star, a grenade, a tank and a pistol. In the original game, the latter is not used at all, but it can be found in pirated versions - there it makes an instant maximum upgrade, i.e. equivalent to the three stars taken (and in some variations also allows you to destroy the "forest"). The actions of the other bonuses, I think, are known to everyone. But just in case, let me remind you: the clock makes the enemies freeze, the shovel creates armor around the headquarters, the star increases the power of the tank, the grenade destroys all enemies on the screen (no points are awarded for them), and the tank increases the number of lives.

By the way, in the pirated versions with a pistol there is another bonus - the ship, which allows you to move through the water. Also, in some modifications, bonuses can be taken by opponents, which radically changes the balance of the game. There were even entire collections of such modifications, where the versions differed in various similar trifles.
Apparently, it was originally planned to include eight bonuses in the game, but in the end there were six of them left. However, when a bonus appears, a random value modulo 8 (from 0 to 7) is taken, which is an index in the bonus table, where the star and grenade re-occupied the place of bonuses not included in the game. As a result, the probability of their loss is 1/4, while the probability of losing other bonuses is 1/8.
The bonus clock and helmet act for 10 seconds (during player respawn, the helmet lasts 3 seconds, including the time for respawn), the spade bonus is 20 seconds.
Bonus carriers themselves - flashing tanks - appear if the enemy has 17, 10 or 3 tanks left in the hangars. Those. tanks 4, 11 and 18 are bonus.
It is worth mentioning that the second by the standards of the game lasts a little longer than usual: the NTSC version of the console has a refresh rate of 60 frames per second, and the easiest way to count the seconds is to increase the number of seconds in each frame if the frame counter modulo 60 is zero . But to simplify the calculations, and also to zero the frame counter as a result of overflow, it does not affect the calculation in this way, this number is rounded to 64 (0x40) - to take a number using this module, it is enough to perform a logical multiplication by 0x3F. It turns out that the game second is 1.06 real seconds, but this does not apply to time periods, which are measured by the number of frames.
An interesting fact, known to everyone familiar with the game: before you stop acting, the defense around the headquarters starts to “blink”, turning into a brick one, then an armored fence (this happens in the last 4 seconds of the bonus action). At the same time, even if the protection around the headquarters was partially or completely destroyed, each time it blinks, it is restored until it is completely turned back into a whole brick wall.
Intelligence opponents
Of course, AI in tanks is not a tactical analyzer, but some logic is still calculated.
To begin with, the game has a dynamic complexity. As a measure of complexity, the value of the delay between the respawns of enemies is used, which depends on the level and number of players - the more these values are, the faster the respawn will occur. The respawn time in frames for a given level (numbering from zero) and the number of players can be calculated using the following formula:
190 - level * 4 - (player_count - 1) * 20
. To get the time in seconds, simply multiply the result by 60.
There are three periods of tank behavior: at first they move erratically, then they pursue the players, and, finally, they begin to attack the headquarters. The duration of the first two periods is the same and equal to eight periods of respawn. That is, dividing the respawn time by 8, we get the duration in seconds (or, multiplying by 8, we get the same time in frames) - for example, for the first level and one player it will be 23 seconds. The third period will last until the seconds counter is reset (reaching 256) and the cycle of periods begins anew.
All this enemy tactics are based on the command system, for which 8 bytes are allocated: two for the players' tanks and six for the enemy tanks. The four high-order bits of such a byte are used for the command, the remaining four low-order ones are used for its arguments (as a rule, this is the direction of motion). The result is 16 teams, for each of which there is a handler. Handlers are called once per frame for each tank after movement has been processed.
Surface consider the existing commands:
- 0 - NOP (no tank)
- 1..7 - to process an explosion of a tank (by command for an animation frame)
- 8 - process status (gliding on ice, etc.)
- 9 - change direction
- A - check for tile border crossing
- B - move to headquarters
- C - move to the second player's tank
- D - move to the first player's tank
- E..F - spawn control commands
There is a function of changing direction, which, when called up, changes the direction of the tank randomly in the first period of behavior, in the second period it gives tanks with even numbers a command to move to the first player, from odd ones to the second one, and in the third one gives a command to move towards headquarters.
When an enemy tank crosses the border of a tile (when both coordinates are multiples of eight), there is a 1/16 probability that this function will be called for the tank. If the coordinates of the tank were not multiples of eight, or the function of changing direction was not called, and with all this the tank rests against an obstacle, then with probability 1/4 the following will occur: the tank will change direction to the opposite if at least one of the coordinates is not a multiple eight, otherwise the tank will be given the command to change direction.
When executing the change direction command, something else happens: the function described above is called with probability 1/2, otherwise, with equal probabilities, either the previous or the following directions from the list are taken cyclically: up, left, down, right (i.e., the tank turns hour or counterclockwise).
You can imagine all this logic in the form of such pseudocode:
void changeDirection(Tank tank) {
An interesting situation is obtained when a tank rests against an obstacle or a wall - since in this case, even if the tank did not turn, one of the coordinates is always a multiple of eight and there is a tile obstacle in front of it, the probability of turning increases several times. Therefore, the tanks practically do not get stuck on the spot: even getting into a corner or niche, they leave there pretty quickly.
No less interesting is the situation with enemy shots. By themselves, they occur completely apart from the tanks - each enemy has one bullet, and the shot in each frame itself decides whether to launch this bullet or not. If the bullet is already in flight, the shot does not occur, otherwise there is a 1/32 chance that the tank will fire. In this case, the bullet is on the front border of the tank at its center and inherits the direction of its tank.
It should say a few words about the behavior of the players' tanks in the demonstration mode. Everything is simple: if there are bonuses on the screen, the tanks tend to them (they can get stuck on the way if the bullet flies past the obstacles they are resting on). Otherwise, they simply pursue the first available tank, and if there are no such tanks, they stand still. Projectiles are manufactured according to the same rules as enemy tanks. Fire on its own in demo mode does not give effect.
Collision handling
The collision calculation has always been a fairly resource-intensive task, which can be critical for a processor like the NES. The calculation of the interaction between the tanks with the implementation "head on" would require to compare each pair of tanks, which, despite their small number, is still quite an expensive procedure for a processor with a clock frequency of 1.76 MHz. But do not forget that in addition to tanks on the map and fired shells. Therefore, the developers went to a rather interesting trick.
The fact is that each tank ... "draws" under an invisible wall. Thus, collision detection between tanks occurs at the collision detection stage between tanks and level elements, for which it suffices to determine whether there is an obstacle along a certain coordinate. It also has side effects: many probably noticed that if you try to “drive” behind a tank moving forward, the player’s movement is blocked until the tank moves off for some distance - just at that moment he moves to the next tile, “drawing” a new one wall and "erasing" the old.

The same effect can be noticed when a shell hits the tank. Visually, contact occurs at different distances from the border of the tank's sprite (both inwards and outwards) - from 0 to 7 pixels. But if tanks paint a wall under themselves, then bullets do not. Moreover, the calculation of collisions between shells takes most of the frame time! And in special cases it may not even be enough, then all these operations will be transferred to the next frame, and the current one will not be changed. Those. Yes, and in “Tanchiki” there are lags.
When a projectile hits one player differently, the second loses motion control for 192 frames, i.e. for three seconds playing. But in the demo mode this does not happen.
It is noteworthy that the projectile has two points by which collisions are checked. They are located on the sides of the nose of the bullet, and in fact two collisions can be processed simultaneously (otherwise, after detecting at least one of them, the projectile disappears). This happens, for example, if you aim at the center of a brick block: a whole strip is destroyed.
Another interesting thing: slow bullets (more detail about the speed below) are processed not every frame, but through the frame. Theoretically, a situation may arise when the game will slow down in every odd frame, and the processor will not have time to reach the handling of collisions of such a bullet, postponing it. But in the next frame, he will not have to handle it at all! Due to a similar set of circumstances, slow bullets in some extreme cases can fly through the walls when the game slows down a lot.
Motion
The speed in the game is determined by the number of frames dropped when the coordinates change. So, for example, the player's coordinate changes every three frames out of four, the fastest enemy in the game - each frame.
The same applies to projectiles - there are two types of them: fast, flying at a speed of 4 pixels per frame, and slow, having a speed of 2 pixels per frame.
Tank | Speed, px / frame | Projectile type | Tank | Speed, px / frame | Projectile type |
 | 3/4 | Slow |  | 2/4 | Slow |
 | 3/4 | Fast |  | 4/4 | Slow |
 | 3/4 | Fast |  | 2/4 | Fast |
 | 3/4 | Fast |  | 2/4 | Slow |
Since it is necessary to handle collisions when moving, it would be appropriate to optimize the calculations, distributing them evenly across frames. With the simplest approach, it would be sufficient to handle the movement of all enemies in even frames, and in odd frames, only the movement of the nimble BTR. But then odd frames could be overloaded, and the game would just lag.
In fact, the load is distributed quite cleverly. A “XOR” is made between the frame number and the tank number, and only then the parity / oddity of the result is checked. The result is that in even frames one half of the slow tanks is processed, and in odd frames, the other one.
And some information about slipping on ice. Everything is simple: after the player releases the direction button on the ice, the tank itself will go another 28 frames (i.e. it will pass 21 pixels) or until it leaves the ice. During this period, the state of its caterpillar track does not change and the sound of the drive does not play, and if you press the direction button already on the ice, a characteristic sound will be played if the slip timer at that moment is zero.
Easter eggs
Easter eggs
are found in many games of the time, they have not avoided Battle City.
If on the title screen you select CONSTRUCTION mode, enter and exit it 7 times (by pressing START, START), then hold down the down button on the first joystick, press the second A button 8 times, then hold down the first joystick on the first joystick, on the second button B 12 times, and finally press the START button on the first joystick, then we will see a tragic love story.

In addition, in the game you can find unused data - the names of the developers (RYOUITI OOKUBO, TAKEFUMI HYOUDOU, JUNKO OZAWA) and hieroglyphs meaning the name of the protagonist lavstori and the name of the street (possibly on which he or his beloved lived).

Perhaps here I will not go into details, because it is described in detail in the link just above.
Epilogue
In conclusion, I want to say that before the creation of games was, in a sense, still a great art than it is now. This is a tremendous skill - to meet the limitations of the hardware, and at the same time create a good game. This is akin to the demoscene, which, in essence, pursues the same goals - to create masterpieces where it is difficult to imagine.
Nowadays, all this is becoming a thing of the past, allowing us not to think too much about how to implement it. In my opinion, this greatly lowers the threshold for entering the game development process, which gradually but surely turns it into an ordinary business rather than an art. Although, perhaps, I'm just too aesthetic in this regard.