Player-missile graphics differ from playfield graphics in two distinct ways. First, the players, which appear as semi- transparent overlays against the background, are memory independent of the playfield graphics. Thus, these sprites, as playermissiles are sometimes called, don't overwrite playfield memory and don't destroy the background graphics as they move around the screen. Second, since they are directly mapped to the screen by the CTIA/GTIA and the ANTIC hardware chips, positioning a sprite on the screen is extremely fast and accurate. They were designed with fast smooth motion in mind.
As we learned from character graphics, the screen is a two-dimensional image; the screen RAM is organized one-dimensionally as one long string of bytes. Animating a character requires erasing the old character image, calculating a new position, then writing a new character image at the new position. Unfortunately, if the motion is vertical, the new position must be some multiple of twenty or forty bytes apart from the original in memory. The necessary calculations are time consuming. Moreover, character graphics animation with 8 x 8 pixel sized characters isn't smooth unless you use numerous transitional shapes. Also the background needs to be restored after each move.
Atari engineers thought of a simpler and better method to achieve smooth animation. They created a graphics image that was both one-dimensional on the screen and one-dimensional in memory. This image appears as a vertical stripe one byte or eight pixels wide extending from the very top of the screen to the very bottom. The image or player appears in RAM as a table of bytes that is either 128 or 256 bytes long. Each byte, depending on the programmer's choice of resolution, is mapped into either one or two horizontal scan lines.
The two hardware chips, the CTIA/GTIA and ANTIC, automatically place a sprite on the screen. While the GTIA is busy putting color pixels on the screen based on graphics information furnished by ANTIC, it simultaneously keeps track of its current horizontal position on the screen. If it finds that the current horizontal position equals the value of the horizontal position register for the player, it asks ANTIC to give it a byte from the player-missile area of memory corresponding to the current scan line. It then interprets that byte as a series of on-off pixels or points, starting with the high bit on the left, and plots them in the selected player color. If the bit in the byte is on, it illuminates or plots a pixel, and if it is off it skips plotting the pixel. Areas of the player stripe that contains zeros or no player data are transparent to the background or playfield graphics. Scan lines that contain data form a solid image that overlays but does not affect the background image.
Player data is mapped in much the same way as character data is mapped in a character set. Where a character is limited to eight rows of data, a player stripe, depending on player resolution, consists of either 128 or 256 rows. Each byte corresponds to a row, and each of its eight-bit positions corresponds to the eight pixels that collectively form part of the player image. A bit that is on or has a one in it lights the corresponding pixel which maps from left to right, high bit to low bit. For example
This data table, which is stored in the player-missile area of memory, is 256 bytes long. Actually, only 192 bytes corresponding to the screen's 192 scan lines are effectively used. A 256-byte area was chosen since it is one page of computer memory, and it is easier to find the start of a player's data when it begins on a page boundary.
The vertical starting position depends on the position in player memory. However, the 0th or first byte is mapped automatically by hardware to an area offscreen past where ANTIC is generating display list scan lines. Therefore, the first twenty or so bytes and the last thirty or so bytes are beyond the normal raster scan of a television set. A player that begins in the thirty-fourth position in player-missile memory will map to the screen starting at the second scan line. Players move vertically by moving the player data through the 256-byte page of player memory. The higher a player image is stored in memory, the lower it appears on the screen.
Atari's player-missile system consists of four players and four missiles that reside in a 2K block of memory known as the player-missile memory area. Think of
missiles as narrow players two pixels wide instead of eight pixels wide. Each of the four players has its own playermissile area of memory. Single-resolution players use 256 bytes or one page of memory, while double-resolution players use 128 bytes or half a page of memory. They are arranged sequentially so that player one follows player zero, etc. The four missiles, on the other hand, are stored in the same block of memory just below player zero. They are arranged in two-bit pairs with the 0th missile occupying the rightmost or lowest twobits, and the third missile the leftmost or highest two bits. While this arrangement is handy if you want to combine all four missiles to enable a fifth player, it presents a problem when moving a single missile vertically on the screen. If the missile data is moved in memory to correspond with a missile's screen movement, then portions of data for the other missiles that reside in those same bytes will also be moved. There is a Machine language solution to the problem that requires masking the bytes during movement, but this technique is unavailable from BASIC except by a complex USR function. Fortunately, horizontal movement is much easier. The designers incorporated a separate horizontal position register for each player and each missile. Even if all missiles are combined to enable a fifth player, each two-bit-wide missile band must be still set individually. Essentially, the fifth player is kept as a unit when it is moved, if each missile is positioned two units apart in the horizontal axis when it is moved.
The color and size of each player and its associated missile can also be specified. Size only affects the horizontal width of the player or missile. Widths can be normal, double, or quadruple size. For example, in double width, the GTIA chip begins double plotting the bytes on bits when it reaches the horizontal position register of the player or missile. Color is assigned to four shadowed player-missile color registers. Since these four additional color registers are independent of the playfield color registers, more colors can appear on the screen at any one time. While each player can have a different color, each missile is assigned the color of its corresponding player. Thus, if player three is green, missile three will also be green. The only exception to the rule is that if a fifth player is enabled, the four combined missiles use the color in playfield three's color register. There is rarely a conflict since playfield three is only used in four graphics modes. Since the GTIA plots both the player and playfield color pixels simultaneously, it can detect any overlap between graphics images on the screen.
Player priority can be set so that all or half of the players are in front of the playfield graphics, all are behind the playfield graphics, or some of the playfield colors are in front of the players with the rest behind. Any overlaps in position are of course returned in a series of read-only hardware locations called collision registers.
These are quite useful in game design. You can also enable bit 5 in the priority register (POKE 623,32) so that you obtain a third color when players 0 and 1 or 2 and 3 overlap. If you don't set the overlap option, the area of overlap will be black.
NOTE: Above values will set all missiles the same size.
NOTE: If missile sizes are set Individually, then add the four different values. The combination is poked into ($D00C).
Enabling Player-Missile GraphicsThere are numerous steps involved to enable player-missile graphics from both BASIC and Machine language. Beginners see it as a long list of mysterious POKEs, but there is a logical and explainable reason for each. First there are two electrical switches between Antic, GTIA, memory and the world, which tell them whether to do DMA (Direct Memory Access) automatically. The first, called DMACTL (Direct Memory Access Control), is shadowed at location 559 decimal ($22F). It enables ANTIC to fetch bytes automatically and plot them to the screen. When it is turned off with a zero, the screen is turned off because ANTIC can no longer fetch bytes from memory. It defaults normally to standard playfield graphics with double-line resolution for player-missiles turned off. This value is 34 decimal. The table below, which depends on which bit positions are set in DMACTL, summarizes the various modes.
The second switch GRACTL (Graphics Control) physically enables playermissile graphics. If we POKE a three into decimal location 54279 ($D01D), both players and missiles are turned on. In addition, we will need to set color, width, and horizontal positions for each player.
Next we need to reserve space for our 2K player-missile area in memory assuming we are using single-resolution P/M graphics. Since it needs to be on a 2K memory boundary, it is best to reserve space and put it above the screen and display list areas beginning at eight pages (2K) below the top of memory. This is accomplished by simply adjusting the top of memory pointer at location 106 decimal with a new value that is eight less than the original value. The computer now thinks top of memory is 2K lower than it actually is, and assigns screen memory and its accompanying display list below that when any graphics mode is set up.
The 2K player-missile area (single-line resolution) begins at the new top of memory which we just POKEd into location 106. Since ANTIC needs to know where we have put our P/M storage area, we need to POKE this value into location 54279 ($13407) known as PMBASE. This is the high byte value of the location. The low byte value is assumed to be zero since it is on a 2K page boundary.
The memory area assigned to player-missile graphics may or may not contain miscellaneous data which will appear as garbage in the P/M stripes. The 2K area should be cleared to zeros. However, since a long series of POKEs in BASIC is rather slow, it is faster to clear only the P/M memory that you will actually use. In the example below, only player #0 is used. Therefore, only the area from PMBASE+ 1024 to PMBASE+ 1280 is set to zero. In the worst case where you were using the missiles and all the players, the unused first 768 bytes in singleline resolution or the first 384 bytes in double-line resolution need not be cleared.
Space Ship ExamplePlayer #0 is a spaceship originally five scan lines high. To make it appear larger each scan line is plotted twice and the width is doubled. In addition, a pair of zeros has been placed at both the top and bottom of the player image for a total of fourteen bytes. The reason for this will shortly be clear. The fourteen player-missile data bytes are then POKEd into the proper P/M memory area for player #0. By POKEing the data starting at SPOT = PMBASE+1024+100 the player will begin 100 scan lines from the top. Since the first thirty-two scan lines are above the top line of text on the screen, our image begins sixty-eight lines from the top. Accounting for the two blank or zero lines of our image, our ship actually begins at the seventieth scan line.
For a player to move vertically, all of its data needs to be shifted in memory. Players can move downward on the screen by shifting the data upward in memory. Conversely a player moves upward on the screen by moving its data downward in memory. The laver case is definitely the easier direction because data can be moved from top to bottom without the danger of overwriting any of the bytes during the move. All fourteen bytes of data are shifted upward two scan lines from SPOT+I to SPOT+I-2. The two extra zero bytes on the end serve to erase the bottom two bytes of the ship's shape in its previous position. If this isn't done, the bottom sliver of the ship will remain as screen garbage after the move.
Moving the ship down the screen or upward in memory is slightly harder. If we start at the top of the shape as before, the first byte (0) would replace the third byte (153), and the second byte (0) would replace the fourth byte (153). Fine, but when we try to move the third byte to the fifth position, the original data (153) has been overwritten by the first move. Eventually all of the bytes will contain zeros and the shape will disappear. The solution to this dilemma is to start at the bottom of the shape and move each of the bytes upward in memory two places. Since you don't overwrite any shape information during the move, the shape remains intact. Again, the two extra zeros at the top serve to erase the tiny two-line sliver that would have remained after the move. It might seem strange that we deliberately added two zero bytes at the top and two at the bottom of our player when the rest of the storage area is obviously full of zeros. However, these could only be used if more bytes than that of our actual shape were moved, and only if we adjusted where the top or bottom of our shape begins. If we need to go to this trouble, it is easier to add extra leading and trailing zeros.
A Player-Missile Machine Language Move SubroutineIt becomes apparent from this simple example that moving a player vertically using POKEs in BASIC is very slow. However, moving a player horizontally is fast because a horizontal position register was built into the hardware.
One clever approach to moving players fast vertically is to take advantage of BASIC's fast string-handling capabilities. These routines are just high speed Assembly language copy routines. The trick to using these routines is to fool the Atari into assigning the player-missile string data to the memory area assigned to player-missile graphics. Then player-missile graphics data can be moved simply with P$ = S$ type statements. While this technique allows the programmer to avoid Machine language subroutines, it is not the easiest method to learn or understand. Since the example and explanation would be intimidating to beginners, we will delay it until much later in this chapter.
Player-missile graphics can be speeded up tremendously if Machine language subroutines are used. There have been numerous vertical blank player-missile subroutines published in the magazines. While most are effective in rapidly moving players vertically by one of several techniques, nearly all have neglected missile movement. This is unfortunate since moving one missile without disturbing the others requires using AND and ORA machine instructions on a bit level to mask off the other missiles during vertical movement. This is something that is beyond the capabilities of BASIC.
We have developed an easy-to-use Machine language subroutine that can handle both players and missiles simultaneously. The subroutine resides in page six of memory but is relocatable to virtually any free area of memory.
There has been much controversy on whether page six of memory is actually a safe area for placing user subroutines for BASIC programs. While it is true that inputting more than 128 characters into the text buffer will overwrite the beginning of page six, any program-especially a game that remains in control throughout-has no problem. In any case, the second half of page six $680-$6FF is always safe.
Our subroutine was designed for single-line resolution player-missile graphics. It requires the input of five variables: player number, player length, the old Y (vertical) position, the new Y position, and the new X (horizontal) position. The format is:
A = USR (1536, PLAYER #, PLAYER LENGTH, OLD Y, NEW Y, NEW X)
The number 1536 ($600) is the starting location of our subroutine. The players are numbered 0-3 and the missiles 4-7. Player #4 in our subroutine is actually missile #0, etc. The player length can only be as large as sixtyfour lines. This limitation will become clear when we explain how the subroutine works. The values for both the X and Y positions can be anything from 0-255. However, you should be aware that only X values in the range of 48-207 and Y values in the range 32-223 are within the bounds of the 40 by 24 text screen or playfield graphics area. The range is actually slightly larger, but visibility depends on the overscan of your television set. While the OLDY value is the programmer's choice upon initialization, it must be set equal to the NEWY position after each USR call. If OLDY isn't set to NEWY immediately after the subroutine call, unpredictable graphics will begin to appear in the player stripe when the subroutine is used again. The reason is that the subroutine zeros out the shape at its previous position OLDY before drawing the shape again at its new position NEWY. If OLDY isn't set to the player's last vertical position, the subroutine will miss zeroing the shape at the old position. Most likely, you will get two shapes or, worse yet, a sliver left at the previous position.
A Player-Missile ExampleThe next example is a simple demonstration of the speed, versatility and ease of use of our player-missile subroutine. This two-part example is instructive in several regards. First, it shows how a simple spaceship capable of firing missiles can be joystick controlled. Even its simple inline code, which allows either the ship to move or the missile to move, but not both simultaneously, gives some insight into the difficulty in designing even the simplest games. Second, the concept of priority, or which player or playfield is to be drawn in front of another is demonstrated. But first I think a discussion of how objects move might be helpful, especially to beginners.
Dynamics of Objects in MotionAny object in motion, whether it is simulated on a video screen or moves in the real world, is subject to the laws of physics. Readers will immediately cringe at the thought of advanced mathematics, mainly calculus. But calculus is merely a method of calculation that involves the summation of many small bits and pieces of a body's velocity and acceleration to determine the actual distance an object travels. Fortunately, the computer automatically divides our time frame into analogously small units, or animation frames.
Let's examine an object in simple linear motion. The object is initially at rest. It is then given a horizontal velocity of one unit to the right. Thus the velocity is +1 unit/time frame. During each animation frame, the object moves +1 units to the right.
An object's direction of travel and its magnitude is represented by a line segment called a vector. An object's velocity always points in the direction of travel. Our object shown below has a velocity of +1 units/time frame, so that the velocity is pointing to the right. Since the velocity vector is to the right, the object moves to the right +1 unit/frame.
Similarly, an object that moves diagonally downward and to the right has a positive velocity vector in both the X and Y directions. This velocity vector is a combination of the velocity components in both the X and Y directions. The object will continue moving in the direction of the velocity vector until either VX or VY changes. For example, if VX becomes zero, the object will begin to move straight down in the direction of the new velocity vector.
This can be formalized into equations for each of the two screen directions X and Y.
VX = +1 Velocity is constant in X direction.
X = X + VX New position is the old position plus the change in position (velocity).
VY = +1 Velocity is constant in Y direction.
Y = Y + VY New position is the old position plus the change in position (velocity).
While this simplistic method of moving objects by instantaneous changes in direction and velocity works on the video screen, objects in the real world don't behave this way. Take the family car for example. You step on the gas pedal, and the car's velocity begins to climb steadily. The distance traveled each second begins to increase as the velocity increases. When the car is only going 15 MPH the car travels only 22 feet each second, but when the car reaches 60 MPH it travels 88 feet each second.
The driving force that speeds up our car is called acceleration ( V = V + A ). Acceleration can be constant as in a rocket's thrust, or like gravity which pulls a falling object to Earth. For a screen object's motion to appear realistic, especially in the case of a falling bomb, acceleration must be taken into account. When a bomb drops, its vertical velocity increases with time. If there were no wind resistance, the bomb's vertical velocity would increase until its impact on the target.
If a constant force was suddenly applied to a stationary object such that it accelerated downward with an increase in velocity of one unit/ frame, the distances moved would grow substantially.
TIME VELOCITY POSITION (distance) 0 0 0 1 1 1 2 2 3 VX = VX + 1 3 3 6 X = X + VX 4 4 10 5 5 15 6 6 21
The plot of the trajectory of a falling bomb is shown below. The trajectory, neglecting wind resistance, forms a curve that is called "parabolic." There are two components to the velocity vector. VX in the X direction is a constant set equal to the plane's forward velocity. VY in the Y direction grows larger with time as gravity accelerates it in the downward direction. This same effect can be observed by dropping a ball from the second or third story of a building. At first, the ball falls slowly, but then it begins falling faster. Observers at ground level will note the acceleration of the moving ball just before it bounces. The summation of the two velocity vectors determines the resultant direction of an object's motion for each animation frame. Since the VY vector grows larger with each frame, the total velocity vector begins to point downward. Eventually, the bomb will be falling almost straight down. Thus:
VY=VY + GRAVITY Y = Y + VY
In the above cases, the acceleration was non-existent or constant. However, as we will see at the end of this chapter, even simple games involving a steerable spaceship that can be thrust in the direction that it is headed, must use variable acceleration. The rate or value may remain constant but the direction changes. While only the beginning of the discussion above is necessary to follow the second example, I hope it will give you some insight into how an object's motion is simulated on the Atari's video screen.
Program InitializationOur first example, a joystick-controlled single ship capable of firing missiles in eight directions, takes advantage of our player-missile Machine language subroutine. As in the first example in this chapter, a 2K block of memory needs to be reserved for player-missile graphics. The top of memory pointers are moved down eight pages, and the player-missile base PMBASE is set equal to the top of memory. BASIC then sets up the screen area and its display list just below. Graphics I was chosen because it uses little screen memory and because we will need to write some large characters to the screen for the second part of our example.
Next, our two Machine language subroutines need to be POKEd or placed into memory. The first subroutine is the player-missile subroutine. It is 151 bytes long. Since it begins at the start of page six ($600) or 1536 decimal, a simple FOR ... NEXT loop that reads the data statements and POKEs memory, will do the job. The second Machine language subroutine is a clear memory routine. It will automatically clear player-missile memory to zero in a fraction of a second. It replaces a slow thirty second long FOR ... NEXT loop which would POKE a zero into each sequential memory location from PMBASE+0 to PMBASE+2047. It also resides in page six of memory, beginning at $6A0 or 1696 decimal. It is twenty-six bytes long.
We must activate player-missile graphics next. We will set DMACTL for singleline resolution and GRACTL for both players and missiles. Player #0 is set to double width by writing a zero to location 53256, and player #2, which will be used in the second part of this example, is set to double width by writing a one to location 53258. The missiles are set to regular width, and the color of each of the players is selected. The priority GPRIOR shadowed at location 623 decimal is set so that player #0 is in front of the playfield graphics and player #2 is behind. We will talk more about this location during the discussion of the second half of this example. Last, we tell ANTIC our player-missile address by writing it to PMBASE at decimal location 54279. Since our subroutine also needs the location of PMBASE, its high byte is. POKEd into location 1686 which is the last memory location of our subroutine. We could have passed it to the subroutine via the USR function, but then you would have had to type the value in each time you used the subroutine.
Player Shapes Using the P/M SubroutineWe then store the two player shapes in our player shape storage area. You will recognize the first shape from the first example. It is our spaceship. The second shape is a solid block ten bytes high. Normally, you would want to POKE your shapes into the proper place in the player-missile memory area. However, when we designed the subroutine, we felt that the best and fastest approach would be to erase the shape at its old location before redrawing it at the new location. The traditional approach was to do a memory move of the entire shape including leading and trailing zero bytes, to insure pieces aren't left behind. If a shape were moved vertically more than one or two scan lines, a series of small memory moves had to be done sequentially.
In order to use our faster subroutine, we had to find a safe place to store our shapes in memory, no matter where you put our subroutine, the player-missile area, and screen memory. Fortunately, the first 768 bytes of the playermissile memory area are unused. We decided to use the first page or 256 bytes for the player storage area. This limits the size of each of the four players to sixty-four bytes. Since sixty-four bytes or scan lines make up nearly one-third of the screen, this limitation really doesn't cause any problems.
Before any shapes are stored in this "safe" area, player-missile memory should be cleared or zeroed with the Machine language subroutine stored in page six of memory at 1536 decimal. The only parameter passed via the USR function is the starting memory address PMBASE. This routine was specifically designed to clear eight pages of memory, precisely that of a 2K block of player-missile memory. If you neglect to clear it, random patterns will most likely surround your shapes in the player missile stripes. The first shape, our spaceship, which is player #0, is POKEd into the first ten bytes of the player-missile storage area from PMBASE+0 to PMBASE+9. The second shape, the solid block, which is player #2, is stored beginning at PMBASE+128.
The spaceship (player #0) must have an initial starting position and starting velocity. While it is obvious that the present position X0=100, Y0=80 is important, its previous Y axis position must have an initial value despite the fact that there couldn't have been a previous one. Setting Y0OLD=80, the value of our present Y position, will suffice. Likewise, the same is true for player #0's missile. YM0OLD=10, a value that insures that it is off screen.
Joystick Controlled Ship MovementThe spaceship's velocity is joystick-controlled. Pushing the stick in any direction instantaneously changes the ship's velocity. If the stick is pushed to the right, the ship is given a horizontal velocity of 2 units/frame. Since there is no vertical component to the ship's velocity, the ship will move to the right 2 units/frame. It will continue in that direction until either it collides with the screen's boundary, or a new joystick input gives the ship a new velocity vector. The equations that control the ship's X and Y position are as follows.
X0 = X0 + VX0
Y0 = Y0 + VY0
There is a separate pair of velocity vectors for each of the eight possible joystick directions. The absolute values on each of the axis are not the same because pixel size is rectangular rather than square. Objects with equal velocities along both axis tend to move faster along the horizontal axis than along the vertical axis. To compensate for this, we set VX = 2 and VY = 3. Diagonal velocities, which are the vector sum of the two velocity components, are faster than anticipated. While it appears that you could correct this by using fractional values for both velocity vectors on the diagonals, you can't move part of a pixel position in either axis. However, if the velocity vectors were much larger you could correct the diagonal velocity by using smaller whole numbers. The diagram below shows the velocity component values for each of the joystick directions.
The only way to set the ship's two velocity vectors for each joystick direction is to test the value of Z0 = STICK(0) against each of its possible values. When the stick is centered (Z0=15) the velocity remains the same and we skip the remainder of the tests. The other eight possiblities are tested in a series of IF statements. If a match is found for any direction, new velocity vectors are substituted. While it appears to be a cumbersome method, there is no better method.
I'm sure many readers wonder why Atari BASIC returns such a strange and illogical set of values for joystick directions. STICK(0) through STICK(3) reflect values returned in the PIA chips locations 54016 and 54017 ($D300,$D301). When the joysticks are centered, each of the four bit locations for each joystick are on (set to one). When the joystick is pushed in any direction, its appropriate bit position is turned off (set to zero). For example, if the joystick is pushed to the right, the fourth bit from the right is turned off. The remaining three on bits add up to seven. Diagonal joystick movements have some combination of two bits turned off. The diagram below shows all of the possible bit patterns.
After the joystick is read, the player's position is updated and checked so that it does not exceed the screen boundaries in either direction. A USR call to our playermissile Machine language subroutine will move the player shape into the proper place in memory so that it appears at the choosen X,Y screen coordinates. The format as discussed earlier is A = USR (1536, PLAYER #, PLAYER LENGTH, OLD Y, NEW Y, NEWX). The player length is 10, OLDY equals Y0OLD in this example, NEWY equals Y0, and NEWX equals X0. Immediately after the USR call, update the OLDY position with the NEWY position so that you don't have to worry about losing this value when you calculate your new position during the next frame.
Missile MovementPressing the joystick button fires a missile. STRIG (0) is normally set to a one value when the button isn't pressed. But when the button is pressed it becomes zero and the program branches to a second joystick read routine at line 185. It is nearly identical to the first, with the exception that the velocity vectors are nearly double in value; VXM0=5 and VYM0=6. This ratio produces nearly identical apparent speeds in both the horizontal and vertical axis. Since the missile needs to be given a direction to fire, it branches past the missile routine if the joystick is centered.
The missile's initial starting position is at the center of our ship. Since X0, Y0 are the coordinates of the ship's missile at the center of the ship, the missile's initial position equals the current ship's position plus the correction. Thus, XM0=X0+3 and YM0=Y0+4. The missile's position is updated each frame the same way the
ship's position is updated each frame. The new position equals the old position plus the missile's velocity vector.
XM0 = XM0 + VXM0
YM0 = YM0 + VYM0
Unlike the ship, however, the missile's velocity vector can't be changed once the missile is fired. Missile movement is in a closed loop that exits only when the missile reaches the boundaries of the screen. This closed loop not only prevents you from firing a second missile before the first has reached the end of its travel, it also prevents the ship from moving while the missile is in motion. This is a good example of how simplistic code that branches to either one event or the other creates an unrealistic effect. The solution, which will be explained in the next example, requires the ship's movement code to be placed in line with the missile's movement code, and conversely the missile's movement code to be placed in line with the player's movement code. This apparent overkill, in fact, allows the ship to be steered once the missile is launched.
The missile is plotted via the USR call to our player-missile subroutine. Missile #0, which is two pixels wide, is player #4 and its height is set to two scan lines to make it square shaped. Its OLDY position is YM0OLD, and its new position NEWX, NEWY is XM0, YM0. Again, once the subroutine is called via the USR call, it is important to immediately set the missile's old position equal to its present position.
Finally, if the missile does reach the screen boundaries, it is necessary to place it out of view beyond the scan of the television set. Since the missiles can't be simply shut off, you should set the horizontal register to either a small number or a large number. Since XM0=10 is offscreen, it will suffice.
Priority DemonstrationThe second part of the example is a demonstration of player versus playfield priority. Whenever two independent objects such as two players or a player and a playfield object appear at the same spot on the screen, the GTIA must decide who gets displayed first and who second. This is known as priority. There is a register called GPRIOR shadowed at location 623 decimal ($26F) that selects which screen object is in front of the others. There are actually only four possible selections. Either all of the players are in front of the playfield, all of the playflelds are in front of the players, players #0 and #1 are in front of the playfields with players #2 and #3 behind, or playfields #0 and #1 are in front of all the players with playfields #2 and #3 behind.
Lower number players take precedence over higher number players and, likewise, lower number playfields take precedence over higher number playfields. Actually, there is no way for two playfields to occupy the same space, since each color pixel is assigned to a specific color register. In fact, only the on bits, or those portions that show in the player-missile stripe, actually are involved in the priority conflict. The off bits are ignored by the system. Obviously, if a player has the shape of a donut, the background or another higher numbered player set behind it will show through the hole.
Unfortunately, player #0 always has priority over the other three players. There is no method to change the priority between the individual players other than to switch the player data between player-associated areas of memory.
The great advantage of priority control is that you can control what happens to players when they meet the background color display. Perhaps you have an aerial combat game. You may want the plane to fly behind clouds, yet always remain in front of the scrolling ground far below. Setting bit three in GPRIOR will give two of the playfields higher priority than the players, and the remaining two playfields the lowest priority.
In our example, we have the word ATARI written in playfield #0, a spaceship (player #0), and a moving block (player #2). Initially, bit I is set in GPRIOR (POKE 623,2) so that player #0 moves in front of the letters while player #2 moves behind the letters. This part of the example can be reached by pressing the START key. The IF statement in line 175 tests for this possibility. The spaceship is joystick-controlled exactly like the first part; however, the missiles have been disabled.
Pressing the SELECT key sets the color of the letters to the same color as the background. However, since the pixels in the letters still refer to playfield #0, and player #2 had a lower priority than all of the playfields, that player is masked by the invisible playfield object as it moves behind it. Our much larger player shows through the spaces around the individual letters, thus illuminating the darkened object.
Pressing the OPTION key does two different things. First, it restores the playfield #O's color to its default color and luminance. Second, it changes the GPRIOR to I so that all players have priority over all playfields. Now both our block and our spaceship are in front of the letters. The ship (player #0) still has priority over the moving block (player #2).
Explanation of Player-Missile SubroutineThe player-missile subroutine was designed to handle both players and missiles simultaneously. In order to do this while retaining compact and relocatable code, several compromises were made. First, only single-line player resolution is offered. We felt that the coarser, double-line resolution could be simulated by setting the player-missile stripe at double width, then double plotting each of the shape's bytes. An attempt to give you a choice of resolution modes would have created a long and messy algorithm since the memory requirements of each are quite different mathematically. In single-line resolution the actual memory storage areas for each player are exactly one page or 256 bytes apart. This makes setting the pointers to the memory area easy since only the high byte is involved. But in double-line resolution the memory areas are 128 bytes apart. A much more complicated multi-byte add is required to set the pointers.
Second, we decided to erase the old shape completely before drawing the player shape in its new position. It is a faster technique since the more traditional method needs to do a series of block memory moves to shift the shape more than one line at a time. Our method requires storing the shapes in a permanent safe spot. We choose the first 256 bytes of the player-missile area. Since there are four player shapes, each shape is limited to 64 bytes or scan lines.
Advanced Assembly language programmers will notice that we did not write the subroutine in VBLANK. We felt that BASIC was slow enough without adding small delays while waiting for the VBLANK interrupt to begin our subroutine. Moving several players and their corresponding missiles on the screen at the same time could slow the game down. Motion smoothness is another reason for using VBLANK for player-missile graphics. However, as you will see in the following game examples, the animation frame rate is not fast enough to produce smooth animation with or without VBLANK.
The subroutine is divided into three parts: a routine to interpret and store the passed parameters in the USR function; a routine for erasing and drawing the player shape; and a routine for erasing and drawing a missile. Once the parameters are stored, a test on the player number determines if it is actually a missile, and if so, branches to that part of the code.
Interpreting USR Parameters
A USR function in BASIC pushes all of its parameters onto the stack before it enters the Machine language subroutine. The stack is like a dish dispenser in that the last value placed on the stack must be pulled off first. The first byte contains the number of passed parameters, a useless value, and one to be discarded. The other parameters are stored in two-byte pairs, high-byte first, in the order that you passed them, The high-bytes in our example are useless since none of our values exceeds 255. The five pairs are pulled off the stack in an indexed loop using the X register. They are pulled two bytes at a time. Only the second, or low byte is stored sequentially at PLAYNUM,X ($691, X). Thus location $691 contains the player number, $692 the player length, $693 the value for OLDY, etc.
Erasing and Storing Player ShapesThe pointers to move our player or missile shape from their storage area to the proper player-missile memory area are set up next, The shape is stored at SHAPEL, SHAPEH. The high byte BASE is just the beginning of the P/M storage area which was POKEd into location 1686 decimal ($696) from BASIC. The low byte is obtained from a table called INDEX. Each of the values in this four-byte table are sixty-four bytes ($40) apart. The actual memory location that the shape is to be stored at in the P/M area is SHPML, SHPMH. Since each player is 256 bytes or a page apart in memory, we added #$04 to BASE in order to set it for player #0. We then did a cute little trick. We put it in a loop and incremented SHPMH for each player number.
Erasing the old player shape from memory could be handled quite easily with simple indexing in the form STA ADDRESS,Y if only one single player were involved. ADDRESS would be the absolute address of that player's P/M memory, and the Y register would contain the value YOLD. Unfortunately, the high byte of each player's P/M memory area is different. Rather than try to update ADDRESS each-time, it is more efficient if the two-byte page address is stored in zero page. Then indirect index addressing in the form STA (SHPML),Y can be used either to erase or plot player-missile data.
If the computer finds a $00 in location $CE (SHPML), and a $9C in location $CF (SHPMH), then the base address is $9C00. The Y register contains the value of the OLDY position. If the shape was thirty-two scan lines down the screen, then the Y register = $20. If the computer wished to erase the shape, then it would store a #300 in the Accumulator into memory location $9C00 + $20, or $9C20 as shown:
The actual code to erase the player shape is reiterated in a loop PLAYLEN times. Since it is easier to increment the Y register from zero until it is equal to PLAYLEN, the vertical screen offset is stored in SHPML in zero page. The address SHPML, SHPMH is now the address of the beginning of the player-missile shape. The Y register offsets into the shape. The code is shown below.
ERASE LDA OLDY ;Y VALUE STA SHPML LDA #$00 ;ERASE WITH 0 TAY ;Y REGISTER = 0 .1 STA(SHPML),Y ;STORE IN PROPER P/M AREA CPY PLAYLEN ;DONE? BLT .1
The code for drawing our shape is quite similar, except that instead of storing zeros in P/M memory, we transfer the player shape from its storage area to its proper place in P/M memory. The pointers to its storage area, SHAPEL, SHAPEH were previously set up in zero page. We need update only SHPML, the low byte pointer to the place we are actually moving our player, with the NEWY position. The high byte SHPMH remains the same. The code follows:
LDA NEWY ;NEW Y VALUE STA SHPML ;SETUP POINTER TO PLOT LDY #$00 DRAW LDA (SHAPEL),Y;LOAD BYTE FROM PLAYER SHAPE TABLE STA (SHPML),Y ;STORE IN PROPER P/M PLAYER AREA INY ;NEXT BYTE CPY PLAYLEN ;DONE? BLT DRAW
Handling MissilesPlayer numbers 4-7 refer to missiles 0-3 respectively in our player-missile subroutine. If we didn't use different numbers, the user would have to type in a letter M or P to differentiate between the two different kinds of sprites.
While missiles are just narrow players, all four reside in the same 256-byte block or page of memory. The missiles, each of which is two bits wide, are arranged parallel to each other. Essentially, all four two-bit pairs for each scan line are in the same byte of data. This makes moving one missile but not others somewhat difficult.
Missile #0 uses the first two or lowest two bits of the byte, missile #1 bits three and four, missile #2 the fifth and sixth, and missile #3 the two highest bits. While the data in one byte determines which pixels are lit for all four missiles on any scan line, each missile has an independent horizontal position register. Thus, each of the missiles can have movement completely independent of the others.
As we mentioned, moving one missile vertically without changing the other missiles' data. can be a problem. If we had just two missiles initially on the same scan line, and we attempted to move missile #0 downward one scan line, either a memory move or an erase before redrawing would affect the missile #1 as well. In the first case, missile #1 would move in tandem with missile #0, while in the second case missile #1 would be erased. The solution is to mask off the other missile's bits during the erase, and to draw the missile's new position using another masking operation that will not affect the remaining bits.
ORA InstructionThis drawing technique uses the OR memory with Accumulator (ORA) instruction. It works on the bit level. If the bits in either memory or the Accumulator are on, then the result is one. If neither is on, the result is zero.
ACCUMULATOR MEMORY RESULT BIT IN BIT BIT ACCUMULATOR 0 0 0 ORA 1 0 1 1 0 1 1 1 1
If missile #0 was already on a particular scan line and you wanted missile #1 to be on the same scan line you would ORA missile #1 with the byte in P/M memory.
0 0 0 0 0 0 1 1 MEMORY ORA 0 0 0 0 1 1 0 0 MISSILE SHAPE 0 0 0 0 1 1 1 1 RESULT
AND InstructionAnother selective drawing technique is the And Memory with Accumulator (AND) instruction. It too works on a bit level and is used to filter or mask out certain bits in the Accumulator. Both the memory bit and the Accumulator bits must be set (on) for the result to be one. If either memory bit is off, or both bits are off, the result is zero. We put ones where we don't want to change bit values, and zeroes where we do. We can erase one missile at a time without affecting the others.
ACCUMULATOR MEMORY RESULT BIT IN BIT BIT ACCUMULATOR 0 0 0 AND 0 1 0 1 0 0 1 1 1
If you wish to erase only one of the two missiles on the same scan line, you AND the data byte with a mask that always produces a zero bit result in the missile bits to be erased, and a one in all the other bits. For example, to erase missile #0 and not missile #2, the mask with which you AND the data bit is $FC.
0 0 1 1 0 0 1 1 MEMORY AND 1 1 1 1 1 1 0 0 MISSILE SHAPE 0 0 1 1 0 0 0 0 RESULT
There are four different missile masks:
Missile mask #0 1 1 1 1 1 1 0 0 $FC
Missile mask #1 1 1 1 1 0 0 1 1 $F3
Missile mask #2 1 1 0 0 1 1 1 1 $CF
Missile mask #3 0 0 1 1 1 1 1 1 $3F
Likewise, there are four different missile shapes. For simplicity and visibility we made each missile two pixels wide. The height is controlled by the length, which is nominally two bytes, However, missiles can assume tall, thin dimensions if the user chooses a length of four or more bytes.
Missile shape #0 0 0 0 0 0 0 1 1 $03
Missile shape #1 0 0 0 0 1 1 0 0 $0C
Missile shape #2 0 0 1 1 0 0 0 0 $30
Missile shape #3 1 1 0 0 0 0 0 0 $C0
The data for the above two tables appears in our player-missile subroutine as MASKS and SHOTS.
The heart of the missile routine is the erase and draw code. Each is a mere three lines long. The selected missile is erased by ANDing memory with the proper mask.
LDA (SHPML),Y ;LOAD OLD MISSILE DATA AND MASKS,X ;ERASE IT BUT DON'T DISTURB OTHER MISSILES STA (SHPML),Y ;STORE RESULT BACK IN MEMORY
The selected missile redrawn in its new position with the following three lines of code.
LDA (SHPML),Y ;LOAD OLD MISSILE DATA ORA SHOTS,X ;MERGE SHOT DATA WITH OTHER MISSILES STA (SHPML),Y ;STORE NEW COMBINED MISSILE BYTE IN MEMORY
The technique is best illustrated with an example where we have three missiles on the screen. Missiles #0 and #1 are on the same two scan lines. Missile #2 is on the same scan line where we wish to move our missile.
Download PMINTB.EXE (Executable program)
Download PMINTB.OBJ (Object code)
Download / View PMINTB.LST (Assembler listing)
Download / View PMINTB.S (Source file)
Download / View PMINTB.RAW (As printed in book)
Two Ship ExampleWe can write a simple two-player shoot-'em-up game in BASIC if we use our player-missile subroutine to provide enough animation frame or speed for playability. The code for the second ship is nearly identical to that for the first ship. In fact, with the exception of using variables having a I at the tail end of the variable names, the code is the same, line for line. The code for player #2 follows the code for player #1.
The code for each player includes: a joystick read routine to determine the ship's new velocity vector and to update its position; a similar routine to determine the missile's velocity and direction; and logic to prevent either the ship or the missile from leaving the screen boundaries. The ship in our previous example stopped dead while the missile moved. These ships, however, not only continue in their present course during the missile flight; they even alter course to evade enemy missiles.
In the previous example, if the missile movement code was executed, it branched past the player movement code. In this example, the missile code updates the player position, and the player position code updates the missile position. This enables the player to continue moving or maneuvering while the missile is in flight.
If you look at the game's flow chart for each player, you will notice that the two possible paths are decided by whether the joystick button is pressed. Pressing the button fires a missile in the desired direction, only if there isn't another missile already on the screen. Of course, if you don't give it a direction (joystick centered), it skips firing the missile and just updates the ship's position based on its current heading. When it fires the missile, it turns the missile flag on (M0FLAG = 1). To prevent it from firing again before the missile reaches the edge of the playfield, the program tests M0FLAG in line 430. If the flag is on, it just updates the missile's position based on its current trajectory. Finally, once the missile reaches the screen edge, it turns the flag off (M0FLAG = 0) and plots the missile offscreen.
When the joystick button isn't pressed, the program code reaches line 110, the beginning of the joystick read routine which determines the ship's new velocity vector. This enables the ship to change direction. Thus, if a player wishes to change direction immediately upon firing his missile, he must release the button quickly, so that it reaches this code on the next animation frame.
Once a missile has been launched, it will continue on its path even while the ship is being maneuvered. Therefore, the missile's position needs to be updated in this pathway, too. This is done immediately after the ship's position has been updated, but only if the missile is on the screen.
Collision and ExplosionsA game wouldn't be complete if we couldn't detect if one or the other ship were killed in combat. There are two ways to die in this type of game-by collision with the opponent's ship or by missile fire. The collision register at decimal 53260 ($D00C) detects player #0 to player collisions. If it returns a value greater than zero, two players have collided. Likewise, collision registers at 53256 ($D008) and 53257 ($D009) detect collisions between missiles and players. The first will return the value 2 if missile #0 collides with player #1, and the latter will return the value I if missile #1 collides with player #0. It is important that these collision registers are cleared to zero before plotting players and missiles on the screen. You do this via the HITCLR register at decimal 53278 (DO1E) in line 90. This line at the beginning of the animation frame loop clears all of the collision registers before any players or missiles are placed on the screen.
The rather simplistic explosions are linked to the sound routine. Each ship brightens from luminance 4 to luminance 14 in its own color, then blacks out quickly. The luminance changes within a low, rumbling sound loop. The formulas in lines 1570 and 1580 were designed to prevent INT (10-I*0.66) from becoming larger than the value 10. If the ship's luminance of 4 when added to this value became larger than 15, the ship's color and luminance would change as the color value would wrap to the next higher color with a low luminance.
Recall that the sound statement is SOUND (Voice, Pitch, Distortion, Volume). The pitch is set to 250, a very low tone, with a distortion value of 4. The even numbered distortion levels 0,2,4,8,12 introduce different amounts of noise into the pure tones, 10 and 14. The FOR ... NEXT loop (lines 1150-1580) decreases the volume level very slowly.
This example's slow animation frame rate produces a game lacking smoothness. BASIC is slow even with the use of Machine language subroutines. There are a lot of IF ... THEN statements that slow the game down. The game, however, will run a little faster if all of the REM statements are deleted. Arcade games really need to be written entirely in Assembly language to achieve fast, smooth animation. The Space War game that is developed at the end of the chapter is a very similar game, but smoother and more controllable.
Shoot Bricks GameThe next example uses both playfield and player-missile graphics in a timed game in which the object is to survive the longest between two crushing brick walls. The joystick-manueverable player can use a pistol to shoot any of the bricks out in the ten rows on either side of him. The bricks are replenished randomly at a rate slightly faster than the player can remove them. Thus, it becomes a matter of strategy and endurance to last for any reasonable length of time.
When designing a game like this, you must anticipate the player's strategy. The player, when confronted with the impossiblity of keeping the entire wall back, will retreat to either the top or bottom and shoot just at the blocks immediately surrounding him. If the bricks were still placed on the screen randomly even after a row of bricks closed completely, the player would have plenty of time to shoot at the few random blocks appearing in the rows adjacent to him. However, if the random blocks are not put on the screen in the already closed rows, bricks would appear more rapidly in the few open rows. This makes the game fast-paced and somewhat challenging. The game itself has little play depth, so don't expect it to hold your interest as a game. It was primarily designed to teach programming technique.
It is desirable to have a different color for each of the ten rows of bricks, but there is a maximum of only four color registers in the non-GTIA graphics modes. Therefore, we use display list interrupts to change a single color register while ANTIC is drawing the screen. It would be easier to use GTIA mode 11, but, unfortunately, this mode does not support collision registers.
We choose to do the display in ANTIC 5, one of the non-BASIC graphics modes, because the four-color characters are sixteen scan lines high. Each of the lines contains forty characters. The blocks are four color dots wide by 16 scan lines tall. The bricks are added and removed in adjacent pairs (two characters) so that they appear to be square-shaped. The availability of the extra colors had little to do with the initial programming design, but when we put in a scorekeeping timer at the bottom of the screen, we were able to draw the characters using a different color register from that of the blocks. A collision is never detected when the player touches the score line.
Setting Up Display ListThe screen resides just below the top of memory which has been lowered twelve pages to make room for both the player-missile memory and new character set. We choose to modify the display list for a Graphics I screen because the display memory (20 columns x 24 rows) is the same as ANTIC 5 (40 columns x 12 rows), and our display list is slightly shorter. The start of the display list, which is shadowed at locations 560 and 561, is exactly the same for both graphics modes. Therefore, it is easy to POKE in our new display list at DUST = PEEK(560)+PEEK(561)*256. This value is 37216 for 48K machines. If we substitute the following 20-byte display list for the original, we will have an ANTIC 5 screen.
112 These three instructions print 112 24 blank scan lines at the top 112 of the screen 69 ANTIC 5 with a LMS instruction added 128 Address of the first line of screen data 145 145*256 +128 = 37248 133 Display the rest of the data in 133 ANTIC 5 with a DLI added 133 We have a total of 11 Antic 5 lines 133 133 133 133 133 133 133 7 Text mode 2 for timer display 65 jump and wait for vertical blank 96 Address of vertical display list itself 145 145*256+ 96 =37216 (return to the top of this list)
InitializationThe initialization for this game resembles our previous example. The playermissile, clear memory, and display list interrupt subroutines are each POKEd into memory in their appropriate places in page 6. The code is stored as DATA statements. The first half of the character set (512 bytes) is then moved to its new location, just above the top of memory, starting at location CHRSET which is equal to PEEK(106)*256. Only the first two characters are used to generate the screen. The 0th character is a blank and is used where there are no blocks. This includes our empty 0th row at the very top. The first character is rewritten as a solid shape using color register 1. The bit pattern for each line in the character is 10 10 10 10 decimal 170 or $AA. Using this bit pattern for color register I is a deliberate choice. The characters in the score line use color register 2. Thus, a collision with our score letters and numbers (playfield 2) will not produce the same value as a collision with a block (playfield 1).
Blocks are placed initially on the screen in rows I through 10 for columns 0 to I I and 28 to 39. There are six pairs of blocks situated on each side of the player on each row. The offset into screen memory for any block is OFFSET = 40*R + C, where R and C are the row and column respectively. The location of the screen is shadowed at locations decimal 88 and 89. With SCREEN = PEEK(88) + PEEK(89)*256, the actual memory location of any block is SCREEN + OFFSET.
Player-missile graphics appear to be double-line resolution set at double width, but they are actually single-line resolution with each byte doubled. Two different players are used for the man. Player #0 faces left, and player #I faces right. They are the same color and have the same vertical position. Player #1 is placed on the screen initially at X=125, Y=50.
The two levels of difficulty are selected with the SELECT key. It toggles between putting blocks on the screen at one-half second intervals and one second intervals. An asterisk (*) at the bottom right of the screen denotes the easier level. The START key starts the game.
Main Game LoopThe game loop tests when to place a block randomly on the screen at one-half or one second intervals. VBLANK increments the timer at decimal 20 every sixtieth of a second. When it overflows (TIMER >255), location 19 is incremented. If we set TIMER = 195, after sixty cycles occur(one second), location 19 becomes 1. This makes a convenient test to determine when one second has elapsed. Likewise if TIMER is set to 225 with the SELECT key, after one-half second location 19 would be incremented. The'test at line 210, IF PEEK(19)=O THEN 344, skips putting a new block on the screen and updating the scoring timer unless the proper timing interval has elapsed.
There are two pointer arrays, L(10) and R(10), that keep track of the inside wall boundaries closest to the player for each row. Initially each of the L(I) elements are set to I I and each of the R(I) elements are set to 28. As blocks are added and subtracted, these values begin to change by multiples of two. For example, if a block were added to both the left and right sides of row 3, then L(3)=13 and R(3)=26. If blocks were added just to the left side, eventually the sides would touch when L(3)=25 and R(3)=26. Since we don't want to add a block to a row that is already touching, we could test if R(I)L(I)=I. If it equals 1, then we choose a different random row and random side to try to place another block. Eventually, we find a place even if it is in one of the rows to the right or left of our player. The gun fighter can be maneuvered around the vacant area of the playfield by a player using a joystick. When the joystick is positioned up or down, the man moves vertically up or down by four units. When the joystick is pushed left, a left-facing figure appears and moves two units leftward. Similarly, a right facing figure appears and moves rightward two units when the joystick is pushed right. The routine also supports diagonal movements incorporating combinations of vertical and horizontal movement. Each of the joystick movements sets the variable PLAY to zero or one to indicate which player is to be placed on the screen through our player-missile subroutine. The player that does not appear on the screen is placed offscreen at X=10. Again remember to set the old Y position equal to the new Y position (YOLD=Y) just after the subroutine is used.
Collision TestA collision between either player and the playfield #1 blocks has to be tested in two different places in the game code. Obviously the test must be done just after the player moves, but it also has to be done just after anew random block is placed on the screen. A collision is detected in either case if the value 2 is set in player #0's collision register 53252 ($D004) or player #1's collision register 53253 ($D005). Since the bricks appear to be crushing our man, it is effective to squash him by setting the player width back to normal. When this happens, it appears that the man is compressed towards the left. This occurs because the player image is always plotted from left to right and begins at the value in the horizontal position register. The GTIA just double plots player pixels when set to double width. The player struck by the left wall remains in contact with the wall when it is compressed, but the player struck by the right wall will shift to the left, or away from the wall, eight pixels. If we just correct the X position by adding 8, it will remain in contact with the right wall and look like it was also crushed by the block.
While a collision between a missile and a block isn't difficult to detect, the program must determine which block was hit. Obviously, the block must be in the row directly in line with the pistol. If you look at the diagram below, you will see that the pistol is sixteen scan lines below the top of the man. Since the top left position of the man is at X,Y, then YM = Y+ 16. The bullet fires from the tip of the gun at XM = X when facing left, and at XM = X + 8 when facing right. The movement routine prevents the pistol from going beyond the top of the first row of blocks. When the pistol is at the top of the first row of blocks (YM = 48) the man is at Y=32. Therefore, the formula ROW=INT((YM-32)/16) determines which row the bullet travels. If we substitute YM = Y+ 16 the expression simplifies to ROW=INT((Y-16)/16). For example, if the man is at the very top (Y=32) then his pistol is aimed along ROW=INT(32-16)/16 or ROW=1 as expected. The bullet moves 2 pixels horizontally each frame until it eventually collides with a block. The pairs of blocks are then removed from the side the player faces. Since the player can't move while the bullet is in motion, there is no ambiguity possible. For example, if the player (PLAY=O) were facing left on row 2, and the left block pointer L(2)= 9, then blocks 2,9 and 2,8 are removed. You need only POKE a 0 into locations SCREEN+OFFSET and SCREEN+OFFSET-1 where OFFSET 40*ROW + L(ROW). Likewise, if a player were facing right (PLAY= 1), the two blocks that need to be removed are at locations SCREEN+OFFSET and SCREEN+OFFSET + 1 where OFFSET = 40*ROW + R(ROW).
The game naturally ends when the player runs out of space. The stopped timer indicates the time elapsed. At this point everything has to be reset for the next game. The blocks on the screen are reset for the next game. The blocks on the screen are reset slowly due to Atari BASIC's naturally slow implementation of the POKE statement. This does give a breather between games. The timer is reset, then the player. A button press is all that you need to begin a new game.
Extra Colors via a Display List Interrupt Subroutine
Some of the advanced programmers might find the display list interrupt subroutine to be of interest. It changes the color value in color register 1 ($2C5) each time ANTIC calls it via a display list -interrupt. Since it keeps an internal counter called PLACE for its X register index, it must know when to reset its pointer. It does this by checking the value in the vertical line counter which increments by one for every two scan lines. When it is equal to line 50, which is two scan lines beyond the beginning of row # 1, it resets PLACE to 0. It loads PLACE into the X register, indexes into the color table, and stores it in the color register. Now PLACE is incremented by one. The next DLI just loads the next color in the table into the color register. The pushes and pulls on the stack at the beginning and end of our subroutine save the current X register and Accumulator so that they are restored upon return to your program.
Space War GameSpace War, the first game with a fully steerable spaceship, was developed at MIT. While most of the newer computer owners won't remember this game, practically everyone is familiar with Asteroids. Most versions of this game have a fully steerable spaceship that can be thrust in the direction that it is headed. Although some versions invoke an automatic deceleration mode, some Asteroid games require the player to turn his ship around so that it thrusts in the opposite direction to slow down.
Dynamics of Motion with AccelerationWe demonstrated earlier in this chapter that objects move in the direction of their velocity vector. An object's new position is its old position plus its change in position due to velocity.
Using the Atari's screen coordinate system for the example above, VY is negative and VX is positive. Therefore,
While the velocity vector may remain constant for many animation cycles, so that a ship will continue to move in the same direction, sooner or later a new velocity vector will be input to change the object's course. This new velocity is the vector sum of the old velocity vector and the new velocity vector.
Those readers who have taken Physics will recall that the velocity of a body in motion changes due to external forces on it while it is in motion. In spaceships, that force is thrust. Thrust causes an acceleration of the object's mass as shown in the equation.
F=m*a=m* delta V
When thrust is applied to a spaceship, it accelerates. If a ship is light and has a big engine with considerable thrust, it will accelerate quickly. But if it is heavy, it will accelerate much slower. Acceleration is essentially caused by a change in the object's velocity if you ignore the object's mass,
Unless you are doing an actual simulation, in which the values of thrust or force and an object's mass are important, only acceleration values need to be considered. Suitable values for arcade games are small and scaled, so that objects don't move fast relative to their size, or fly off the screen in the blink of an eye.
If we consider a spaceship that is in motion for three frames, then thrust only during the fourth frame, it will change direction depending on the vector sum of its old and new velocity vectors. This is illustrated below. The applied thrust is straight upwards, so that VX = 0 and VY = -2. The ship's new velocity vectors for each direction are calculated as follows:
VX = VX(old) + delta VX = 2 + 0 = 2
VY = VY(old) + delta VY = -1+(-2) = -3
Likewise, the ship's new position is equal to its old position plus its change in position due to velocity for that animation frame. This breaks dowq into components for the X and Y directions.
X = X(old) + VX
Y = Y(old) + VY
The ship's new velocity vector causes it to move two units in the X direction and three in the negative Y direction during each frame until a new thrust vector is applied. The resultant position can be summarized in the table below.
Frame X Y delta VX delta VY 0 10 100 2 -1 1 12 99 2 -1 2 14 98 2 -1 3 16 97 2 -3 Thrust applied here 4 18 94 2 -3 5 20 91 2 -3Now, when the acceleration on an object, is sustained over many animation frames, the increase in velocity is cumulative. For example, if thrust were applied to a stationary object in the positive X direction with a force of 1 unit/frame, the new VX would increase from zero by units of one for each animation frame.
CYCLE VX X CYCLE VY Y 0 0 0 0 0 0 1 1 1 1 2 2 VX=1 2 2 3 Similarly VY=2 2 4 6 3 3 7 3 6 12 4 4 10 4 8 20
Our example makes it clear that if you accelerate for too many animation frames, the spaceship will be moving too fast. A limit should be set on the velocity vector for both directions. just what the limit should be depends on the effect you wish to achieve. Obviously, if your animation rate is that of VBLANK, 60 frames per second, a ship velocity of 5 units/cycle will race across the screen in one-half second.
Joystick RoutineA joystick will control the ship's direction in our game. Pushing to the left or right rotates the ship to one of eight directions. Pushing up thrusts the ship. The joystick is read in the VBLANK routine every 1/60th of a second. While a Machine language subroutine is more than equal to the task of keeping up with the update, its speed creates a small problem. If a player turns his ship by pushing left or right on his joystick, the ship will spin rapidly, making 1/8 of a turn every 1/60 of a second, That translates to 7 and 1/2 turns per second, a speed that is obviously uncontrollable. The joystick must be read less often, say every sixth VBLANK cycle if the ship is to turn slowly enough to steer.
Machine language joystick routines are inherently simpler because only four bit positions for up, down, left and right, need to be tested. Diagonals are usually ignored because they nearly always represent combinations of commands. For example, a diagonal up and right command in our game means thrust while turning right. The joystick read subroutine will detect the bit pattern twice, once when testing the up bit to determine if it should reset the velocity, and a second time when testing the right bit to determine if it should turn the ship. The bit pattern is as follows.
X X X X RIGHT LEFT DOWN UP 128 64 32 16 8 4 2 1All of the bits are normally set when the joystick is centered or in the neutral position (I I 1 1). When the joystick is pushed in any direction its particular bit position is turned off, as illustrated in the diagram below.
To test a particular bit position you only need to AND it with that bit. For example, to test if the left bit is turned off:
00001010 Diagonal left & up 00000100 AND #$04 00000000 Result=0The result is zero if the bit is turned off when the joystick is being pushed in that direction. If the joystick were in the neutral position, the third bit would be on, and the result after the AND operation would be non-zero. When the result is zero we want to decrement DIR, the ship's direction, and make sure that if it becomes negative ($FF) it is reset to #$07. The code is as follows. All of the code is indexed with the player number in the X register.
CHKLF LDA STICK,X ;READ JOYSTICK AND #$04 ;LEFT BIT BNE CHKLF ;BRANCH IF NOT ZERO DEC DIR,X ;DECREMENT THE SHIP'S DIRECTION LDA DIR,X ;CHECK IF NEGATIVE CMP #$FF BNE .1 LDA #$07 ;SET DIR=7 STA DIR,X .1 JMP CHKFD ;CAN'T BE RIGHT IF PUSHED LEFT CHKRT LDA STICK,X
Pushing forward on the joystick thrusts the spaceship. ANDing the joystick value with #$01 tests the up bit. If the result is zero, the joystick has been pushed up. The acceleration or thrust vector depends on the ship's direction. If the ship points to the right, DIR = 2, then VXT = 1, and VYT = 0. If the ship points diagonally upward and to the left, DIR = 7, then VXT = -1, and VYT = -1.
Ship's Direction and Velocity VectorsNote that many of our ship's directions produce negative velocity values, while others produce positive values. Separate routines are required for adding and subtracting in Machine language. BASIC, however, just adds a negative number (X = 5+ (-1)). That's the clue. Adding a negative number is exactly the same as adding a positive number in Machine language. The difference is that negative numbers, like -1, are represented by the two's complement which for -1 is $FF. There is a limit for signed numbers of + or -127, because the BMI instruction tests the carry bit and considers the value if set. With the simplification of our thrust vector addition problem, we can construct a table of velocity vectors for each DIR value.
The equations for the ship's two velocity vector components are as follows:
VXP = VX(old) + VTX(DIR)
VYP = VY(old) + VTY(DIR)
A speed brake can be incorporated into the algorithm to prevent the velocity from exceeding a preset value. This would be analogous to wind resistance on a fastmoving automobile. It prevents a vehicle's speed from increasing infinitely. I choose a maximum velocity of 5 units per frame. It is based on keeping the animation smooth and the speed in bounds. The code for the X direction is as follows:
LDA DIR,X ;GET PLAYER DIR TAY CLC LDA VTX,Y ;GET X(DIR) THRUST VECTOR ADC VX ;ADD OLD VELOCITY VECTOR CMP #$FA ;IS IT -6? BNE .5 LDA #$FB ;CLIP TO -5 .5 CMP #$06 BNE .6 LDA #$05 ;CLIP TO 5 .6 STA VX STA VXP,X ;STORE NEW SHIP VELOCITY IN X DIRECTION
Addressing the Correct Shape TableNow that we can control our ship in eight directions, we need shape tables for each of these directions. That means eight separate shapes, each twelve bytes long. The plot subroutine that places the shape in the player-missile memory area is virtually the same as that used in our player-missile subroutine discussed earlier in the chapter.
The zero page pointers to our shape and to its eventual storage location in player-missile graphics memory are set up in a subroutine called PLOTSET0 for player #0 and PLOTSET1 for player #1. Since our drawing routine takes the direction into consideration in order to obtain the correctly rotated shape from our shape table, we can find the correct low byte of the shape by the following formula:
SHPL = SHPLO(DIR)
The shape number DIR, which is also our direction, is placed in the Y register so that we can find the low byte pointer to our shape stored in a table called SHPLO. Each of the values in that table are twelve bytes apart starting at #300. The high byte is constant for all shapes.
LDY DIR ;VALUE FOR DIRECTION OF ROTATED SHAPE LDA SHPLO,Y ;AS INDEX TO PROPER LOW BYTE OF SHAPE STA SHPL ;STORE LOW BYTE POINTER IN ZERO PAGE LDA /SHAPEO ;HIGH BYTE STA SHPH ;STORE HIGH BYTE IN ZERO PAGE
If the ship were turned so that it was pointing right, then DIR = 2 and SHPLO(2) = $18. This low byte of the shape table is stored as SHPL. The drawing routine will now plot the second shape from our shape table.
Smoothing the Ship's MovementRecall that when we updated our ship's position in our last BASIC example, movement wasn't very smooth because of the slow animation frame rate. Potentially, we face a very similar problem here because the ship's velocity vector that controls its next position is only updated every sixth frame. If the ship's position is updated in the same loop as the ship's velocity, it will appear to have very jerky movement, sometimes moving as much as five pixels per frame. It would be better to move the ship in smaller increments more often, even as fast as the scan rate of sixty times per second. Of course, at slower velocities the ship would have to be moved less often. The trick is to control the rate.
If the ship were moving in a direction at full speed, VX = 5, we would want to update the position every frame, but if the ship were moving slowly, VX = 1, we would want to update the screen every fifth or sixth cycle. This means that we should skip plotting the ship at its new position until a number of frames has passed. We could use a counter called SKFLAG to check when we should plot. Naturally, it would be different for each velocity and could be stored in a lookup table for easy access. Its values would nearly be the reciprocal of the velocity. At VXP = 5 we would want to replot each frame so SKFLAG = 1, and when VXP = 1 we would want to replot every fifth or sixth cycle or SKFLAG = 6. The relationship between the ship's movement and its actual velocity VXP isn't exactly linear. If you look in the table you will notice that it is fairly linear at slow speeds, but jumps from twenty pixels every sixty frames at VXP = 3, to thirty pixels every sixty frames at VXP = 4, to a whopping sixty pixels every sixty frames at VXP = 5. Fortunately, this is only a game, and the discrepancy is not noticeable.
The code that determines when the ship is to be moved and replotted for each direction is quite simple. The number of frames to skip, SKFLAG, is obtained from our table based on the ship's current velocity and direction. Five has been added to the velocity so that negative VXP, and VYP values can be indexed, too, using the Y register. Counters for each axis, SCRCNTX and SCRCNTY, that keep track of the number of frames elapsed since the last screen update, are incremented. They are then compared against the values from our table. When it matches, that counter is reset to zero, and the player's position for that axis is updated and plotted. The code for the X axis is listed below.
UPDATEX CLC ;LOAD PLAYER'S HORIZ. VELOCITY LDA VXP,X ;SO NEG #S APPEAR IN TABLE TOO TAY ;USE AS INDEX LDA SKFLAG,X ;GET VALUE FROM TABLE INC SCRCNTX,X ;INC. COUNTER FROM LAST UPDATE CMP SCRCNTX,X ;AT UPDATE TIME? BGE .1 JMP EE ;NO! DON'T UPDATE .1 LDA #$00 ;RESET COUNTER STA SCRCNTX,X LDA VXP,X ;BEGIN UPDATING PLAYER POSITION
Each time the player's position is updated in either axis, the player moves one pixel position in the proper direction. If the velocity is negative, the player moves either to the left or up depending on which axis is being updated. If the velocity is positive, the player moves down or to the right. Remember we decided to move the ship only a single pixel at a time because moving the player in finer increments at higher frame rates produces smoother animation than discontinuous jumps at slow frame rates. The ship's overall velocity is the same, but the ship doesn't strobe as it moves.
The screen has a wraparound feature in this game. This means that when a ship reaches the right side of the screen and exits, it reappears on the left side with its velocity and direction the same. This is true in the vertical direction, too. A ship leaving the top of the screen will reappear at the bottom. All that is needed is a simple check to determine if the ship has reached the screen boundary. If it has, its position is reset to the opposite screen boundary. Since the boundaries of the playfield screen don't quite reach the edges of the television set, we extended the screen boundary coordinates by eight pixels in all directions. The ship's vanishing point should be nearly at the edge. If a ship is traveling to the right, it will vanish at X = 216 ($D8) and reappear at X = 40 ($28). Don't confuse playermissile coordinates with the screen coordinates used in PLOT and DRAWTO statements. The top left corner of the playfield in player-missile coordinates is X = 48, Y = 32. Coordinate 0,0 is offscreen.
Missile MovementThe missiles in this game are controlled entirely by a separate subroutine. The subroutine needs only the player number inputted in the X register to operate correctly. It decides if a new missile can be fired or if one is already on the screen, then moves the missiles appropriately. There is no need to read a joystick for missile direction as in our earlier game, because the missile fires and moves in the direction, DIR, that our ship faces. Also we don't need to worry about updating the ship's position while in the missile subroutine, since the main program loop takes care of this.
The subroutine's initial decision is whether the joystick trigger STRIG0,X ($284,X) is being pressed. When the button is pressed, the subroutine returns a zero, and, if untouched, a one. There are also timers TMIS,X for each missile, to count the number of screen cycles that a missile travels. The actual values are unimportant, except whether they are zero or not. When TMIS,X is greater than zero, the missile is already on the screen and merely has to be moved while checking for screen edge boundaries. However, if TMIS,X is zero, the missile has to be placed initially at the ship's position, and the firing sound started.
Since we only have one missile per player, we can't have rapid fire missiles or more than one missile on the screen at a time. Games that allow a train of five or six missile tracks on the screen at one time are using character set animation or bit mapping, not missiles. We could have allowed the player to reset his missile track each time he pressed the trigger, but instead we decided that you couldn't refire until the missile reached the screen's edge or hit its target. Since many players might hold the trigger down indefinitely we had to test in both branches for whether the missile was already on the screen. If we had omitted this test, the missile track would reset each time a player pressed the trigger. If you wish to change the game to this mode of firing, just remove the test.
Once a missile is launched, it must continue to move in the initial direction, regardless of a change in course of the mother ship. We accomplish this by storing its initial direction at launch DIR,X as DIRMO,X. This direction is used to look up the missile velocity vectors VTX and VTY from tables. The position is updated by the following formula:
XMIS(new) = XMIS(old)+VTX(DIRM0)
YMIS(new) = YMIS(old)+VTY(DIRM0)
The code is as follows:
.2 LDY DIRMO,X ;LOAD Y REG. WITH MISSILE'S DIRECTION LDA VTX,Y ;LOOK UP X DIRECTION VELOCITY VECTOR ASL ;DOUBLE VELOCITY VECTOR CLC ADC XMISO,X ;ADD MISSILE'S OLD X POSITION STA XMISO,X ;STORE NEW X POSITION LDA VTY,Y ;LOOK UP Y DIRECTION VELOCITY VECTOR ASL CLC ADC YMISO,X ;ADD MISSILE'S OLD Y POSITION STA XMISO,X ;STORE NEW Y POSITION
These missile shapes are not the square 2 by 2 blocks of our earlier example. They are two pixels set vertically, horizontally, or diagonally in the direction of travel. A chart of their shapes follows:
First, notice that each missile has a different set of numerical values. Missile shape #0 uses the first two bits in the byte, and missile #1 uses the third and fourth bit positions. Therefore, there is a shape table for each missile. The shapes are stored in two-byte pairs for the two scan line high shapes. There is also an index to the low byte to each of these shapes. They are in two tables called MISLO and MISLO1 for each missile set respectively.
The missile setup subroutine sets up the zero page pointers to the correct shape and to the storage location in player-missile memory. This area is just below the players or.75K from the start of player-missile memory. In addition, the proper mask for that player is stored as MASK,
Plotting the missile requires erasing the old missile first by ANDing the byte containing all four missiles with the proper mask. This step erases only the desired missile because the mask contains zeros in the missile bits to be erased and ones elsewhere. We then plot the missile on the screen by ORAing with a byte in the missile memory that may or may not contain other missiles. For example, if we wish to erase missile #I in a byte that contains missile #0, we AND it with the mask $FC.
00001010 MEMORY AND 11111000 MASK 00000110 RESULT
If we move missile #1 to a scan line that contains missile #3 then we ORA its shape with the byte in missile memory for that scan line.
00000100 MEMORY ORA 00000010 SHAPE 00001000 RESULT
The code is shown below:
MPLOT LDY #$00 .1 LDA (SHPMOL),Y;LOAD OLD SHAPE AND MASK ;MASK WITH PROPER MISSILE BEING MOVED STA (SHPMOL),Y;STORING IT DOESN'T ERASE OTHER MISSILES INY ;NEXT BYTE CPY #$02 ;MISSILE 2 BYTES HIGH BLT .1 LDY #$00 .2 LDA (SHPL),Y ;GET BYTE FROM CORRECT SHAPE TABLE ORA (SHPML),Y ;ORA AGAINST OTHER MISSILES ON SCAN LINE STA (SHPML),Y ;PUT IN MISSILE AREA INY ;NEXT BYTE CPY #$02 BLT .2
Derez Style ExplosionThere are many types of explosions suitable for destroying a killed spaceship. One of the more effective methods is to de-rez the object. This technique makes the object appear to slowly disintegrate by randomly flickering the individual pixels until most of the pixels vanish.
The trick is to take the ship and store it into a temporary shape table called DEREZ. We load a random number, then ORA it with another random number to insure that we don't get a number with a few of bits "on". We then AND it with the shape in DEREZ and store it there. If we then AND the original ship's shape with this value three times, we get a significantly degraded image of the ship. We then ORA this with our degraded temporary shape in DEREZ so that we get a less degraded shape than we would get if we performed only the first of the two previous steps. Since we wanted this effect to last at least 30 cycles or half a second, the routine was largely experimentally determined. The image begins to degrade slightly during each cycle but isn't quite steady. Any byte's image can improve slightly but randomly in any frame, but the overall effect is continued degradation. The loop is 48 cycles but the image usually vanishes completely after 30 cycles. An example of two separate passes for a single byte is shown below.
The explosion sound gives the effect of a slow rumble that slowly decreases in volume. We are working with a fairly low frequency and a distortion of zero in channel one and a distortion of 8 in channel 2. The use of two channels with these distortion and frequency values was determined experimentally to produce the best effect. ORing SBANG/4 with $E0 gives us a range of frequencies $EO-$EF. These are stored in AUDF1 and AUDF2. Since both AUDCI and AUDC2 use the lower nibble for volume control, the value for AUDC1 can be obtained by ANDing the frequency with #$OF to mask out $EO. Since the upper nibble is the distortion, then a distortion of 8 can be obtained in AUDC2 by ORing AUDCl with #$80. The equivalent sound routine in BASIC is as follows:
10 FOR SBANG=64 TO 0 STEP -1 20 X=INT(SBANG/4) 30 SOUND 0,224+X,0,X 40 SOUND 1,224+X,8,X 50 NEXT SBANG
ScorekeepingThe score line is Written directly into GRAPHICS 1 (ANTIC 2) playfield memory beginning at the top of the screen memory. These locations SCREEN to SCREEN+ 19 contain internal character code valves stored initially in a table called SCLINE. During initialization they are moved into screen memory and the scoring digits, SCREEN+7, SCREEN+8 (tens digit, ones digit) for ship #0 and SCREEN+18, SCREEN+19 (tens digit, one's digit) for ship #1 are updated during the game. A diagram showing the score line and its internal character data is illustrated below.
Flow Chart & Game Code
Download SPACEWAR.EXE (Executable program)
Download SPACEWAR.OBJ (Object code)
Download / View SPACEWAR.LST (Assembler listing)
Download / View SPACEWAR.S (Source file)
Download / View SPACEWAR.RAW (As printed in book)
Player Missile EditorWe have included a player-missile editor that will make designing player shapes easy. The grid on the left hand side of the screen supports a shape twenty-two scan lines high in the single-resolution mode. Each of the enlarged blocks is the same shape as the individual pixels on the screen.
The cursor appears in the upper left hand corner. The message at the upper right initially is set in the NOPLOT mode. To put the cursor in the PLOT mode, press the letter P, for the ERASE mode, press the letter E, or return to the NOPLOT mode by pressing N. Pressing any of the arrow keys without the control key moves the cursor. Move the cursor to your desired starting position, and press P for plot. A block fills for each position of the shape, and the byte's decimal and hexadecimal value appears to the side.
As your shape forms, you will begin to see an actual sized player shape emerge in the lower right portion of the screen. You can toggle the player width by pressing the W key. It will progress from single width to double width to quadruple width before returning to single width. Likewise, toggle the player resolution from singleline resolution to double-line resolution by pressing the R key. If you wish to clear the entire screen of your shape, just press SHIFT-CLEAR.
The editor, which is written in BASIC, is somewhat slow. Most of its slowness is due to our decision to write it in graphics mode 8, so that we could plot accurately while simutaneously incorporating text on the screen. Machine language programmers need the hexadecimal values of the player shapes for their shape tables. Owners of the ABC BASIC compiler by Monarch Data Systems will find that the program compiles with no modification, and runs significantly faster.
Player-Missile Movement Using Strings
An alternate method of moving player shapes vertically at close to Machine language speed is to take advantage of Atari BASIC's string manipulation routines. One in particular in the form of PO$(N)=SO$ will take a smaller string such as S0$="ABCD" and place it starting at the Nth position of the larger string P0$. Thus, if N=6, P0$ would be as follows:
BYTE VALUE 1 0 2 0 3 0 4 0 5 0 6 ASCII A 7 ASCII B 8 ASCII C 9 ASCII D 10 0 11 0
The shorter string could just as easily be our player shape. It would be very easy to move it just by changing the value of N. The only hitch is that we would have to erase its previous position before we moved it, or the player string would begin to repeat itself, or parts of itself, within the larger (256 byte) player area string. Since there is no need to move the player more than two bytes at a time, we can easily accomplish the erase by overwriting the trailing or leading edge of our last position with two trailing null or zero characters at each end.
How BASIC Stores StringsThe biggest problem in this method is -to fool Atari BASIC into assigning the player-missile display area as a string variable. In order to understand how to do this, it is necessary to know how Atari BASIC stores variables in memory. Two areas are set aside in memory. The first, the Variable Value Table Pointer (VVTP), stores eight bytes of information for each variable declared in your BASIC program. The second, the String Array Table, reserves space in memory according to the size specified when you dimension an array. The VVTP table is arranged as in the following example. If it were for a string dimensioned as P0$(255) it would have these values:
Every time BASIC has to access information in a string it goes to the VVPT to find the current address of the string. Byte I contains the variable type which is 129 for a string. Byte 2 contains the variable number. BASIC tokenizes variables and refers to them by numbers, not names. Bytes 3 and 4 contain the value of the offset from the string/array area (STARP) where BASIC reserves memory for all dimensioned variables. The values are in low byte, high byte order. Thus the address of your string is STARP + Byte 3 + 256*Byte 4.
If we change the pointers to bytes 3 and 4 so that they now point to the playermissile area for player #0, we could use string move commands to move our player. To do this we will need to calculate the difference between the player-missile area and where BASIC was planning to store our string in STARP.
VVTP= PEEK(134) + 256*PEEK(135)
STARP = PEEK(140) + 256*PEEK(141)
PLAYER0 =PMBASE*256 + 1024
OFFSET=PLAYER0 - STARP
If we take this OFFSET value and divide it into its low and high byte values, then POKE them into bytes 3 and 4, BASIC would think that our strings were actually in the player-missile area. Since we have four players, we have to do this for each group of eight bytes in the VVTP table. Your dimensioned player strings should be the first variables in the program; otherwise, you will have to scan through the table looking for a match.
The following example uses this technique to move four players vertically. Horizontal movement is of course done by setting the player's horizontal position register. The example has four randomly moving, bouncing balls which reverse direction upon collision with another ball or the playfield boundary.
Just to give you a rough idea of the actual memory locations and the change we need to make in the VVTP table for player #0, the values for the examy4e are listed below:
VVTP = 7760
STARP = 10932
PMBASE = 38912 (High byte = 152)
PLAYER0 = 39936
so that OFFSET = PLAYER0-STARP = 29004 then LO = 76 and HI = 113 and they are POKED into bytes 3 and 4 respectively.
While this is a clever method of obtaining fast vertical motion for players, there is no easy way to animate more than a single missile when using strings. The technique is shown more for completeness than for usefulness. We suggest that you use our player-missile subroutine for your games and avoid the complications of using string manipulation.
Return to Table of Contents | Previous Chapter | Next Chapter