Stuart Cheshire
Sidney Sussex College
It is important to notice that the program only uses the routines in a one-way fashion. This means that it only ever makes calls to instruct the graphics routines to display something on the screen, never to enquire about what is currently displayed. This is because the behaviour of the simulated World is in no way altered by whether or not the player is watching it on the screen. It cannot even be assumed that the player's tank[13] will be on the screen, because, although it normally has to be, it may not be, if the player has selected a remote view from one of his pillboxes.
The solution was to define over 200 map characters, including corners in roads, T-junctions, bends in rivers, large compound buildings etc. A complex set of adjacency rules were designed, to decide which character to draw for each map square, according to what terrain surrounds it. For instance, a piece of road with water on both sides of it is drawn to look like a bridge. The rules are very complicated, and very arbitrary, so they are not worth describing here, but when looking at the screen photographs remember the meagre data from which the picture is derived.
The screen display,
including the status indicators down the right-hand side of the screen.
The Garden
The Lake
The Marsh
A Building Complex
The Forest
The Harbour
Rivers
The Maze
A sea-based fortress.
Most games have a single main loop which goes through all the objects on the screen -- the player, bullets, the monsters etc. -- and for each one it rubs them out, moves them by altering the coordinates in some appropriate way, and then redraws them. This approach was not applicable to the project for two reasons.
It would seem sensible to consolidate these changes, so that first, all the erasing (redrawing background squares) is done, then all the characters are moved, and then all the redrawing is done.
In the case where setob is called to alter an existing object, the question arises of how the routine knows which object on the screen to alter. For example, when a call is made to move a tank, there may be many tanks on the screen, so, while it is clear where the new image should be drawn, it is not clear which old image should be erased.
For this reason, all objects must be identified. A simple naming convention is used to assign a one byte identifier to each character on the screen, and this identifier must be used whenever calling setob to draw a new object on the screen, move an existing object, or erase an old object. The identifier is generated in a simple way. The bottom four bits are the player number of the player that the object belongs to. The top four bits identify which actual object in the game the character represents -- whether a tank, or a shell, or a running man or any other kind of object.
All shells belonging to a particular player are given the same identifier. This is because they move so quickly that it is not worth identifying them individually. It is only important to identify a character on the screen if it is likely not to have moved between screen refreshes. If it hasn't moved, then we don't want to waste time (or make it flicker) by erasing and redrawing it unnecessarily. Since shells will always be moving, it will never be the case that we want to leave one as it is and not redraw it in a different position.
When a new packet arrives from a player, all his shells are erased[14] together, and then they are all redrawn at their new positions using the data in the packet. As long as the identifier correctly indicates which objects in the table are shells belonging to this player, so that they can all be erased, then it is not important which particular shell is which.
If all the necessary background squares were redrawn (to erase altered objects), and then all the new objects were drawn, and this whole process could take up to 1/4 of a second, then the time between erasing a foreground object and redrawing it again would be noticeable. Objects on the screen would appear to flash on and off when the update was slow.
The time between calls to refresh is negligible -- it is by far the most significant of the foreground routines. If characters are erased-drawn-erased-drawn-erased-drawn, etc, and the time for each process is roughly equal, then characters are invisible for roughly half the time. Even if the screen is redrawn 25 times a second, characters will only appear on alternate frames and will flicker and have a `ghostly' appearance. This is not acceptable. Characters must appear solid and real if the `feel' of the game is to be right. It is important that the player gets `drawn into' the alternative reality presented by the game. Anything -- such as seeing that characters are actually being drawn and rubbed out -- which reminds the player that this is a computer program, will weaken this illusion.
The ideal solution is to use a double-buffered framestore (ie prepare the picture off-screen and then swap it in during the vertical retrace[15]), but there is not enough memory in the BBC micro, so this is out of the question.
The aim is to keep the ratio of `time on screen' to `time not on screen' as high as possible[16]. To this end, we want to redraw a character as soon as possible after it has been erased, and not after we have finished all the erasing. This is what the mechanism described below is designed to do:
It does this by searching the object list to see if there is already an object with the given id. If there is, and it has not changed, the routine returns without doing anything. If it has changed, it flags for redrawing all the background squares which fell underneath it[18], and then writes the new character data into the table and flags it for drawing too on the next refresh.
If the object was not there before, then it has nothing to erase, but just writes the character data into the table and flags it for drawing on the next refresh.
This enables us to both draw new objects and move them once they are on the screen. The only remaining action required is to erase the character depicting an object which has now left the screen. Since all characters are 16x16 pixels square, or less, any character with an x coordinate of 0 is totally hidden off the left-hand side of the screen. Thus specifying 0 as the x coordinate of a character causes nothing to appear on the screen. This fact is used to eliminate the need for a separate erase-object routine. Instead, setob is called, with the object id identifying which character is to be erased, and the x coordinate set to 0 to show that nothing should be drawn in its place. The only remaining worry is that this may cause the object list to become cluttered with old characters which are not actually on the screen. This is not in fact a problem. The setob routine automatically decides not to store any information about any character which has no pixels visible because it falls entirely within the border around the edge of the screen, and all characters with an x coordinate of 0 fall into this category.
As each square is examined, two things are done:
(i) if the flag is set, the square is redrawn.
(ii) to minimize the time between redrawing the background square and replacing the foreground characters on top of it, any foreground objects with this as their key square are immediately redrawn. Since they have been sorted into order, this is easy. At each square, only the first object in the list has to be checked. When it is found that its key square is the one which was just redrawn, it too is drawn and then discarded from the list, so that now only the new front object in the list has to be checked each time, until its key square is found, and so on, to the end of the list.
![]()