Graphics and animation techniques
 | DIBs and transparency We used Windows DIBs to implement all of our graphics objects.
Every bitmap displayed or stored internally in the game is encapsulated by the CDIB class.
Since every DIB is rectangular, we had to support transparency when displaying it.
This is achieved using a color-key. A specific palette entry that, when copying a region
from one DIB to another, only pixels that do not have this palette value get copied.
Other techniques exist (for example, creating a bit-per-pixel map of transparent areas and
using AND and OR BitBlts to filter out the background pixels).
We favored this straightforward technique over the other techniques since it requires no
additional memory and is very fast for small bitmaps (90% of our bitmaps are smaller than
40x40 pixels).
|
 | The ImageManager The CImageManager class
handles all of the chores of image management and display.
Its main features are:
|
Rendering and game objects techniques
 | Pixel-level collision detection All images in
the game are stored in rectangular DIBs.
To test if an image (e.g. a shell) collides with another image (e.g. a tank), we must
apply pixel-level collision detection instead of the simple rectangular-level collision
detection.
Most of the images (bonuses, bullets, bombs etc.) don't require this level of detection.
Only the game objects that require pixel-level detection are supplied with a pixel-lookup
table generated automatically (on image load time) and stored along the image's DIB in the ImageManager.
All game objects support a collision detection method (against another object).
We first do a rectangle-level collision detection to rule out the easy cases. Only if the
result is a hit and both images hold a pixel-lookup table, a slower but more accurate
pixel-level detection (only within the intersection of the two rectangles) is made.
|
 | Objects interaction All objects in the game inherit from a single
class - the CGameObject.
Each and every object can perform:
 | Return its state at a given time (dead or alive). |
 | Tell its type. |
 | Return its location on the map. |
 | Return its size. |
 | Return its current image. |
 | Return its height (z-order). |
 | React to another game object:
 | Explode with a variable intensity. |
 | Block with a variable level (e.g. a wall blocks by 100% while a water pond reduces speed
to 50%). |
 | Give a bonus of some kind (for bonus objects). |
|
During the game, in the GameManager's main loop, all objects are traversed and their
status (state, position, image etc.) is gathered. All objects are checked for reaction
against all other objects (N x N complexity at the most) in a single rendering pass.
|
 | Objects list and z-order All the objects in the
game are stored in a special list (encapsulated in the CObjectsList class).
Internally, this list is implemented as 4 statically allocated arrays (for performance
reasons).
Each array corresponds to a specific z-order of the object.
We support 4 different heights (z-orders):
 | Ground level - only one object has this level: the game map (terrain). |
 | Lower level - has objects that are on the ground (tanks, mines, bonuses etc.). |
 | Higher level - has objects above the ground (bullets, shells, etc.). |
 | Sky level - has the highest objects (bomber planes, game-over animation). |
The GameManager traverses the list of objects during rendering. It starts with the
ground level and ends in the sky level. This ensures objects are drawn in the correct
order for a more realistic experience.
|
 | Map terrain generation Each new game session, the hosting computer
creates a new map. The map object is responsible for its own creation (implemented in
CGameBoard).
To create a new map all the game board need are two parameters: the map complexity level
(1..20) and a random seed (short integer).
The game board object initializes the random mechanism with the seed (srand) which ensures
a pre-defined series of semi-random values form the rand() function.
All the obstacles on the map are selected randomly from now on. The walls are erected
first, then the various other obstacles are placed (ponds and rocks).
The host computer has to pick the random seed randomly (using the system timer).
When a client joins a game session, all it has to get are the two map creation
parameters. Since he uses the same map generation algorithm, it will have the same exact
map.
|
 | Constant frame rate To achieve a constant frame rate, the game
