The graph paper (computergraphics) David Lubar.
The Graph Paper
Up to now, we've been covering graphics concepts in a bit of a vacuum, looking at specific areas as independent entities. This month, in what will be the last of the series, we'll be concerned with integrating these concepts into actual games. A lot of what follows falls into the theoretical, and I should stress that none of this is gospel. You should always be on the lookout for new approaches and better techniques.
The Illusion of Simultaneity
Those used to programming in Basic wonder how arcade games can cause so many objects to move at the same time. The answer is that this is an illusion. In the Apple, there are no simultaneous events. Everything happens in sequence. You put a byte on the screen, then another, and so on. Thanks to the speed of machine language, a long series of independent events can appear to happen at the same time. The trick is to organize these events in such a way that this illusion is maintained. If you are animating several objects, they should all be moved with the same section of code. One way to do this is to keep an ordered list of all necessary information for each item.
Let's say your program requires the following information about each object: old X coordinate, old Y coordinate, new X, new Y, X velocity, Y velocity, shape number, and some sort of status byte (more about that later). If these eight parameters are stored sequentially in RAM for each object, all animation can be handled in one loop. The X register can be used to index into the list. The easiest way to do this is to separate each set of entries by some power of 2. In this case, eight is sufficient. If there were nine or ten items in the list, the separation would be 16 bytes. Suppose, in our example, that the list starts at $8000. Old X coordinate of the first item would be stored at $8000, old X for the second item would be at $8008, the third would be at $8010, and so on. Now, if a variable is used to keep track of the number of items in the list, and if the separation is a power of 2, the variable can be shifted to provide an index into the list. An example can be found in Listing 1.
The number of items in each entry will be determined by the program. Some programs require only simple parameters. Others can get fairly complex. Let's look at some of these parameters.
The smallest unit an object can be moved is a single pixel. In simple programs, all locations can be treated as integers. The change in X and Y location (DX and DY) can be held in one byte each. One trick is to exploit the cyclic nuture of bytes and always use addition. To move right one pixel, a DX of $01 is used. When moving left, DX is $FF (adding $FF is the same as subtracting $01). This is simpler than having separate routines for adding or subtracting DX and DY. Also, it makes changes of direction easy to calculate. To make an object bounce, you just subtract DX or DY from $00. The result becomes the new DX or DY ($00 - $01 = $ff and $00 - $FF = $01). This works for any value of DX and DY.
While integers are fine for simple motion, they have some limitations. If the change becomes too great, the object jumps rather than moving smoothly. And corved paths are difficult to produce. The answer is to use fractional valuse. While an object can't be moved a half or fourth of a pixel, this fractional portion can be kept track of. Imagine an object has a DX of one and a DY of one half. Every time it is plotted, it will move one pixel to the right, Every second time, it will move up one pixel. If both DX and DY have a value of one half, the object will move on a diagonal, but at half speed. Such things as gravity and curving paths are easily done wsing fractional values. I believe this is called "fixed point arithmetic.' Whatever, let's take a close look at it.
Each parameter requires two bytes, one for the fraction and one for the integer portion. As before, all calculations can be done with addition. The trick is remembering that the fraction is a binary fraction. So one half uould be $80 (.10000000 in binary), one fourth would be $40, and so on. As with integer values, direction can be flipped by subtracting the values from $00. The only constraint is that the subtraction must be treated as a two-byte operation. First the carry is set and the fraction is subtracted from $00. The result is the new fractional portion. Next, without touching the carry, the integer portion is subtracted from $00 and the result saved as the new integer portion. For instance (calling the integer portion DX and the fractional portion DXF), suppose DX is $01 and DXF is $80. The object would move right at a rate of one and a half pixels per plot (actually, it would alternate between moving one pixel and moving two pixels, producing an average rate of one and a half). If you wanted to make the object bounce back to the left, you would subtract DXF from $00, getting $80, and DX from $00, getting (with the borrow from the first subtraction) $fe. The next DX of $FE and DXF of $80 would equal a movement to the left at a rate of one and a half pixels per plot.
The ability to work with fractional motion adds a great deal of control to programs. If you add a value to the fractional portion, you produce acceleration. One object can be made to chase another. If the target is to the right of the object, add something to DXF. If the target is to the left, subtract something from DXF. The amount added or subtracted determines how accurately and quickly one object tracks another. Listing 2 shows some common calculations done with fractional values.
Another handy parameter is a status byte. Not all objects will be treated the same at all times. Something might be disappearing, something else might be appearing, while a third object is undergoing a change such as an explosion. By using a byte as a flag for these conditions, one display loop can handle everything. Let's say you want to remove an object from the screen. The display loop would ordinarily calculate a new location, erase the old position, then draw the new position. But each portion first checks the status byte. If the byte contains a certain value, that portion is skipped. In this example, the status byte would indicate that the drawing portion could be skipped. Thus, the object would be erased but not replotted (the calculation of its new location could anso be skipped).
Another independent event that appears simultaneous is sound. Without it, games would be very dull.
The Apple has a rather limited sound capability, though you couldn't guess that from the great sound effects produced by some programs. Basically, all that can be produced is a click from the speaker. But by playing with the rate at which the speaker clicks, you can get a wide variety of sounds. The trick is to integrate the sound within a program. If sound is handled as a separate event, everything will stop. This is fine for music on a title page, but not desirable in the middle of a game. Thus, the code for producing sound must work in conjunction with the rest of the program.
For instance, let's say you have a ball bouncing on the screen, and want a sound whenever the ball hits a wall. The wrong way would be to go to a subroutine that produced the whole sound. The object is to integrate the sound. One way is to find some loop and insert the sound routine there. Most plotting routines occur in a loop. Let's say you add some code to the plotting routine. This code, after putting each byte on the screen, checks a flag. If the flag is set, it strobes the speaker. If not, it skips that code. When the ball bounces, you set the flag. The result will be some sound on every collision. The speaker is storbed at location $C030. Every access to that location causes a click. A load (such as LDA $C030) produces one click. A store actually accesses the location twice, thus producing two clicks. By experimenting with speaker strobes in various loops of code, you can produce many sounds, but this is a trial-and-error approach. Another method is to have a subroutine that is called one or more times during each pass through the program. This routine, using values contained in specific variables and flags, would access the speaker one or more times (depending on values in the variables), or it might skip right to the RTS (again depending on the variables and flags). This subroutine can actually contain a short loop or perhaps ten or twenty iterations without noticeably slowing down a program. By controlling frequency (number of times per second that the event occurs) and volume (number of clicks in each event) your game can come alive with sound.
The best way to learn is to experiment.
It's time to take a look at overall program structure, followed by some thoughts on game design.
One Big Loop
Aside from title page and other frills, a game can be thought of as an infinite loop of code. Once the initial conditions have been set, the code just keeps repeating. Some events are skipped, others may happen just once, but the overall structure is one big loop. The key is organizing the units within the loop. As mentioned, the goal is to take individual events and produce the illusion of simultaneity. The more efficient your code, the better the illusion. A rough flow chart can help in setting up the program. It needn't be an immaculate work of art. Just a rough sketch with boxes and arrows is a great help. This will show the logical sequence of events, and make the task of actual coding simpler. Many programs start by reading the controllers, then do the plotting, followed by handling any special actions required by the events on the screen. If you are reading a joystick or two paddles, place the reads at separate areas of the code (if you read two paddles without enough intervening delay, the first value will affect the second).
Events that happen frequently are usually placed in line. Rare events can be placed in subroutines. There are no concrete rules here. Each program has its own requirements. But you'll find that the common tie throughout all good programs is logical structure. Sneakers, Raster Blaster, and Serpentine contain vastly different code, but if you mapped the structure of these or other games, you'd find elegant, logical organization, breaking a complex whole into simple units. When writing a large program, tackle the work one unit at a time. This is where the flow chart really helps. Each group of blocks with one entry at the top and one exit at the bottom can be thought of as a unit. By programming one unit at a time, and debugging it, you'll end up with a tight, efficient program.
While whole books have been written on the subject of program structure, the best way to learn is to write programs.
Enough probably can and has been said about game design, so I won't dwell too long on the subject. But I can't resist a few words. Apple games have become very sophisticated. Certain requirements are almost mandatory in any new arcade game that hopes to make it to market. For starters, the graphics must be clean. There is also a trend these days toward cuteness. This may or may not last, but any unique or cute touch in your graphics is probably an advantage, though such things must make sense within the context of the program. All possible controller options should be allowed. If you are using a keyboard, the player should be able to define the keys. For a joystick, the player should be able to switch the X and Y axes (just in case his joystick and yours differ).
Games can't be static. There has to be some increase in difficulty or some new level to reach. In other words, a game must have depth. A game must contain learnable skills. The average player should be able to notice an improvement in his performance--a reward for repeated play.
The best way to assure that your game will be fun is to have others play test it. No programmer can be totally objective about his creation. And by the time you have finished the game, you will be so good at playing it that you won't be able to balance the difficulty fairly. It is not uncommon for a programmer to make his game harder and harder, not realizing that the average player will be blown away by the result. On the other hand, don't assume that no one will top your own performance. Add some levels or difficulty above the point you can reach. It is almost inevitable that someone will be better at the game than you.
I could ramble on, but enough has been said here, and one person can't hope to cover everything. Fortunately, there are more and more graphics articles appearing. The word is getting out. I hope I've filled in some of the gaps that existed, and shown that there is no real mystery to Apple graphics.
Before signing off, I want to thank those who, directly or indirectly, had a part in this. First, special thanks to Bob Bishop, both for taking the time to give me a push in the right direction, and for showing all of us, from the start, that there is magic in the Apple. Thanks to Mark Pelczarski for many things. And thanks to all those who shared ideas or answered questions. Such a list, though far from complete, must include Mark Turmell, Dan Thompson, Hunter Hancock, Ernie Brock, Bill Budge, and Frank Covitz. Finally, thanks to all of you who've shown interest in this series and taken the time to send questions and suggestions. It's been fun.
Table: Listing 1.
Table: Listing 2.