Defender - The Source ===================== Update 02 ========= By Dave Edwards =============== Assuming that you're reading this in order to find out about the ACC Defender program that I'm working on, and that you already have the old source file, then this update is devoted to the latest additions as of 4/9/92. VERY IMPORTANT! READ ALL OF THIS FILE BEFORE PROCEEDING. MUCH HAS CHANGED! SHOULD YOU FAIL TO NOTE ALL OF THE CHANGES, THEN MAKING USE OF THE DEFENDER SOURCE CODE WILL BE A GOOD DEAL HARDER THAN IT SHOULD BE! Defender is reaching the stage where it's starting to look 'whole'. I also have to admit that the blitzkrieg speed of the incomplete versions which pleased me enormously when I first saw it has faded due to all of the addit- ional material. I'm now performing 31 blits (including one huge one to clear the playing area) before I get round to plotting aliens, and on Attack Wave 15 there's 120 of them to handle, so processing time is getting tight! Real- time...HAH! Assembly Changes ---------------- Now the game is too big for one file. It's now in two parts. The file 'Defender1.i' contains all of the keyboard reassignment code (see below for a full resume of this new feature!) plus a few other ancilliary routines. Debugging Routines ------------------ When you assemble the code first time and run it, you could see some odd numbers appearing on the display in the actual play area. This is my own little debug tool, allowing me to see what's happening to parts of the prog- ram during testing. If you really HATE the sight of them and want to remove them, look for code resembling: move.l DebugL(a6),d0 moveq #10,d1 moveq #70,d2 bsr ShowLongR in the main game loop, and just comment it out. DON'T erase it altogether! If you're the type that likes really messing around with other people's code, it would be a good idea to hang onto this. Basically, three routines exist that allow you to examine variables, ShowByteR(), ShowWordR() and ShowLongR(). The byte/word/longword to show goes in D0, the x position to display the value is put into D1, and D2 contains the y position. Limits are: 0 < x < 39 (works on a byte-wide basis), and 0 < x < 255 (vertically, printing is pixel-aligned). The variable table entry DebugL is a block of 8 longwords for you to mung in accordance with your own preferences/fetishes should you wish to hack this code to bits. Expand the RS def for DebugL if you need more space, but I advise that you DO NOT move it from where it is in the table. And most cert- ainly DON'T insert data entries of your own before it if they're byte-sized or it's Guru time (address error-word/long access on odd boundary) for A500 and A1500 owners. A3000 owners are all right-68030's can do odd-boundary word and longword accesses provided the on-board MMU is configured to allow it. I wish I had an A3000... Keyboard Assignment ------------------- In response to Mark and a couple of others, the keyboard and joystick assignments for Defender are now player-configurable! The code that does this is split into several routines. Also, changes to the definition of the PlayerData structure have been made, so that players can maintain their keyboard and joystick preferences. This means that, among other new features, two players can have DIFFERENT keyboard and joystick as- signments, and the program will remember them! Aren't I kind? So, instead of having to remember any keyboard and joystick setups I may have forced upon you as in the past, you can now choose your own. Better still, you can choose to use the joystick for some functions and the keyboard for others. And to make life even easier, all you have to do is follow inst- ructions! Well, not quite. Basically, when the title screen is active, hit function key F3. The instructions for keyboard/joystick assignment changing will begin. First, you will be asked if you wish to change the assignments for player 1, to hit the ENTER key if you do, and the ESC key if you do not. Similarly, you will later be asked to decide similarly for player 2. Should you choose to change the keyboard assignments by hitting the ENTER key, the screen will change. In the left hand section of the screen you will see a game function being listed (for example, "move ship up"). At this point, if a keyboard key is selected, it must be PRESSED FIRST and THEN RE- LEASED. Once it is released, the name of the key pressed will appear. Should the user wish to select a joystick movement for this function, the same proc- edure applies:move the joystick in the given direction, and then release the joystick. The joystick fire button is also thus handled. Once you have selected the keyboard or joystick assignment for the first game function, each of the remaining game functions will appear listed down the left hand side of the screen, and the chosen keyboard/joystick ass- ignment will be lested down the right hand side of the screen when chosen. Finally, once ALL selections are made, you are invited to press the ENTER key if the selection is to your liking, or ESC to go back and re-select in the case where an error has been made. Once this selection has been made by player 1, player 2 will be invited to make a similar selection. How Is This Done? ----------------- This is the HARD part. Using the code shouldn't be too hard, but I had LOTS of fun writing it! Right. First of all, the interrupt handlers set the values of four variables (referenced via address register A6 as it always is in code that I have written). These variables are: OrdKey : Contains the hardware key code for 'ordinary' keys (i.e., all except the shift-type keys), or $FF if no key is being held down. ShiftKey : Contains bits set according to which shift keys are held down. These keys should be a prime choice for playing Defender, as they can be sensed independently! JoyPos : Contains the state of the joystick. For Defender, the VBL handler for the joystick only allows up and down movements to be sensed. JoyButton : Contains the state of the joystick fire button ($FF=on, $00=off). These variables are all byte-sized (handy), but to allow arbitrary selection of ANY of these to be used, I needed a fairly complex system to achieve this. First, I defined some pointer variables for each of the game func- tions (up, down, reverse ship etc). These pointers point to the variables I have listed above, using code such as: lea OrdKey(a6),a0 move.l a0,UpKeyPtr(a6) Secondly, in order to allow the keyboard variable to be sensed and unique key codes to be read, I defined extra variables into which the required VALUE of the variable for each function was placed. So if the user selected the non- shifted key "X" from the keyboard, then the pointer variable would point to the variable "OrdKey" above, and the value variable would contain the hard- ware key code of the "X" key. Complications arose when handling shifted keys and joysticks using this system. To this end, I defined a mask variable into which a mask value would be written. When deciding when a game function was to be performed, the code for that function would first obtain the pointer, read the value of the variable addressed by that pointer, logically AND the value with the mask, & then compare with the required value. The code in all instances is identical in form to the following fragment for the Reverse() function: move.l RevKeyPtr(a6),a0 ;get ptr to I/O variable move.b (a0),d0 ;get its value and.b RevKeyMsk(a6),d0 ;AND with mask cmp.b RevKeyVal(a6),d0 ;required key hit etc? bne.s WrongKey ;branch if it isn't All functions have their own pointer, value and mask variables. The complete list of these is: UpKeyPtr, UpKeyVal, UpKeyMsk : Ship up DownKeyPtr, DownKeyVal, DownKeyMsk : Ship down RevKeyPtr, RevKeyVal, RevKeyMsk : Reverse ship ThrKeyPtr, ThrKeyVal, ThrKeyMsk : Thrust FireKeyPtr, FireKeyVal, FireKeyMsk : Fire lasers SBKeyPtr, SBKeyVal, SBKeyMsk : Smart Bomb Why go to this trouble? Well, partly I wanted to allow the shifted keys to be handled in a bitwise fashion (the way that they were meant to be). To do this I needed to AND the value of the ShiftKey variable with a mask for the speci- fied bit, before comparing with the bit value. That way, if two or more shift keys were held down, only one would be looked for in the CMP instruction. So, what values are needed for each of these variables? To make the system work, each variable needs the following value for the following user selection: User Selection Pointer Value Mask -------------- ------- ----- ---- Non-SHIFT key Address of Hardware $7F OrdKey(a6) key code always SHIFT-type key Address of Set bit Set bit ShiftKey(a6) for given for given key key Joystick Address of 1 for up, $7F Movement JoyPos(a6) 2 for down always Joystick Address of $FF $FF FIRE JoyButton(a6) always always The 'Pointer' column contains the required value of the xxxKeyPtr variable in the earlier list, the 'Value' column the required value of the xxxKeyVal var- iable, and the 'Mask' column the requied value of the xxxKeyMsk variable. The various routines SetUserKeys(), AlterKeys() and friends are responsible for setting the variables appropriately. See the comments for more information! There is more! Not only are the xxxKeyPtr etc., variables defined in the main variable table, but there are copycat definitions in the PlayerData structure! Thus, once the user's keyboard assignments have been written into the keyboard/joystick assignment variables in the main variable table off A6, it's a simple matter to copy them into the PlayerData structure, and then to allow a different set of values to be written in for the next player! See the function SetPlayerKeys() for more information! Not only that, but when each player is brought back to life on the main game screen, the GetPlayerKeys() function copies the values BACK to the main variable tablefor that player, thus allowing each player to have differ- ent keyboard and joystick assignments! This idea can be extended to games of more than two players simply by creating more than two PlayerData structures within your own code! This can be applied HOWEVER the PlayerData structure is defined for other games too-see how wonderfully useful data structures are? Minor Complications ------------------- The ONLY problems arise in the following circumstances: 1) The user selects weird keys such as the numeric keypad keys, which don't have a character set definition for printing! 2) The user selects ALL unshifted keyboard keys for the game functions. In case 1), the information printed on screen will probably be garbage, since the system will attempt to find a printable character in memory and use what- ever CHIP RAM data it happens to hit upon. In case 2), the information disp- layed will be valid, but you won't have the luxury of independent response to different key presses to perform different functions (which is why I suggest using the shift keys). This will put you at a playing disadvantage to shift- key users who will be able to fire, thrust and move the ship in one go, and risks the occurrence of 'ghost' key presses as documented in the Amiga Hard- ware Reference Manual (which CAN'T happen with the shift keys). In this case, an injudicious key selection could result in strange things happening since the keyboard does NOT have true N-key rollover. Of course, rich users blessed with Amiga 3000s might have true N-key rollover keyboards in which case this won't be a problem. The moral is:pick keys wisely! Oh, and to maximise user I/O efficiency, those with functioning joysticks (unlike poor Mark, who has described the wonderful Yoga positions needed to use his joystick because it lacks decent suckers on the underside) should use joystick functions too. My personal preferences are (and these are the initial defaults for both players that I set up at the start of the game): Move ship up : Joystick UP Move ship down : Joystick DOWN Thrust : Left ALT Fire : Joystick FIRE Reverse : Left AMIGA Smart Bomb : Left SHIFT Since the Amiga can tell whether left or right shift-type keys are pressed, left-handed joystick users can simply substitute right-shift-type keys in the above list for left-shift-type keys, assuming that they would hold the joy- stick in their left hand. I'm right-handed and hold the joystick in my right hand, just to make matters clear. So, not only can different players have a different keyboard and joystick configuration, they can also have configura- tions that suit left- and right-handed players! The ONLY restrictions thus far are that you can't use the ESC key or the function keys, you can't use the mouse, and the joystick MUST be in its usual haunt, i.e., plugged into gameport 1. The function keys only get used when the title screen is active, and the ESC key is used to exit the game! Anyone who wants mouse control, write your own mouse handler! Also, you can have all the fun of rewriting the keyboard/joystick configurer for mouse handling - I think I've done enough! Scanner Handling ---------------- This is a nice complicated topic for me to document, since I've yet again adopted some nice convoluted approaches! First of all, I decided to use the hardware sprites as the means by which I would implement the scanner. My initial reason for choosing this path was to take advantage of the independence of the sprites from the rest of the hardware, and give them the hard work of displaying the graphic data while I used the processor and the blitter for other, more important tasks (such as the landscape-see below!). However, once I began implementing the scanner using the sprites, I found that the memory and register organisation of the sprite system was as friendly as a tankful of piranha fish at feeding time. But I decided to per- severe anyway, and this is the result. Some restrictions that were imposed from the start concerned colour selection. By using all eight sprites, and setting the attach bit in the con- trol words, 15 colours would be possible (even on an 8-colour screen!). But I decided that I had to use six sprites without the attach bit set in order to fill the space allotted to the scanner on screen, and this reduces the number of usable colours to three. This need not hamper the scanner, however, as will be seen (using the odd trick or two). The major obstacles to using sprites for the scanner fall under the headings of register design and memory allocation. So, to some routines. First, a routine called MakeSprCTL() is used to construct the sprite control words. Pass the sprite X/Y position in D0 (see the source comments for more info), and the sprite height in D1. The routine returns the sprite control words in D2 and D3. Feel free to cut out and re- use this routine in any of your own programs-it has LOTS of uses within any program needing sprite manipulation, and is freely available for such. Just to make everyone's life easier, I've included in the comments a breakdown of the format of the sprite control words that are produced by the routine, thus allowing the coder to make sense of the code, and a perusal of the format of the sprite control words will give some indication of the horrors of handling Amiga sprites directly in hardware! Next, the ZeroSpriteImg() routine is used to clear that part of the sprite data used for the graphic image. Pass a pointer to the sprite memory block used in A0, and the height of the sprite in D0. Again, see the comments in the source to find out how it works. Right. A quick refresher course on sprites! Sprites are organised in the following way in memory (in this illustration, N stands for the start ad- dress of the sprite memory block, and H for the sprite height): N : Sprite Control Word 0 N+2 : Sprite Control Word 1 N+4 : Sprite Data, Line 1, Plane 1 N+6 : Sprite Data, Line 1, Plane 2 N+8 : Sprite Data, Line 2, Plane 1 N+10 : Sprite Data, Line 2, Plane 2 ... N+H*4 : Sprite Data, Line H, Plane 1 N+H*4+2 : Sprite Data, Line H, Plane 2 N+H*4+4 : Zero Word N+H*4+6 : Zero Word So, if we wish to alter the graphic data in line 7 of the sprite graphic (as- suming of course that the sprite is at least 7 lines high!), we need to per- form something of the form: lea SpriteData,a0 ;ptr to sprite block moveq #7,d0 ;line 7 add.w d0,d0 ;* 4 for long add.w d0,d0 ;word access move.l #DATA,d1 ;sprite data to write move.l d1,(a0) ;alter it Now, the BIG problem is how to address the correct sprite out of the six that I am using for the scanner. And believe me, it IS a big problem! Welcome to the solution. This should give novice coders a unique in- sight into the devious mental twists required to implement game code, even a game such as this, which is most certainly NOT state-of-the-art in terms of coding sophistication! First, my objects have an X coordinate ranging from 0 to $1FFF (or in decimal, 0 to 8191). There are 8192 possible values for X in this scheme, al- though this could be changed. Coders attempting to change this be warned that doing so will upset the scanner, because the code assumes that the X coordin- ates of aliens lie within the above limits. Now six sprites (each 16 pixels wide) contain 96 pixels. So I need a way of turning an X coordinate of 0 to 8191 into an address pointing to one of six sprites, plus a bit number within the sprite so that I can set that bit and form a dot in the scanner. Well, let's use a table. Table accesses are nice and fast. But since I don't want to set up a table of 8192 elements, I can't use the X coordinate directly as an index. So I use X/64 as in index, giving me a number from 0 to 127. That sets us up nicely with 128 elements required in the table. Already I have less work to do. Now, how to construct the table elements? Use a byte-sized element- it's economical on memory and doesn't require silly scaling tricks on the in- dex quantity used (you know the sort of thing:you pick an index & then you discover that you've got to multiply it by 4 to index an array of longwords) and thus makes life a little easier again. Let's furthermore divide the byte into two nibbles:the upper nibble contains the bit number, and the lower nib- ble the sprite number to use. Again, nice and simple. This table lies at the label __SPTH (Sprite Pointer Table Horizontal) and contains all 128 entries required. This is where the fun starts. We now need the ADDRESS of that sprite! So, we use yet another table. This table contains only six elements, namely the pointers to the six sprites to use. Actually, I use pointers not to the start of the sprites (since the first two words contain sprite control data) but to the first word containing sprite graphic data (just to simplify life even further). This table lies at the label __SLst (Sprite List) and contains the six sprite pointers. To get the pointer, I use X/64 as an index into the first table, and then extract the sprite number from that entry. This sprite number is used as an index into the second table, and from this, I extract the pointer to the memory area to write to. The first table also provides the bit number to set in that memory area, and I use it to create the appropriate bits using code of the form: moveq #0,d1 ;clear this bset d0,d1 ;d0 contains bit no. 0-15 swap d1 bset d0,d1 ;2 copies for 2 words ... ;other stuff... move.l d1,(a0) ;and set sprite data... Now, I use the Y coordinate to index into a third table (label __SPTV, stand- ing for Sprite Pointer Table Vertical). This table contains offsets to add to the sprite address obtained above. Well, since I'm constraining the table en- try size to a byte again, each entry is the number of LONGWORDS to offset to in order to create the correct scanner location. To create the correct offset to add to the basic pointer, multiply this entry by 4 (two ADD.W's do the job just fine). This table of vertical offsets needs to be sufficiently large to cover the range of vertical coordinates needed. Now the fun part. I can now convert the position of any object within the game to an address pointer and a bit number to set for the scanner. But I haven't told how the scanner is set up! Well, changes have been made to MakeCopper() to insert instructions into the Copper Lists to set the sprite pointers. Initially, they are all set to point to a pre-defined null sprite. The InitScanner() routine (which uses a bank of variables referenced off register A6) creates the six sprites with- in CHIP RAM, sets the pointers to the sprites in the variable table, and sets the sprites to zero. It also sets up the table of six sprite pointers ment- ioned above. Finally, just before the main game is brought to life, a special rou- tine is called twice, once for each Copper List. SetSprites() changes all of the sprite pointers in the Copper Lists, and I use WaitVBL() to ensure that I only alter the Copper List that is NOT ACTIVE (the VBL handler automatically handles the changeover, so that the CopWaiting variable in the main variable table ALWAYS contains the pointer to the inactive Copper List). This is nec- essary, in order to prevent weird display anomalies caused by altering Copper Lists while the Copper is processing them (and altering sprite display poin- ters while the sprite hardware is using them-this creates wonderful firework displays!). SetSprites() again uses variables referenced via A6. Now to increase flexibility (and allow yet more tricks), I expanded the AlienObject data structure to contain new entries. The new entries added are the computed address pointer to the sprite, a word containing the set bit to write to the sprite (actually, two such words, one word for each sprite bitplane) and a mask word to determine if that data should be written direct- ly to the sprite or not (again, two such masks). The structure entries are called: AO_ScanLoc (LONG) : Ptr to sprite memory AO_ScanBit1 (WORD) : Sprite Plane 1 data AO_ScanBit2 (WORD) : Sprite Plane 2 data AO_ScanMsk1 (WORD) : Sprite Plane 1 mask AO_ScanMsk2 (WORD) : Sprite Plane 2 mask Having added this lot into the program, I then expanded the main precomputa- tion routine BlitPreComp(), and used it to generate the data in each of the AlienObject structures before writing the pixel data to sprite memory. This routine also uses the mask data to ensure that for each of the different ob- jects, dots of the correct colour are written into the scanner. BlitPreComp() is also used to blank out unwanted dots when the object moves, and takes ac- count of the non-existence of the pointer when the game is first set up, thus ensuring that sprite data is only written WHEN A VALID POINTER EXISTS. One final point:the code in BlitPlot() which handles object death has to take account of the scanner's existence too, and blank out the dot for an object once it is shot. Since the data for the pointer and the sprite graphic are pre-computed and stored for each object before plotting, erasing the data is a simple matter. See the code and its comments for more info. And now for a trick. So that Pods can have dots that change colour, I added a piece of code to the Pod handler that changes the mask entries of the AlienObject structure for the Pods, so that the dots change colour every time they are plotted. Neat, huh? Just four lines of assembler too... Right. Now that you've had a good look at some of the more devious of my game code, feel free to implement suitably weird and wonderful ideas with- in your own code. You can borrow my ideas if you like, provided that 1) you credit me with them; 2) you can actually find a use for them elsewhere! But seriously, this sort of idea is the sort of thing that makes software tick, and the more ideas of this sort that coders come up with, the better the re- sulting code will be. OK, for now, I haven't optimised the program at all, I haven't done a truly thorough timing analysis and made moves to weed out all of the binary rust in the system, but for now it works, and works reasonably well. Even now the executable is quite fun to play with! Landscapes ---------- At last, Defender has a landscape! Well, it isn't much, but neither was the original landscape that was used on the arcade original. Mind you, when you see how much effort is needed to implement it, you'll appreciate it all the more! First of all, I dug out DPaint and drew a single 16-pixel long line, to use for the flat part of the landscape. Then, I drew two diagonal lines, so that the endpoints of the two lines form the corners of a 16x16 square in pixel terms. Again, I dug out the Kefrens IFF-Converter and created the three small graphic files in RAW-BLIT format (once again, thanks, Kefrens!). Now the fun part. I wanted a randomly 'hilly' landscape, so I devised a scheme to achieve this. First I create two tables, one containing bytes to determine which piece of landscape is being drawn at any one time, and one to contain some memory offsets. All will be explained... Right. The first table contains bytes taking the values 0, 1 or 2. A zero byte represents a piece of flat land, a '1' byte a piece of 'downhill' land (travelling from left to right across the screen in usual European fas- hion:Arabic-speaking and Hebrew programmers may treat right-to-left as their preferred direction, this being the direction in which both of these langua- ges are read) and a '2' byte a piece of 'uphill' land. The NewLSList() routine which achieves this performs several checks to ensure that the list is sensible. First, despite using random numbers to determine where the hilly bits lie, hilly bits are assigned in uphill/down- hill pairs. Thus if one traverses the list of bytes, one starts on flat land at a given height, and after going up and down a few hills (and even into a valley or two if the routine creates them) one ends on flat land at the ori- ginal height. Thus I can wrap around as many times as I like without the land moving up or down the screen after each wrap-around. So what we end up with is something like: _______ -32 / \__ -16 _______/ \____ ____ 0 \_ __/ +16 \__/ +32 in appearance. The numbers down the sides are the differences from the start height value of the flat land in pixels for each appropriate piece. Since the range of X coordinates goes from $0000 to $1FFF (0 to 8191) and each landscape element is 16 pixels wide, we need $2000/16 = $200 (512) elements in the list. So table 1 is a 512-byte table. Now the tricky bit. The routine that plots this little lot uses yet another programming trick. I pre-compute the values for the blitter registers for the first piece of landscape, and pre-store them in a special block with- in the main variable table. That is, I pre-compute COMPLETELY all of the val- ues needed EXCEPT for BLTCPTH/L and BLTDPTH/L (I'm doing a cookie-cut blit). For these values, I simply store the address of the top of the screen being plotted to at the start. Then, I pre-compute the vertical offset to be added to these pointers to plot flat land at the initially chosen height. This will remain a constant and so needs only be computed once. So when I plot the flat land, I use MOVEM to get the blitter data and transfer it en masse, EXCEPT for the pointers, to which I add the offsets before doing the MOVEM. But what about hilly bits? This is where the second list comes in. I compute a list of vertical offsets to be added on for the hilly bits (so that an 'uphill' piece is plotted 16 pixels nearer the top of the screen than the preceding flat land). This list is constructed so that the extra offsets to add on are correct for the ENTIRE landscape, so that a landscape such as the example landscape above will be plotted correctly. The relative pixel disp- lacements above are converted to memory offsets to add onto the pointer for plotting at the correct height. This list, incidentally, is 1K in size, being a list of 512 WORDS. And how do we index them and decide which parts of the list to plot? Well, our old friend CurrXPos (variable in the main variable table) which is used to determine which part of the game area we are currently in, becomes an index into these tables (when divided by 16 of course-see why I chose lots of powers of 2 for important parameters now?). And the loop that plots all land- scape pieces plots the first using the precomputed blitter values, adds 2 to the values of BLTCPTH/L and BLTDPTH/L stored in the variable table to move to the next column on the screen, and gets the next byte in the landscape byte table. This is used to index into a table of graphic pointers to select the correct graphic (flat/uphill/downhill) and a table of precomputed values for BLTSIZE (which will ALWAYS stay the same unless you change the graphics, and so get get pre-computed by the general initialisation routine InitVars() at the start of execution) for the required graphic. A copy of CurrXPos is kept and updated to index the table correctly (and a quick AND.W with the constant $1FF ensures wrap-around, hence the powers of 2 again:no messy arithmetic to slow things down) and this also indexes the table of vertical plotting off- sets. It takes a bit of doing, but it works! Oh, I had to do some jiggery-pokery to take account of the change in CurrXPos being negative when moving from left to right, and also to take ac- count of the correct blitter shift values for pixel alignment during screen scrolling, but since this was easier than re-writing the entire screen mana- gement code, I hope you'll all forgive me! Oh, and since I'm only using an 8- colour screen, I chose a red landscape since I didn't have a brown to spare, and if you're bothered about realism, imagnine you're flying over Mars, which is red for real (see NASA Viking probe photos if you don't believe me). Again, see source and comments for the complete rundown of how it all works, but this should provide a reasonable explanation for the many ACC disc users who don't yet regard 68000 assembler as second nature even with liberal comments. The Death Sequence ------------------ And now, the fateful moment when your ship is hit by something! There are two routines to handle this, the first is called MakeDeathList() and is responsible for constructing the DeathList for the ship. This list contains a number of values required for plotting the explosion of the ship. The first entry in the table is the number of explosion fragments in the list:currently there are 320 of them! The following entries are defined as follows: WORD : An offset from the current plot location of the ship on screen. This entry is not used directly, but is used as an increment to the following entry. WORD : Offset from the current plot position as above. but this one is used to actually create the screen plot position. The entry above is added on afterwards to create the movement of the fragment. WORD : Shift count. Actually TWO shift counts:the upper byte is used as an add-on, and the lower byte is used as the actual shift count, the upper byte is added on (modulo 16) to create the next shift value for the next frame of the explosion sequence. Once MakeDeathList() has generated the list at the start of play, a counter called DeathTime (in the main variable table referenced off A6) is maintained and while the counter is zero, the death sequence remains inactive. Once the ship is hit, the AOF_HIT flag is set in the ship's AO_Flags entry of its data structure, and the ShipCode is called (a pointer to this is found in the AO_SpecialCode entry of the data structure). This code sets the DeathTime counter to a non-zero value (for now, the value is 10), and then it sets the AOF_DYING flag in the ship's AO_Flags entry. Once DeathTime reaches zero, the ANF_DISABLED flag is set for both the ship and the flame Anim, to finally kill off the ship. Oh, and ship forward movement is killed off too, so that the ship stays in one spot when it dies. This took some extra thin- king, as the ship sometimes did not originally stop dead when in the middle of a reverse sequence. It turned out that I needed some extra code within the main game loop AND an extra variable (which I decided to call 'Brakes', since that is its effect upon the Defender craft). Note that aliens continue moving around while the Defender is stuck still, just as on the arcade original. The routine that plots the death sequence is called PlotDeath(), and reads the data entries from the DeathList, using them to plot small squares of 2-pixel width on screen. This is done directly using the 68000, as it is a complete waste of time to set the blitter up for a task this mundane! For those who wonder about the timing involved, it plots 320 of these explosion fragments surprisingly quickly. The use of precomputed memory offsets (thus reducing the arithmetic to little more than ADDs) helps here! Again, both of these routines are fully commented and documented, and can be understood by anyone who has successfully handled the code in its previous incarnations on ACC disc until now! Displaying Player Status ------------------------ Now you can see your score AND an indication of lives/smart bombs re- maining. Not too hard, just a simple bit of code that reads plot positions from a table (what else!) & plots the appropriate graphic. It also restricts plotting to a maximum of four items, so if you've accumulated 22 lives during play, the only way you'll know is when you lose your current life & all four ships remain on your display next time around. Bug Corrections --------------- The smart bomb handler used to detonate every smart bomb, one after the other, if the key used for the smart bomb was held down. This no longer happens-I've used a locking variable (SBLock) as I did for the laser firing to stop this. Now it waits until the key is released before unlocking smart bomb detonation. The laser fire lock now works according to plan, although the player needs a VERY FAST trigger finger if using the joystick fire button to create a 'laser shower'. On auto-fire, however, the results are quite impressive. I have also finally implemented the correct method of ensuring that aliens only die if their X coordinates bring them within laser range-now you cannot kill aliens that are behind the ship, as you could with earlier versions! Not only that, but the alien has to come within genuine laser range! Landers now kidnap Bodies properly, and turn into Mutants properly if they reach the top of the screen. I still haven't figured out why the occas- ional Mutant turns up with an off-screen y coordinate, because I've tried to prevent that. Now, however, it appears to be VERY occasional. You'll know if it happens to you because the scanner display will be corrupted, and you may find the bitplane moduli/Copper lists corrupted to boot, with the usual spec- tacular results. A Little Trick -------------- I'd like to point coders to the InitPlayerKeys() routine in the file 'Defender1.i'. Since my keyboard assignments use pointers to the various key and joystick variables instead of the variables themselves, I need to load a collection of pointers into the PlayerData structure. But ALL of my variables are created using AllocMem() and then referenced off A6! Until I create them I don't know where they are! So how can I supply addresses in the source at assembly time if I can't possibly know where AllocMem() is going to set them? Think about it-since the assembler can't know where these addresses will be, and neither can I as a programmer, how do I sort this out? Well, it's RS definitions to the rescue! These are all offsets within the variable table. A variable is ALWAYS referred to using: Variable_Name(a6) or similar in all of the other code. Since I'm using a static table of data to retain the keyboard/joystick preferences, which will be in existence be- fore the variables will, I can't use the usual references. However, I CAN use the offsets (which are defined at assembly time). So, I store the OFFSETS in the static table (which are fixed at assembly-time and won't move) using the construct: dc.l Variable_Name where 'Variable_Name' has been defined in the RS definitions at the start of the program. Then, in the code, I call InitPlayerKeys() ONLY when all of the relevant variables (the main table and both PlayerData structures) exist, and not before. Then, I create the required pointer by the simple expedient of: lea Table,a0 ;ptr to table ... move.l (a0)+,d0 ;get entry=variable offset add.l a6,d0 ;create true pointer since A6 now points to my main variables! C programmers are fond of using the above trick and its relatives to allow a static data table to exist that will be used to initialise a dynamically created structure, although I won't show the sort of C code used as it looks awful...anyway, you might like to use it in your own code somewhere. Yet To Implement ---------------- For now, the list of 'things to do' comprises:hyperspace (so far ig- nored completely), high scores (ignored again), allowing 1 or 2 players (it WILL come in time), attack wave handling (for testing I force you to go to Attack Wave 16 but not for much longer), tidying the game loop, allowing 'all time greatest' high scores to be saved to/loaded from disc (not a high prior- ity one, this), and sound. If anyone has any nice sounding raw samples (they should be OctaMED compatible) I can embed in the program, especially if they sound like the Defender originals, please send them & I'll include you in my list of credits in the source. Oh, and so far you don't see Baiters appear- ing. You will... Bugs Still Remaining -------------------- Watch what happens if a Lander takes a Body to the top of the screen. The scanner dots are left behind! BlitPreComp() only handles alien deaths, a Body death is handled elsewhere. I'll fix it later-if you can't wait, fix it yourself! I'm still getting the odd Mutant with bizarre Y coordinates. I don't know why, the code SHOULD stop this (but apparently doesn't always). You can tell if this is the case quite simply-the Mutant can't be shot while its Y coordinate is outside the normal range, and if the Y coordinates are way off the mark, stand by for Amiga FIREWORKS_MODE as the Copper Lists get seriously mutilated... That wraps up Update 02. Have fun with it, and as you all know and love: Live fast, code hard & die in a beautiful way Dave Edwards.