manager is placed in a dedicated worker thread.
Every frame cycle, it checks the time the entire frame calculation and drawing took. If it
took less than a single frame time (50 milliseconds per frame for 20 frames per second),
it goes to sleep for the remainder of the time (if it's not too small).
This frees up CPU for other threads (user-interface and the CommManager) while maintaining
a constant frame-rate.
The GameManager thread is given THREAD_PRIORITY_HIGHEST for better performance.
|
Communication and game synchronization techniques
 | The Judge Since the game is distributed among more than one machine,
various machines can perceive a different game-state at the same time and reach
conflicting conclusions. For example, two tanks believe they both picked up a single bonus
while only one of them did.
A special entity, called the Judge, had be created to solve such problems and act as a
mediator between players.
We chose to implement the Judge as the player on the host machine.
This was chosen for 2 major reasons: the host machine is usually stronger and can take the
extra CPU load and the player on the host machine is placed in the middle of the classic
server-clients star configuration and has the best view of the game.
Most of the time, the players cannot create conflicting decisions.
Local tanks decide when they die, where they are, what's their ammo status and their
shield level. All they do is transmit that information to other tanks periodically for an
update.
However, the judge is responsible for the following game session properties:
 | Existence and identity of participating tanks (informs new joining clients of the tanks
in the game). |
 | Mines on the board and their locations. |
 | Current bonus type (if exists). |
The host, when receiving this update information from the judge (the local player),
stores it locally.
When a game-state snapshot arrives from a player's machine
(non-judge), the host checks that information against the one published by the judge and
sends corrections accordingly.
|
 | Game snapshots and checksums Each player in
the game, sends a game-state snapshot to the host periodically (once a second).
This information includes:
 | The number of active tanks. |
 | The current bonus type (if exists). |
 | Local tanks' position. |
 | A checksum of the active mines (divided into sectors). |
 | For each tank (local or remote):
 | It's zombie state (true or false). |
 | It's shield level. |
 | It's ammo count. |
|
The information regarding the local tank of the sending player is stored and
broadcasted to the other players.
The information about remote tanks (remote to the sending player) is compared against the
stored information (from other reports) and specific update messages are sent upon a
mismatch.
Not every mismatch generates an update message. Only mismatches that are critical
enough are considered update-message-worthy.
The information in the snapshot message uses checksums to preserve bandwidth while
still providing mismatch detection at the minimal required resolution. For example, all
the information about the mines in the board (can reach several hundreds mines) is stored
in 2 double words (8 bytes).
|
 | Zombie state
Every tank (local or remote) can become a zombie during its lifetime.
The zombie state indicates that the machine the player is using has network congestion
problems.The Zombie state is temporal and once the congestion goes away, the tank can
resume normal play.
While in zombie mode, the tank cannot move or perform any game actions but it cannot be
hurt or destroyed.
The host is responsible for determining which tank becomes zombie or alive again. It
does that constantly monitoring the players packet round-trip time.
In another safeguard mechanism the host employs, a client becomes zombie if no message
arrives from it for a long time (5 seconds). After about 30 seconds of inactivity, the
keep-alive mechanism of DirectPlay kicks in and disconnects the player.
|
 | Tank position synchronization Each player sends a game-state
snapshot every second to the host.
Included in this snapshot is the current position of the player's local tank.
The host, upon receiving this location information, broadcasts it to all the other
players.
Thus, each player receives up to 3 (there are up to 4 players in a game) position
information per second (depending upon network throughput).
Each position information should update its matching remote tank object at the receiving
client.
The only problem is that the information takes time to arrive (client A to host, host to
client B). By the time the client receives it, it's old news.
To solve this, every tank object keeps a cyclic history buffer. On every game frame (up
to 20 frames per second) the tank stores its current position and the current time at the
tail of the buffer, pushing the oldest information out of the buffer's head.
Once such a tank (remote tank object) receives a position update (from the network), it
finds the matching history location, calculates the drift and applies it to its current
location.
For example:
 | Player A drives the red tank (the red tank is a local tank at player A). |
 | Player B drives another tank but also sees the red tank on his screen (a remote tank
object). |
 | Player A sends a position update about the red tank at time 100. The position is
(50,50). |
 | Player B receives this update at time 120. |
 | Player B looks up the time of the incoming message (100) and finds the nearest time slot
in the history buffer of the red tank object (remote tank) he holds. It finds the slot and
reads the position there (at time 100) to be (47, 49). |
 | Player B now knows that, at time 100, the remote red tank he holds was 3 pixels by 1
pixel away from its real location. |
 | Player B updates the current location of the red tank he holds by that offset (from
80,80 to 77,79). |
This technique is based on heuristics but it proved to yield good synchronization
results even with very difficult network connections.
We keep a history buffer of about 1500 milliseconds for each tank (30 slots). This is
adequate since any latency greater than 1500 milliseconds is handled by the zombie mechanism.
|
Miscellaneous techniques
 | Maneuver sets In order to isolate the tank object from the keyboard
or the network, we came up with the concept of maneuver sets. A maneuver set is a set of
actions telling the tank what to do next (for example: move forward, turn right and fire a
bullet).
Maneuver sets cannot contain contradicting actions (turn right AND turn left).
Maneuver sets are encapsulated in the CManeuverSet class.
To make tanks behave the same regardless of their locality (local tank or remote
tanks), the maneuver set of each tank can be updated from exactly one of two sources:
 | The maneuver set of the local tank is updated by the user interface main thread.
When the user presses a key, the key is captured and checked against the defined maneuver
keys. If there's a match, the maneuver set of the local tank (there's only one local tank)
is updated, removing contradicting actions. |
 | The maneuver sets of remote tanks are received from the network and directly update the
corresponding tank object. |
Every tank, when it calculates its new state, checks to see if the maneuver set it uses
has changed from the last frame. If so, the tank asks the communication manager to send
the new maneuver set to the host. The host broadcasts it to all the other players.
|
|