The Archimedes Gamemakers Manual
Copyright © APDL and Terry Blunt 2002. All Rights Reserved
Chapter 3 - More Planning
3.1 Identifying Major Stages
The secret of managing a large sophisticated program is not to try to handle it all at once but to break it down into smaller sections and keep on doing so until you've got bits that are small enough to manage. The only real problem is then deciding where to start.
All properly designed games can be divided into the three principal stages below.
- Game loop
- Finalising - Endgame
Initialising will, of course, involve setting up the framework of the data structures the game requires, loading or defining score-tables and startup values. From the game players point of view, the initialising will consist of the title screen, instructions, and where relevant, the story line.
The game loop is the game proper, where the program spends most of its time.
Finalising would mean storing any sections of data, like score tables, that will be used next time the game is played and ensuring a tidy exit, preferably to the desktop. From the game players viewpoint, the endgame would have the closing messages, best score congratulations (or otherwise).
As you can see, we've already broken the game down to three much smaller parts, and we don't even know what the game is yet!
If we break this down further we might get something like the list below.
1.1 Title screen
1.2 Set up data structures and major game constants.
1.3 Load sprites and other data
1.6 Key / Mouse choice
1.7 Continue saved game request
2.1 Start game loop
2.2 Set up game level
2.3 Play level
2.4 Update scores, lives, level
2.5 If loop conditions OK repeat game loop
3.1 Endgame message
3.2 Position save request
3.3 Close program and return to desktop
If you just keep breaking the problem down like this you will soon find that you have it defined almost to program level.
You will see that I've put the bulk of the true initialising after the title screen, but before the rest of the program. This fools the players by giving them something to look at so that they don't realise how long the game takes to set up.
When you get to the main game loop begin by listing every action that takes place in the loop. Don't worry about the order at first, the main thing is to make sure you've not missed anything out. Once you have the list you can then begin to sort out what actions are dependent on others and get the order right with the appropriate tests. The list below is fairly typical for an arcade game.
- Check key press
- Check mouse
- Move player object
- Move Enemy object(s)
- Move missile(s)
- Check collisions
- Destroy objects
- Create objects
- Check time
Obviously not all operations will be taking place at the same time, so you can start by positioning those actions that have to take place every pass. This will form the framework that everything else hangs on, hence the importance of making sure you don't miss anything out at this stage. Notice that the collision check has to be made every pass to allow for any possible movement or creation of new objects. Below is another fairly typical list. This is for a draughts game and is in a more familiar pseudo code form.
Game loop start
Identify which player to move
If computer move then
Calculate best move
Accept player input
Check for game end
Repeat Game loop
The precise method you use to formulate your ideas and get them down on paper isn't too important. The important points are that, at a later date, you understand what you have written, and that the notes assist you in clarifying your thinking.
3.2 Data Structures
It should be around this point that you start to think about the variables you are going to use. You will probably be manipulating a lot of data in the body of your game and you will need to work out what form or structure that data should have. Where you have groups of objects or characters it is likely that you will eventually decide to use an array of some sort to contain this data, with a FOR-NEXT index variable picking off individual items. With the powerful array and matrix arithmetic available in Basic V arrays become particularly attractive for manipulating related data such as three dimensional vectors.
Names are most commonly held in strings or string arrays, but numeric values are less obvious. Normally integer arrays will be most practical. If your game seems to need an array of real numbers you should look again, as you may be using unnecessary precision. Bear in mind that in Mode 13, for example, pixel size is four graphics units so for drawing purposes you don't even need integer accuracy. When necessary you can sometimes increase integer accuracy by using the Basic barrel shifting operations to multiply up by, say, 1024 while performing calculations, then shifting back down again afterwards for your actual results.
Where you are expecting ARM code mixed with ordinary Basic the most efficient structures will probably be word aligned blocks of memory. These can be accessed directly as double words in ARM code and as indirected integers in Basic. Indices are simply barrel shifted to get the correct address as shown below.
;R1 contains the array address R2 contains the index
It may be best to think of all the individual data structures as a whole entity. Ask yourself if there are repeating types that can be usefully combined and handled by common procedures or function. If, for example, you are working on an adventure it might be wise to treat all data relating to characters as a single structure. If you call this structure an object you can define a set of procedures to handle the object. Internally they can be quite complex, with the separate arrays handled in appropriate loops. To the rest of the program they are closed boxes with an object number passed, possibly with some information being returned. Those familiar with other languages will recognise this as a primitive record type.
Whatever structures you finally settle on it is best to leave some spare elements. There is a good chance that later enhancements or final trimming of the game can use these to good effect, and you can then slot the modifications in with minimal disturbance to the rest of the program.
If your game is to be really attractive then it is vital that you get the layout right. This is something you will need to decide on quite early and spend a lot of time on. To begin just make outline sketches on paper. You may prefer to use !Draw or an art package of some sort for this, but unless you are good at handling them they may slow you down at this stage, so pencil and paper may be better.
Usually, with fast action games, it helps to keep the peak activity slightly below the centre of the play area. The slower the game the more you can afford to spread the action out without the player losing control of the game.
In most arcade games the play area is reduced and various bits of relatively static information placed on borders and panels around the edges. If you do this you must be careful about where on the screen you put the information. Figure 3.1 shows common arrangements for a range of games. I've only shown the bare bones of the layouts. As well as static detail in the borders in most games there would be more information to make the overall appearance more attractive.
The player can only really concentrate on one or two areas at a time, so the one I suggest for most fast games is layout 4. Here all the vital information is directly under the main game play area, and the player's peripheral vision should be able to pick up the less important stuff either side of this without any difficulty.
Layout 5 is the worst possible. Having information spread all around the edges of the screen will distract the players while reducing their chance of assimilating the information. This will destroy what might otherwise be a good game due to player fatigue; the very last thing you want!
The proportions of the various areas of the screen are also important. Notice how different each of the drawings of Fig 3.1 looks although they contain essentially the same information and have similar play areas. Normally rectangular outlines look better than square ones, although this is partly determined by the contents of each area. Drawing 2 would really only look good in either a vertically scrolling game or if large circular objects were involved. Where practical try to produce rectangles with height-width ratios as close to 1.6 : 1 as possible. Artists will recognise this as an approximation of the Golden Ratio . I don't know why but these proportions are generally regarded as most pleasing to the eye.
Some games use the entire screen as the playing area, so with these it is only the action that you need to concentrate on while in the play mode. Even so, you should give thought to the layout of score tables and message screens that appear at the end of each level. A simple 'Game Over' splashed across the middle of the screen looks very amateurish but is still surprisingly common.
It's a good idea to consider novelties, like the player's character slowly sliding down the screen or disintegrating. You could make the background fade out, or if you have some of the PD screen fade and dither routines, use these. Try having a different end to each level.
Score tables in particular can be very static and boring. You can liven them up considerably with animation. A few game sprites running around the edge is one possibility. Or a sequence where the words and letters are continuously dragged into place by the goodies, only to be shot up by the baddies.
3.4 Preliminary Testing
Before you develop your game too far you will need to check that the overall idea is really viable. The only practical way of doing this is to write as much of the essential parts of the game as possible, with dummy values and deliberate time delaying calculations, so as to nearly as possible reproduce the real game.
As you don't want to waste a lot of time with this you should just grab random bits of screen for sprites. It is their size and screen mode that is important, not their content. Similarly, you can use a procedure or function to read key presses and mouse clicks without actually doing anything with the results.
In this way you can quickly establish whether you are likely to have a lot of trouble with your game concept. If you find you are getting bogged down trying to track down a host of irritating but seemingly minor bugs then you shouldn't be afraid to take a deep breath, completely scrap the program but retain the game idea and try again with different data structures, screen layouts, and control procedures. Remember that if it's getting complicated, you're probably doing it wrong. You may, unfortunately, find your game isn't viable at all, but it's better to discover this sooner rather than later.
3.5 Time and Memory
Although the Archimedes is fast, there is still a real need to keep time consuming operations as compact as possible. This usually means some sacrifice in readability. Without getting too cryptic you should reduce variable and procedure names to the minimum readable.
3.5.1 Improving loops
Oddly enough, you can actually make significant speed improvements by increasing the number of variables. The two rather contrived FOR-NEXT loops below do exactly the same job, but the second version works up to 20% faster. This is because there are fewer calculations inside the loop. The more passes there are through the loop, and the more complex the calculations, the greater the difference becomes. For the loops shown, assume that a%, b%,c%,d%, e% and num%() have been defined in some other part of the program.
FOR I%=a%*b% TO c%*d% STEP a%*b%
FOR I%=X% TO Y% STEP X%
Where you have a choice of loop constructions write a test program to time the loops with various actions inside them. Make sure you've balanced the most repeated actions against the most complex ones when you do your timings.
Generally Procedures operate faster than Functions as Functions always return a result. Also passing parameters to Procedures and Functions, although desirable for clear bug free programming, is time consuming. This is particularly true of the RETURN keyword used to pass values back from Procedures. Therefore, especially inside repeating loops, it is better to plan data structures to use the minimum of parameter passing and as few local variables as possible. A sensible range of global variables will prove to be far more efficient, but is most important that you document these properly so that you don't try to use the same variable twice.
It is very rare for GOTO or GOSUB to be practical. If the program is of any size and the lines identified are well into the program then these commands will operate relatively slowly. These is only a speed advantage if the lines are very close to the beginning of the program as few lines will need to be stepped through.
3.5.3 Faster Printing
It is often worthwhile experimenting with small sections of a program to see how efficiency can be improved. The results can be surprising. Of the two lines below it is the second that runs faster. Actually about twice as fast.
IF A%=4 THEN PRINT TAB(0,5)"Done"
IF A%=4 PRINT TAB(0,5)"Done";
There are two reasons for this. The first is that in the second line, the Basic interpreter will assume the keyword THEN faster than it can actually decode it in the first line. The second factor, which is even more significant, is that the semicolon suppresses the newline normally produced by PRINT. This newline requires the sending of two characters through the VDU drivers, so the shorter the print string is, the more significant this becomes. Where A% has any value other than 4 there is much less difference in speed.
If you have a commonly repeated group of colour change, tab and string printing, combine the commands in a single print string, using the direct VDU equivalents for the commands. This should be done during initialising, then when required the string is printed with a single statement. The two extremes are shown below.
PRINT TAB(15,0)"Score = ";
a$=CHR$4+CHR$17+CHR$5+CHR$31+CHR$15+CHR$0+"Score = "+CHR$5
3.5.4 Arithmetic Variations
Mathematical calculations can produce quite a few surprises. Of the two lines below, the second will execute 20% to 30% faster while producing exactly the same result. This is because it only has to use a simply multiply instruction, while the first line has to be handled by a complex power calculating algorithm.
3.5.5 Decision Ordering
Try to arrange IF-THEN-ELSE constructions so the the first choice is the most frequently selected, and where practical use the single line version rather than the block structured one spread over several lines. Also where you have rarely realised IF conditions with ANDs it is much better to use the combination of stacked IFs. The two arrangements are shown below.
IF seldom% AND sometimes% AND often% PROCsomething
IF seldom% : IF sometimes% : IF often% PROCsomething
With the latter example if seldom% is FALSE, the rest of the line won't be evaluated at all. This can make a dramatic improvement in speed.
Where memory is more important than speed, such as for strings of instructions, use a simple loop that reads lines of data. If speed is more important then, in your initialising, read the data into an array. Each item will now be instantly accessible by its array index. You can sometimes get the best of both worlds by storing data in a file and reading it in at the start of the game, re-reading it on any restarts if necessary.
Where you are reading data, place the data immediately after the routine that reads it and use RESTORE+n, where n is the number of lines between the RESTORE line and the first line of data to be read. In a large program this is very much faster than restoring to an absolute line number. You should also put the data immediately after the ENDPROC of the reading procedure, not before it. In this way no processor time is lost stepping through data lines.
3.5.7 Look-Up Tables
A common method of increasing speed at the cost of memory is the use of look-up tables. This is particularly useful for things like vector calculations. For example, you can easily build up a table of sine values, keeping in mind that, in most cases, you only need to plot to an accuracy of four graphic units. Taking values from this table in your game will be dramatically faster than using any other method. Don't forget that you only need to cover the first quadrant, and that the sine table, looked at from the other end is, of course, a cosine table.
Almost any series of calculations can be put into look-up tables. This could be screen addresses, sprite locations and sizes, bounce directions, and even, in a simulator, equivalent prices for goods exchange. The list is limited only by your imagination and the memory available. The deciding factors are, whether the data can be ordered and indexed, and whether there will be sufficient speed improvement to justify the memory usage.
3.5.8 Screen Handling
Having high resolution 256 colours graphic modes it is tempting to use them regardless. There is a considerable processor time overhead with these modes, as well as the problem of swallowing up large chunks of valuable memory. You should work out just how many colours you really need, remembering that in say, Mode 12, all 16 colours can be redefined in fractional amounts of red, green and blue. Also, a game often looks better if the screen pixels are square. The higher resolution modes have very squashed pixels.
Changing screen modes can produce other useful benefits. When working in lower screen resolutions sprites take up less memory and execute faster. Sprite plotting can often present speed problems so two other ideas worth considering are reducing the number of sprites plotted at any time and reducing the size of the sprites, as smaller sprites can be plotted faster.
If you are using a lot of saved screens you will almost certainly need to consider using screen compression techniques. Simple run-length encoding is sometimes adequate, particularly on cartoon style graphics, and may allow you to have three or four times the number of screens on the disk. Screen loading is likely to be considerably faster too. There are a number of different screen compressors in PD libraries. It might be best to try out several to see which is most efficient for your needs. You may even be able to benefit by combining two different techniques.
3.5.9 A Last Twiddle
A neat way of making quite a significant speed increase is with a simple call to the memory controller as below. This will make up to a 20% speed improvement in Basic programs, although it seems to have little effect with an ARM 3 processor, which would already be several times faster anyway.
I strongly advise you to use the corresponding call to restore things to normal afterwards, as playing with the operating system like this can do strange things to applications or modules that don't expect it. For the same reason it may be as well not to use it in multi-tasking games. The restoring call is below.
Important The previous section has been retained for historical reasons but you should be aware that the "OS_UpdateMEMC" call should no longer be used as it will cause a complete crash on most machines using RISC OS 3 or later.
3.6 Alternative Strategies
Unfortunately there are times when your game idea simply can't fit into the limitations imposed by the hardware. By far the commonest problem is, again, speed. Apart from abandoning the game altogether the only action you can take is to look at different solutions to the same problem. For example, in the chapter on graphics there are two main methods of collision detection. If you read through you will see that each method has its strengths and weaknesses. You may find that your first choice was the wrong one, in which case a change to the other might solve your problems.
One way you can make improvements is to keep flags to identify changes in data that would require re-calculations to be performed. This is particularly relevant with three dimensional drawn objects. If only one object is moving but you re-calculate all points of all objects before plotting then you will waste a great deal of time performing unnecessary calculations. If you keep a set of flags for each object identifying any changes then only the objects that have moved or rotated need to be adjusted. This could mean that for the same running speed you may be able to double or triple the number of drawn objects in a scene.
Any change that simplifies calculations is beneficial, particularly if a complex expression can be replaced with an algorithm that uses simple addition and subtraction. A trivial example of this is shown in Figure 3.2. The obvious first choice for drawing a star is to calculate on the basis of two concentric circles, as in the first drawing. However, in this special case of a four pointed star it is much simpler to use the steps shown in drawings 1 to 4. Only one point has to be calculated, and even that is a simple piece of pythagoras. From then on, every new point can be found simply by reversing the sign of x, y or a.
There are many other shapes that can be synthesised without the need for time consuming trigonometrical calculations. Lateral thinking can be applied like this to many other areas to give you new, possibly better approaches. Hit-and-go situations are particularly amenable to alternative treatments.
3.6.3 Advance Calculations
Assume you have a 3D drawn missile that you want to realistically fire off at a target in a tank battle simulation. Your first thought might be to calculate all the points of the drawing in real time. This could turn out to be a major undertaking if you hope to get smooth movement. One possible solution would be to pre-calculate the action and drop the values into a large array ready for plotting.
Using care you can make your game continuously re-calculate while the player is adjusting gun attitude, tank speed, etc. The player knows that in real life you can't instantly swing a heavy tank turret round at right angles, so the time taken to re-calculate can be absorbed by interleaving it with an animated sequence involving the movement of the tank. When the player hits the fire button, the missile will smoothly follow it's trajectory. A bonus is that with the final point of the trajectory known in advance it may be possible to pre-calculate collisions.
If you really want to speed things up you can step through the missile firing routine, interleaved with the whole of the action and the partial calculation for the next missile. All of this is an excursion in investigating processor redundancy. This can often be put to good use.
3.7 Time Sharing
The greatest waste of time in any game is when the computer is waiting for player input. Often this is done with a simple G%=GET. The computer will wait forever, doing nothing until a key is pressed. This is a waste, as your program could be building up the next level on a shadow screen, running a simulator of some kind in real time, or possibly just animating a few beasties round the screen. One way of doing this is shown below.
IF mark%<count% PROCbuild
UNTIL G%=32 OR B%>0:REM spacebar or mouse button
REM print acceptance message
IF mark%<count% THEN
FOR I%=mark% TO count%
REM rest of program
REM build one object
REM move one beastie
You must ensure that routines used for building your next screen can work in very short time bursts or there may be an unacceptable delay in key response. Also note that I have included a loop after the input routine that finishes any building that wasn't completed in time. This could also be interleaved with other activities.
Some other aspects of time sharing and interleaving of animation with object movements are covered later in the book.