|
Chapter 9 Advanced Arcade Techniques
Maze Games
Maze games achieved the height of popularity with the debut of
eat-the-dot games like Pacman and Ms Pacman. They weren't the first
eat-the dots games: that honor goes to a car game called Head-on. They
were, however, the first games to transpose the usual open field chase or
pursuit games to the narrow confine of maze corridors.
Topographically, a maze is merely a network of interconnecting paths.
These pathways constrain the player's movement. In a sense each individual
section of the maze gives the player a set of movement rules. The passage
walls that are open allow the player to reach the next section, while the
closed walls block movement in other directions.
The maze can be divided into a number of small blocks, each the height
and width of the passageway. Depending on the graphics mode, each of these
blocks could assume the size of a character. That way each character
becomes one of the blocks in the maze. The characters can be open blocks,
with various combinations of open exterior walls, or they can be floors
and ladders for use in a climbing, jumping arcade game. While most people
do not think of games like Donkey Kong and Apple Panic as maze games, they
too require a set of movement rules to keep the player confined to floors
and ladders.
The instructions that guide a player about the maze can be very
complicated, or they can be quite simple. They can take thousands of bytes
or just several hundred. Naturally, each individual block needs to be a
part of the playfield and requires one byte per block. If we drew out maze
in graphics one characters (eight dots by eight rows), we could have a
maze twenty blocks wide by twelve rows deep. That takes 240 bytes of
memory. Next we need instructions to tell the player if he can move up,
down, left or right from the center of the block where he is. That could
take as many as four bytes per block. These could all be placed in tables
so that if we knew which block we were in, we could index into each table
look up the legal moves for that block.
Storing a zero for an open pathway and a one for a closed pathway
doesn't use a memory location to its capacity. Only the lowest bit is
used. If we could combine all four directions into one byte, with each
direction using one bit, we would only use a fourth as much memory and
still have half of the byte left. With Left using the lowest bit or 0th
bit, Right the first bit, Up the second, and Down the third, we can test
each of these bits by ANDing with a MASK that contains a I bit in the bit
position we wish to test and 0 bits everywhere else. LEFT CLOSED 0000 0001
RIGHT CLOSED 0000 0010
UP CLOSED 0000 0100
DOWN CLOSED 0000 1000
Thus to test if the up direction is closed we AND the instruction
byte with $04. In the following example only the right direction is open. 0 0 0 0 1 1 0 1 INSTRUCTION BYTE
0 0 0 0 0 1 0 0 AND #$04
_______________
0 0 0 0 0 1 0 0 RESULT IS POSITIVE IF UP DIRECTION IS CLOSED
We could set a flag for the gates that we find open. If we did the
above test and found the result positive, or the up direction closed, we
could set FLAGU = 1. The four flags FLAGU, FLAGD, FLAGL, and FLAGR, would
be set to 0 if found open and to I if closed.
Since we need to design a character set to visually show which walls
are open and which ones are closed, it would be beneficial to mirror the
byte value of the block's instruction byte. Thus, if a character had the
only the left wall closed, its instruction byte would have a value of one.
Therefore, we will design the first character in the character set to have
only the left wall closed. The Oth character has no walls closed, and the
fifteenth character in our table has all four walls closed. The
instruction byte that reflects this has a value of 15 ($0F). Each of the
characters in our set is shown below.
The playfield consists of twelve rows, each with twenty of these
characters. As a whole they make up an entire maze. A player in the top
left hand block in the maze is in the Oth position of the character data
that ANTIC uses to generate the playfield or maze. If the player moves one
row down to position XB,YB (0, 1) it is at the twentieth position of the
character data; and one position to the right (1,1), the twenty-first
position position in the data. This can be formalized as:
BLOCK = (YB*20) + XB
If we index into the screen data, SCREEN,Y where the Y register
contains the value of the character at the player's position. That number
is the same as the sum of the individual instruction bits for legal
movement in each of the four directions. The fact that only one table is
needed for both the screen data and the legal movement instructions is not
a coincidence. It is simply a clever way to condense data.
The design of any maze game should include a simple premise with a
simple set of rules. The object of PacMan is to eat all of the dots on the
maze floor in order to advance to the next level. The object of the maze
game is for the player's letter to chase two higher letters in the
alphabet. The player catches the next higher letter in order to become
that letter as it advances toward the letter Z and a harder maze. just as
the four ghosts are the adversaries in PacMan, the red minus sign is the
adversary here. It constantly pursues your player and if it catches it,
your letter value decreases by one. Thus, the game begins with the letter
A chasing letters B and C. If it catches the B, it becomes a B and
continues chasing a C and D. If the minus sign catches your player at that
point, it reverts back one letter to the letter A.
There is no point scorekeeping system for this game. The progress you
make during the game becomes a visual scorekeeping system. The goal is to
progress forward, not to gain points. If we were to award 100 points for
each letter gained, and 50 points for each letter lost, players might
prolong the game indefinitely, advancing several letters than losing a
few.
All the players move at exactly the same speed. This forces strategic
play, since you can't catch the fleeing letters by simply tailing them.
The game must appear winnable, so we never make the player start the game
over when he is caught by the minus sign. The game would become
frustrating if a player advanced to the letter T and reverted back to an A
just because the minus sign caught him repeatedly. Beginners would also
find the game frustrating if the minus sign caught them and put them in
another random position in the maze, just as they were about to trap a
letter.
The maze game code is much too long to run within the time frame
provided by vertical blank. Since I had tested my theory of maze game
logic with a single player within the vertical blank period, I decided to
place the remainder of the game code outside the vertical blank time, but
sychronized with it. Normally code entirely within VBLANK runs in a
endless loop (FOREVER JMP FOREVER) and only leaves this loop during the
interrupt period. When your code runs outside VBLANK it runs until the
VBLANK interrupts it, then executes the code in the interrupt routine. At
the end of the vertical blank period it returns to where it left off in
the code. With the 6502 running at 1.8 MHz a moderately short program is
likely to cycle through twenty or thirty passes between VBLANK interrupts.
The only solution to synchronize the two pieces of code is to allow the
code to run once then wait in a tight loop testing for some flag that can
only be set if the program reaches the vertical blank routine. This is
precisely what we did. Our VBLANK routine sets a flag call VBFLAG = 1. The
main program code sits in an endless loop testing this flag. It can Only
exit the loop to the beginning of the program code when VBFLAG = 1. FOREVER LDA VBFLAG ;TESTFLAG
CMP #$01
BNE .1
JMP LOOPM ;EXITS WHEN VBFLAG=1
.1 JMP FOREVER
Player MovementThe player is joystick guided. When a player
orders his player to move in a certain direction a number of things
happen. First the program checks if the player is at the center of a block
and if so checks which movement directions are legal. For example if a
player initially started at the top left corner, it can only move right or
down. If the player began moving down, he could no longer move left, but
only back in the direction he came from, at least until he reached the
center of the next block. So once a player begins moving the legal
direction flags must be reset to reflect the legal moves between adjacent
blocks. Second, since player movement should be automatic once a player
begins moving in the desired direction, a auto flags denoted as DR, DL,
DU, and DD must be set. The auto flag is on when set to I and off when set
to 0. So if our player is heading downwards, DD=1.
Joystick SubroutineThe joystick subroutine is similar to other
joysticks routines except in its treatment of diagonals. Only a single bit
has to be tested if it is off to determine the desired direction for the
up, down, left and right positions. But in the four diagonal directions
two bits are off. Since we can only move horizontally or vertically in the
maze, a decision must be made to which direction the player means. If a
player were moving right and he wanted to turn up at the next intersection
he would likely point his stick diagonally up and right. Since he is
travelling right in an automatic mode, he probably doesn't mean to order
his player right but instead up. So if the right auto flag is on DR=1, it
means that he is already travelling right and intends to go up the next
time he reaches a block that allows him to move in that direction.
Similarly, if the is moving up, DU=1 so he would probably intend to go
right at the next intersection.
If you look at the flow chart for the joystick subroutine, you will
notice that in addition to setting auto flags and resetting legal flags
for each of the four primary joystick directions, a flag called INHIBIT is
set sometimes in the diagonal test. This is necessary because if for
example we had an up and right diagonal and we were traveling the logic
would say that we want to test for a right move. Since the right bit is
off, the apropriate flags will be set correctly, but when it reaches the
up bit test that bit is also off so that it will try to set latches for
that direction too. But if it has to pass a INHIBIT = 1? test, it will
prevent the program from reaching the second bit position test, The
INHIBIT flags are only set for situations where the code will branch to
the right and left bit tests first. Code that branches to either the up or
down bit tests can not reach, the left and right bit tests to cause
problems.
Whenever a joystick command is given to change the player's direction,
the joystick routine checks to see if the player can move in that
direction from its current position. It checks to see if the legal move
direction flag is open or closed in the desired direction. If it is open
(0), it shuts off all the auto flags except the one for the new direction.
Finally it resets the legal move flags so that it can only go forward or
reverse between blocks, and then moves the player one unit forward. If you
try to command the player to move in a direction that it can't go, it will
keep on going forward automatically if it can.
Auto Mode CodeThe auto mode is what makes the joystick routine
particularly smooth and responsive. If it didn't exist, you would have to
exert more effort to get your man to steer properly. Besides always
needing to push the joystick one way or the other, you would have to be
careful to reach the exact center of a block before successfully
negotiating a turn.
The auto mode code simply checks to see which auto flag is on and
checks if it is legal to continue moving in that direction. If not the
player stops until it receives a new command.
Legal Move SubroutineA subroutine appropriately called LEGAL
determines in which directions the player is allowed to move. You input
the player # in the X-register and its current X and Y positions, and it
sets the legal move flags FLAGL,X, FLAGR,X, FLAGU,X, and FLAGD,X for the
appropriate player. It is a two step process. First it decides if the
player is at the center of a block. Each block is 8 pixels wide by 16
pixels deep. For a player to be at the center of a block it must be at an
exact multiple of 8 horizontally and an exact multiple of 16 vertically.
The player's position is in player-missile coordinates. The top left
corner of the maze is at location 48,32. And the 8 scan line high pla'yer
is initially 4 scan lines lower in order to center it in a 16 scan line
higher to center it in a 16 scan line high
block. We can get two values TEMPX and TEMPY by subtracting 48 and 36
from the player's horizontal and vertical positions respectively. The
coordinates of the block XB,YB that the player is currently in are
calculated as follows. TEMPX = XP-48
XB = TEMPX/8
TEMPY = YP-36
YB = TEMPY/16
BLOCK = YB*20 + XB
It is quite simple to determine if the player is at the center of
the block by checking the values in TEMPX and TEMPY. If any of the first
three bit positions in TEMPX containing anything (a remainder) we have a
value that is not an exact multiple of 8. You can AND with #$07 to check
the first three positions of TEMPX for a non zero value. For example: 00001001 TEMPX=#$09
00000111 AND #$07
________
00000001 RESULT positive
Similarly we can AND with #$0F to test the first four positions of
TEMPY for a non-zero value or a remainder.
If the above two tests prove that we are at the center of the blocks,
we can test the individual direction bits for the character number for
that block. We open all of the flags before testing each direction. The
address to the screen data or maze map has previously been put in zero
page at locations MAPL, MAPH. The code for testing if the block's left
direction is open is shown below. LDY BLOCK ;BLOCK WE TEST IS USED AS INDEX INTO TABLE
LDA (MAPL),Y ;GET VALUE OF BLOCK
AND #$01 ;TEST LEFT DIRECTION
BEQ ;IF RESULT 0 THEN LEAVE FLAG OPEN
LDA #$01 ;SET FLAG CLOSED
STA FLAG,X ;X REG. CONTAINS PLAYER
.2 LDA (MAPL),Y
Computer Controlled PlayersThe three other players, the two
letters and the minus sign, are computer controlled. The two letters must
continually flee from the player's joystick controlled letter, while the
minus sign homes in on the position of the player's letter.
Each of the computer controlled sprites must determine the direction or
directions that they wish to travel relative to the player's letter in
order to either seek it or flee from it. If the playfield were completely
open the logic would be rather simple. The minus sign would approach the
player in one of two random directions, horizontally or vertically until
it was even in that axis, then move towards it in along the opposite axis.
A letter fleeing from the player's letter would would move away from the
player along one axis until it reached the playfield boundary, then move
towards the corner opposite that of the player's letter. Of course it
would get trapped in the corner.
Mazes have lots of corners where fleeing letters and pursuing minus
signs could become trapped. A pursuing player that found itself in a
parallel corridor would closely follow your motions as you moved back and
forth safe but next to it. There might not even be an escape since you and
it would reach the next turn at exactly the same time. On the other hand,
a player that fled would often get stuck in a corner awaiting for you to
become parallel with it along one corridor before fleeing along the other.
As many maze game designers will testify, the only practical solution is
to force both the pursueing and fleeing computer controlled players to
always move forward in the corridors, never in reverse.
The logic needed to keep a player moving forward is not always simple,
for sometimes it disobeys the general rules given to each type of player
to either pursue: or flee from the joystick controlled letter. In general,
if a player is at a decision making point, the center of a block, it must
decide if there is a direction available, other than the direction it is
traveling in or reverse. If it doesn't have a choice it just continues in
its current direction. But if there is a choice it must decide if one of
those directions is a better choice than the direction it is currently
travelling.
A set of four relative direction flags, RELL,X, RELR,X, RELU,X and
RELD,X, can be set to indicate whether the computer controlled letters and
minus sign should move towards our letter or not. Each of their X and Y
coordinates are checked against the joystick controlled letter's X,Y
coordinate. Obviously if the player's letter is to the left of the minus
sign, the minus sign would want to move left so RELL,X is set to 1. The
minus sign is player #3 (X-register = 3) so that RELL,3 = 1. The minus
sign would also like to move up so that RELU,3 =1. On the other hand, the
letter B, which has a similar relative positioning as the - sign in the
diagram below, wants to escape and move in the exact opposite direction.
The testing algorithm is the same for both types of players but instead of
setting RELU,1=1 the letter B would like RELU,1=-1. If it wants to go the
opposite direction it is much simpler to just set the flag for the
opposite direction than to test for negative relative flags. ThusRELD,1=1
is the exact equivalent and the letter subsequently flees.
The next step is to decide if any of these relative direction flags
match any of the legal direction movement flags. For example if RELL,1=1
and FLAGL,1=l then either the fleeing letters or the pursuing minus sign
can take the turn. If the move is possible it sets a move flag MFLAGL,1=1
for that direction. The reader at this point is probably muttering, "Not
another flag!" It is actually necessary because there can be more than one
possible direction to move. In that case the program will have to choose
one direction to move. There is a counter called NUM that increments each
time it sets a move flag. If NUM >1 we will have to choose a direction
randomly. The choice of directions are arranged in pairs left-right and
up-down. Depending on the random number value, the test will be on either
the vertical or horizontal direction first. There is no danger that the
program will miss finding a set move flag if it branches past the
horizontal axis since it only occurs when NUM=2. The two flags that have
been set can't be both left and right since a fleeing or pursuing player
would choose only one direction to move in any axis. Once it determines
which move flag has been set it resets the auto flag and moves in the
appropriate new direction.
There are cases especially in corners where the relative flags and the
legal flags don't match. In these cases where the player would become
trapped, the player must be forced to make the only legal turn even if it
means moving the wrong way towards the joystick controlled letter. I
called the subroutine CORECT. The routine, realizing that the player was
moving forward when it became stuck, tests which auto flag is set and
compares it with the legal move flag for that direction. If it can
continue moving forward it goes to AUTOP, the automatic mode for the
player, and bypasses the code to force it to make the corner against its
will. However, if it becomes stuck, it, it tests the legal move flags for
the two directions along the opposite axis. For example if it were heading
right and became stuck it would check FLAGU,X and FLAGD,X. There is no
need to check the FLAGL,X because the player is not allowed to reverse
itself. Once we have determined the new direction we reset the auto flags
and move it one pixel in that direction.
Player CollisionsEventually, either the joystick controlled
letter is going to catch the next higher letter or the minus sign is going
to catch it. When any two players overlap a collision register is set
which we can test. If the joystick controlled letter catches the next
higher letter it must become the next higher letter and the letter that
was just caught becomes a letter two higher than it was. Thus letter A
which catches B becomes a letter B, letter C remains the same, and letter
B becomes a D. The caught letter must be repositioned somewhere else on
the screen preferably at the opposite end of the
screen. If our hero is at the bottom left of the maze, we put the new
letter at the top right. The four possible placement positions aren't
random but in specific spots with specific starting directions. The actual
repositioning is accomplished in a subroutine called PLACE. It is very
simple and clearcut routine. It does randomize the left-right starting
positions for the bottom two locations because occasionally two letters
are caught almost simultaneously in either of the top two outer pathways.
A collision of your letter with the minus sign results in a decrement
of one letter except when you are the letter A. In fact all three letters
are decremented one letter so that if you are the letter C chasing a D,
after the collision with the minus sign you are a B chasing a C in exactly
the same position. Although the minus sign is repositioned at the bottom
of the screen on the opposite side of the maze from the player, it was
felt that penalizing your player by repositioning it at the top corner was
frustrating to beginners who got caught all too often and felt that it was
like starting over.
Each of the three lettered players has a variable that points to which
letter it currently is. POINTO refers to the joystick controlled letter,
and POINT1 and POINT2 refer to the two fleeing letters. This variable also
controls which letter shape is taken from the shape table during the
PLOTSET subroutine. The shapes are arranged in numerical order. Shape #0
is the minus sign. The 26 letters follow in sequential order. Two blanks
are placed at the end so that when the player reaches Y it is chasing only
a Z and one invisible player. When the player reaches Z there are two
invisible players still on the screen,
While it is possible to test whether player #1 or player #2 has
collided with our joystick controlled letter, you can't be sure that the
collision is with the next higher letter because that letter alternates
between the two players. Therefore a test comparing the values of the two
colliding letters must also be done to determine if the two letters are
one apart. Say our player is an E (POINTO=5) and it collides mistakenly
with player #1 that currently is a G (POINTI=7). The test POINT1-POINTO
gives a value greater than 1. Therefore we ignore the collision. But if we
collide with player #2 that is an F (POINT2=6) then POINT2-POINTO is equal
to 1. We then increment POINTO our player to a F and increment POINT2
twice to H. Now we have a F chasing a G and H.
Second Maze Level
Eventually the player reaches the letter Z in the game. There is a four
second pause before a new maze appears. Setting up the screen is simple.
The 220 bytes of character data for the second screen is moved into screen
memory starting at location SCREEN. The zero page pointers MAPL and MAPH,
which are used by the subroutine LEGAL to obtain the value of a particular
block, are also set to point to the character data at DSCREEN2. LDX #$00
SLOOP1 LDA DSCREEN2,X ;LOAD NEW MAZE DATA
STA SCREEN,X ;STORE ON SCREEN
CPX #$F0 ;DONE?
BNE SLOOP1 ;NEXT BLOCK
LDA #DSCREEN2 ;SETUP ZERO PAGE POINTERS TO DATA
STA MAPL
LDA /DSCREEN2
STA MAPH
The layout of the second maze was designed to be quite harder. The only
open pathways are along the bottom and two sides. The interior pathways
are designed so that if you don't concentrate and choose your pathways
with care, a wrong turn will nearly always give the pursued letter a
bigger lead.
It is possible to add many more levels to the game. If there is only a
few you could duplicate the above code and by testing which level you have
just finished branch to the appropriate block of code to put the maze
character data in screen memory. More advanced programmers should set up
an indexed table for the hi byte pointer to each 256 byte block of
character maze data if there are more than four maze levels.
Pause FeatureA pause game feature is important to most arcade
games, especially one in which the game might last more than several
minutes. The problem with an Atari is that game code within the vertical
blank executes 60 times per second regardless of any pause control in the
main program loop. If the game is to stop, the pause code must branch past
the game code within vertical blank to XITVBK. In our case, if we branch
past the VBLANK flag which we use to syncronize the non-vertical blank
code, the rest of the game action will also stop. The pause code shown in
the flow chart below is placed at the very beginning of vertical blank.
Download
ALPHMAZE.EXE (Executable program) Download
ALPHMAZE.OBJ (Object code) Download
/ View
ALPHMAZE.LST (Assembler listing) Download
/ View
ALPHMAZE.S (Source file) Download
/ View
ALPHMAZE.RAW (As printed in book)
Tank GameA tank duel between two or more tanks was one of the
first few classic/games developed for coin-op play in the arcades. It was
quickly translated to the Atari 2600 game system under the name Combat,
and was supplied with the first four to five million game systems.
Regrettably, it was never rewritten for their home computers. While the
cartridge had some interesting variations like Tank-Pong, in which the
shells bounced off walls for quite some distance, it was unrealistic in
that the tank turret could only fire in the direction the tank pointed.
Basic Design of GameOur game, Tank Battle, is designed to be much
more realistic. The tanks, which can turn and drive in eight directions,
are equipped with rotatable turrets so that they can fire in directions
other than the one that they are traveling. The terrain features or
barriers, which are useful for hiding behind, can be blown away by tank
fire. It takes two shots to completely fracture one of the terrain blocks.
Tanks that are blown away are immune to enemy fire for several seconds
upon reappearance. This is necessary since the tank always reappears in
the same position and it would be only fair to allow the player to move
his tank or fire back at a tank waiting in ambush.
Regrettably, the control system is a compromise. Game design would be
so much easier if joysticks had two buttons. The single button serves a
dual function for turning the turret and firing the gun. When the button
isn't pressed, the tank is in the steering-drive mode. Pushing to the left
or right rotates the tank, and pushing forward or back moves the tank in
those directions. Since the tank can't penetrate barriers, it often needs
to be backed up to turn away from the obstacle. Pressing the button puts
it into the turret-fire mode. The tank can still be driven forward and
backward, but pushing the stick left or right rotates the turret
counterclockwise and clockwise respectively. The gun is fired when the
button is released.
The use of two players for each tank, one for the body and one for the
turret, thwarted any attempts to make it a four player game. We're not
saying that it couldn't be done, but we could just see the flickering that
would occur if two tanks using the same players ended up on the same scan
lines. Actually, now that we think about it, it could have been done if we
sacrificed the distinct second color of the turret. The turret might not
show clearly, but it would reduce each complete tank to one player. Now
you would need sixty-four tank shapes, one for each combination of tank
direction and turret direction. As it is we use eight tank shapes and
eight separate turret shapes for each of our eight joystick-controlled
directions.
Only one missile can appear on the screen at a time for each tank. Each
missile continues along its directed path until it exits the playfield or
reaches either the enemy tank or a blow-away wall. Since the missile is
armed upon pressing the trigger, it becomes increasingly important to be
able to not only adjust the turret's direction but also the tank's forward
and backward position before the shell is actually fired by releasing the
trigger. The inablity to reload and fire before the previous missile
completes its trajectory may be quite realistic in tank warfare, but can
be a liability in an arcade game. Perhaps this forces the game to
transcend the quick reflexes genre to one of strategic play.
The game was virtually designed to run in Deferred Vertical Blank.
While collisions, explosions, and missile movement are performed for both
tanks every VBlank cycle, reading the joystick and the actual calculation
and updating of tank and turret positions alternate for each tank every
VBlank cycle. Tank #0 is updated on even cycles and tank #I is updated on
odd cycles. This was necessary for it appeared that there were far too
many instructions to fit within the Deferred Vertical Blank period.
Updating the tank's position is one of the more intriguing problems in
this game. Calculating the tank's next position based on the direction
that it is moving is quite straightforward. The formulas are:
PLAYERH = PLAYERH + HOFFS(NEWD) PLAYERV = PLAYERV + VOFFS(NEWD)
where NEWD is the tank's new direction.
Tank Collision with PlayfieldThe tank obviously has to be
prevented from moving into a wall. We wanted to avoid the unsightly bounce
that occurs in many games that test illegal positions using
player-playfield collisions. The problem is that you can't test the
collision until you actually move there, but once you are there you have
to move back from the illegal position, or in this case, away from the
wall.
The solution is to test for a collision at the tank's next calculated
position before the tank is actually moved there. This can be accomplished
exactly the same way collisions are detected when rastering shapes on the
screen, by ANDing the player byte against the playfield byte beneath it.
If any two bits overlap, the result is a positive number. Remember that to
do the comparison, the tank doesn't have to be physically where it is
supposed to be. You need only calculate which player and playfield
character bytes intersect and compare the data for an overlap.
By calculating the tank's new position NEWH, NEWV, the offset into the
character XOFF, YOFF can be determined simply by ANDing the position with
#$07. This is equivalent to the expression NEWH -8 (INT(NEWH/8)). It masks
off the higher bits above 8 so that only the remainder is left. The actual
character involved in the intersection also has to be obtained. Its
position is at CHRX, CHRY and is obtained by dividing the new player
position NEWH, NEWV by 8 in each axis. Its position in memory is CHRHI*256
+ (CHRX +(20*CHRY)).
When the player shape is offset horizontally into the character, part
of its shape is in that character and the rest extends into the next
character to the right. If we are going to make a comparison in the data,
we will need to obtain data from that character also. The eight bits that
we need to compare are beneath the player byte but span the two different
character bytes. If we look at the diagram below, out player shape is
offset into the first character by five bits. Thus, three bits of the
first character byte need to be combined with five bits from the adjacent
character. Once we have a byte we can AND the character data with that of
the tank data. The data is tested for each byte of the vertical overlap or
until we detect a collision. If we don't detect a collision, we clear the
carry before exiting the subroutine. However, if we do, we set the carry
before exiting. Upon return we need only perform a BCS instruction to
determine if the desired move is indeed legal.
0th byte overlapped character data $FF intersects tank data $38 1st
byte overlapped character data $FF intersects tank data $C6 LDA (P0TMP1),Y $38 1 1 0 0 0 1 1 0
AND BYTE $FF 1 1 1 1 1 1 1 1
________________________________________________
RESULT >0 (Collision) $38 1 1 0 0 0 1 1 0
Updating Tank Position and RotationThe two tanks are updated on
different VBlank cycles. While it is true that tank #0 is updated on even
cycles and tank #1 is updated on odd cycles, there is a rest of two
additional cycles before they are updated again. Essentially, we are in a
four-cycle loop with two null cycles. If you look at the code beginning
with the label UPDATE, you will notice that we loaded the Accumulator with
the clock timer value and then ANDed it with #$02. If you look at the bit
patterns for the numbers 0-3 and perform the operation, you will get the
following: ACCUM. 00 01 10 11
AND #$02 10 10 10 10
______________
00 00 10 10
Notice that we obtain non-zero values for the second and third
cycles. We could branch on a non-zero test and skip updating the tank on
these two cycles. Incidentally, the clock doesn't need to be reset every
four cycles. It continues to count to 255. However, as far as the last two
bits are concerned, 4 is the same as 0, and 5 is the same as 1, etc. We
can then determine which tank to update by ANDing the clock RTCLOC with
#$01. The value is either going to be zero or one, and that can be placed
in the Xregister to obtain the appropriate tank's variables.
There are several more tests that have to be performed. Obviously, if
the tank is dead, we don't have to worry about updating it. Next we need
to decide if we are in the turret mode. This is important because we need
to distinguish between a button that hasn't been pressed and one that has
been just released to fire. In the latter case, the turret mode is set so
it branches past the first button test that would have shunted it to code
to move or rotate the tank, and instead reaches the second button test. If
the button is actually released, it means that the button had just been
released and it trips the gun to fire. The turret mode is turned of f in
that event. However, if the button is still held down it branches to
ROTATE where a new turret direction is calculated every fourth jiffy.
Tank RotationsThe calculations to rotate either the tank or
turret are very straightforward. The new direction is either incremented
or decremented depending on joystick direction. Both TURRETD, the turret
direction, and PLAYERF, the tank direction, have values between 0 and 7.
These direction values are used to obtain the correct shapes to plot in
the player-missile area of memory for both tank and turret. The tank and
turret rotations are coupled such that the turret's position relative to
the tank is constant as the tank is turned. If the turret points out the
front of the tank, it must remain in that position when the tank turns. We
update both the tank and turret directions in the code when turning the
tank. You'll notice that we are always keeping temporary values for
movement, including new tank and turret directions. Since the act of
rotating the tank could cause a collision with the wall, it may be
necessary to ignore the new values and simply not rotate the tank or
turret.
Missile or Tank FireThe beginning section of the VBlank code
concerns collisions between missiles and playfield, missiles and tanks,
and between two tanks. When a missile strikes the playfield it has to
distinquish between border characters and barrier characters. The
character set is as follows: 0 -Blank
1-6 -Border characters
7 -Unused
8-15 -Walls (in pairs)
The missile track ends when it hits a border character. If you
choose to change the maze so that border characters aren't used, you will
need to remove the comment field from some of the statements in lines 2540
to 2760. These statements actually test boundary conditions rather than
simply character numbers from 1 to 6 to determine if the missile reaches
the border. When the missile strikes a character that makes up a wall, it
decrements it if it is an odd numbered character, or erases it if it is an
even numbered character. The characters are set in pairs. The higher
numbered character is a complete shape while the lower numbered character
is the fractured shape.
Tank ExplosionsOf course, it is very likely that the player's
missile will hit its intended target, the other player's tank. If it does,
the hit tank begins to explode. In order to prevent the tank from
reinitiating its explosion by another closely fired missile, we need to
test if CYCLES >0. We also need to determine if the tank is immune from
enemy fire just after it reappears. Again we test for a value IMMUNE
>0. Both CYCLES and IMMUNE are counters that count down every VBlank.
The tank's explosion pattern is a series of horizontal lines that
rapidly expand vertically outward along the tank's 8-bit-wide player band.
The tank shape is replaced by a series of horizontal lines. Their vertical
positions are stored in two arrays, DZAP and UZAP. These two arrays each
store the vertical positions of eight of these horizontal lines. The lines
are moved at different rates by adding or subtracting different values to
their vertical positions. For example, if we consider the eight lines in
the DZAP array, they all begin initally at X=80. Then DZAP(INDEX) =
DZAP(INDEX) + INDEX, where INDEX ranges from 0 to 7 for the eight values
in the table. The higher values in the table move the fastest downward.
Eventually all of the lines exceed the screen boundary at #$CO and the
explosion ends. Similarly, the eight lines in UZAP move upward as
UZAP(INDEX) = UZAP(INDEX) -INDEX. They, too, eventually exceed the screen
boundary at #$20 and the explosion sequence ends.
The collision routine that detects collisions between the two tanks has
to be more selective than usual. If we aren't careful, the debris from the
exploding tank could blow up the other tank if it had a similar horizontal
position. This can be avoided by checking the explosion cycle counter to
see if it is greater than zero.
The blown up tank is put back on the screen once the CYCLE counter runs
out after four seconds. The IMMUNITY counter is then set to 255 jiffies or
four seconds so that the enemy tank can't sit in ambush. It does give the
defeated tank a slight edge for several seconds but this can be
compensated by having the other tank stay out of the line of fire.
Game Timer
The game is timed to last three minutes. This is set up initally as two
minutes and sixty seconds, and can be changed to suit the player. The
timer operates in the decimal mode. Both the ones and tens digits for
seconds are stored in the lower and upper nibbles of the variable SECONDS.
The nibbles are separated and converted into character data before being
stored in score screen memory at locations LINE3+10 and LINE3+11. The
value for minutes, which is in the lower nibble only, is easier to obtain.
It is eventually stored at LINE3+8. The game ends, GAME=0, when the timer
reaches zero. ENABLE, a VBI flag, is also set to zero at that time so that
the entire VBI routine is skipped when the game isn't in progress.
Download
TANK.EXE (Executable program) Download
/ View
TANK.LST (Complete assembler listing) Download
/ View
TANKMAST.SRC (Master source file) Download
/ View
TANK.EQU (Source file) Download
/ View
TANKDATA.SRC (Source file) Download
/ View
TANKSUBS.SRC (Source file) Download
/ View
TANKVAR.SRC (Source file) Download
/ View
TANKVBI.SRC (Source file) Download
/ View
TANK.SRC (Source file) Download
/ View
EQUATES (Source file)
Return to Table of
Contents | Previous
Chapter | Next Chapter
|
|