Dogfight

Graftgold Memories

The first game I ever programmed was a submarine game written in BASIC for the ZX80. I never finished it. I ran out of memory and got fed up of trying to shorten the program to get a bit of space for a few extra lines. I realised that the way to write games was in machine code.  Then the code would be a lot smaller and run much quicker. I had done a bit of ICL and IBM assembler at work so found Z80 easy to pick up. I liked the instruction set which had some really neat instructions such as the block move instruction LDIR.  The big problem was that at that time there was not room on the ZX80 for an assembler. An assembler converts human readable assembly code to a series of hexadecimal bytes that the computer needs. In particular jump and call instructions had to have the correct addresses to jump or call.  For example   a call to a subroutine in assembler would look something like CALL Sound but would convert to something like CD6AAA  where 6AAA was the memory address of the routine called Sound.  The trouble was that as you change the program all the memory locations change and you would be forever calculating the new addresses and one mistake could be disasterous for the program execution.  So one of the first programs I wrote was a routine that looked at the BASIC program in the machine to process code written as a series of REM statements. I would for example write   REM CD@SOND.   The @ symbol signified that a label followed. Rather like an assembler does my Hex Autoloader kept a list of all the references to labels and a list of all the labels and their address.   Then it fixed up gaps that the loaded left in the machine code with the correct relative or absolute addresses. I did consider writing an assembler and did draw up some little lookup tables but was more interested in writing games. The Z80 instruction set  had a lot of instructions and I knew it would take ages to get right.

  The first version of the HexAutoloader was written in BASIC and the first program I wrote with it was an assembler version of itself. Then I could discard the original and use the assembler version to maintain itself. That always amused me, a program that was used to write itself. I love that idea and would love to try and write and extend that idea to write a whole system booting up from a small set of routines that are then used to extend themselves.

The HexAutoloader was extremely quick. I did advertise it in one of the computing magazines in a tiny ad and sold 2 copies. That was my only attempt at self publishing.  I used the program for all games up to Quazatron. For Avalon I also used a disassembler..  Another amusing thing I disassembled the disassembler with itself so that I could change it to access by label list and so give me a disassembly listing of my program with my labels in the source code. That made it much more readable and easy to check against my handwritten source code.  I used a tape recorder to save the early games and kept a rotation of 5 generations of tape in case of a bad save. I tried microdrives but found them totally unreliable so sent them back. I bought a floppy disk system called Beta Drive. I hacked into its machine code so it integrated with my printer , HexAutloader  and disassembler. In those days nothing seemed to work with anything else unless you tweaked the code. It was a real change saving and loading using the disk.  It was important to get a quick turnaround time for a test run as they could easily crash, requiring a fix and a rerun.  The other addition I made to my Spectrum was a full size keyboard so I could type quicker. It had little stickers on the keys with the letters on which eventually started to wear off.  

The big issue with all this extra hardware was that it all had to piggy back at the back of the Spectrum. The keyboard actually had the Spectrum board installed inside of it which was pretty neat. The beta drive and printer connecters were stacked onto the rear connector. I lost count of the times the Spectrum reset during typing due to a slight wobble of all the gubbins at the back. 


I tried using an assembler for Quazatron , probably the Picturesque assembler. I could assemble and link several modules together. I need about 8 modules  as the Assembler took up so much room in the Spectrum. Each new module had to be smaller as the previous modules object code had to be in the machine. It was limiting so I started to look around and decided that cross assembling was the answer. Rather than run the assembler on the Spectrum the idea was to assemble on another machine and the transfer the machine code to the Spectrum. Andrew saw an ad. for a system written on the Dragon32. We went to have a look but unfortunately never got there. We went to the address in London but did not find the company. Then I realised there were two roads with exactly the same name and we had walked for ages down the wrong one. Andrew needed to go home to feed his dog so we decided to call it a day.   Soon after that I decided the way to go was to get a couple of PC compatibles. These were proper computers with floppy disk drives monitors and full size keyboards. I had to solve the problem of how to download the machine code to the Spectrum and  C64. The C64 was easy, it had a parallel interface and is was just a matter or ordering the connecters and making up the lead. My Spectrum had a Centronics parallel interface  for the printer.  I knew the Z80 chipset and realised this would contain a standard parallel interface chip that could work in both directions. All I had to do was connect one pin and the device was switchable by sending a data value to its port. SO then I just made a parallel lead up. It used the PC's parallel printer port, We essentially "printed" the machine code to download it from the PC to the Spectrum. I tried to make the interface work the other way so I could upload from the Spectrum but could not get that working. The PC ports were wired up as input only.  We used the Avocet cross assembler for Z80 and 6502 and I was very reliable.  It contained a routine that outputted machine code as text rather than the actual bytes. They very nicely wrote us a version that output the actual hex bytes which was twice as fast to download to the target machines.  It was a big step up and a lot easier to work with. At this time we moved into a little office at the back of my house. We had proper desks and leather director style swivel chairs. 

When John Cummings and Dominic Robinson joined us we change to the PDS development system. This had its own interface. This was a system that ran on the PC that had a cross debugger as well as an assembler. This was a huge improvement. Before this getting code to work was a matter of looking at the source to try to spot the error. Now we could step through the program and monitor memory easily.  We had problems with the system at first as we used floppy disks. It was meant to work with floppies but it had bugs as the testing had been done using hard drives. The code should have been drive generic if they were using the operating system properly.  We had a few heated phone calls. We needed the system to work to complete Flying Shark on schedule. I was withholding full payment until we had a system that worked properly. We got the essential fixes we needed and Managed to complete Flying Shark in 6 weeks. That was the final dev. system we had for the 8 bits. We used it in conjunction with those cartridges that were mostly used to pirate games or put pokes here and there to cheat. PDS needed a little boot program loading on the target machine. With this on the cartridge we could reboot the C64 and load the boot routine, then download the code for a test run really quickly. I used a cartridge on the Spectrum for situations like program freezes that usually indicated an infinite loop. with a press of a button I could see what the program was up to. The PDS system had the peculiarity of having a limit of 64k for each source module. So it allowed you to have 8  64 k modules. It was frustrating having this memory constraint. It was due to the 8 bit nature of the earliest  PC's. You could address memory blocks of larger than 64k if you used a base register and an address register, The programmer said he did not use this addressing mode to make it quick.

Today I use Microsoft Visual 2017. It is really annoying not being able to look at memory as the machine is running. US programmers do not seem to have used systems that employ a little routine in the interupt routine of the target program. This communicates with the debugger and allows you to do lots of things while the program is running.  It is an improvement having assembler, editor and debugger all in one integrated package but in many ways the system is overcomplicated. I get really annoyed when I cannot see my typing because it tried to put popups write over where I am working. 
My other big beef is that loads of times it doesn't print where my cursor is. It seems to pick a random place to insert the code I have typed. It could be my laptop doing it. I use a touchpad and there is a strange delay after typing a key. It ignores pad movement and buttons for a while so you think you have clicked to place the cursor but it ignores it, so the cursor is wherever it was last.  So if you are cut and pasting bit of code at speed it can all go horribly wrong. The debugger is brilliant when it works. Unfortunately it goes wrong almost every run. Its various tricks are 
Telling me code is stale when it is not. It thinks I have changed the code so will not debug it.
Telling me I have changed a structure when I have not. This occurs  when I make a harmless change such as changing a number. 
Refusing to stop at a breakpoint I have placed, cant work out if it is running that version or not.
Not letting me put a breakpoint.
Appearing to accept a change to a #define value, says it has compiled the change and then completely ignores it.
All these things mean you never can be sure of what is actually going on till you stop, recompile and start again, not very helpful when it takes ages to get to the bit to issue to debug.  I do things like habitually tidy code. Changing whitespace should not affect debugging ever. Still it is free and I could nit do without it.

Deepest Blue.

Andrew came round for dinner and asked if I had used the rumble on the joypads. For some reason I had not thought about that so thought that would be a good thing to add at this stage. I wrote a rumble driver that worked a bit like an old fashioned sound routine. You can assign a frequency and a time and a change in frequency to either of the two rumblers.  That gives a good range of effects from a simple bit of code. 


To allow player to test the joypad or keyboard mappings and settings I decided to add a test button that took the game to a test scenario where you can dogfight with a Seiddab fleet. This was fairly easy to add using the main scenario code. I just had to take care to isolate the test sector from the real game sectors. On my first attempts the Seiddab tried to fly from the real scenario to the test one. The real scenario gets processed in the background when you are in any of the "system" screens. So it is running two things at once whine in the test screen. I also had a test button in the ship build screen so I fixed that to call the same test scenario. Then a player can select a ship and try it out before they buy.

Once I had the test scenario up and running I tried the various control settings to see what worked. I found dogfighting extremely hard on the joypad so went back to the sensitivity algorithm. For the analogue controls joypads generate a linear response to how much the joystick is pushed.  That tends to give too much sensitivity when pushed a little bit and not enough range. So often the square of the output is used. A plot of x squared starts of gradually and gets steeper. I wanted to give a range of sensitivities so the player could tune the pad output to there own preferred sensitivity. That meant I needed a ranges of sensitivities.  I tried to model an exponential curve using 1/x translated and scaled to give the curve I wanted but after several attempts gave up the idea. Instead I used a much simpler method of giving a weighted  intermediate value between a linear curve and a cubic curve.

Using the test screen showed several deficiencies in the AI fighting routines. The enemy just sat there while I attacked. The attacked craft would wake up and flee or fight but the rest of the fleet just sat there like dummies.  I was using the collision system to also detect near threats but this was not working properly. Each cycle it was meant to check a random direction . some directions were being ignored. In a large fleet only the nearest ships to the attacker responded. I adjusted this and also added a mechanism where an attacked ship effectively sends a message to any ships nearby. Now  when I entered the test scenario they did not wait to be attacked. Some of the fighters started to stalk me straight away. I now had to quickly start moving and get into combat mode before being shot down in flames.

The Ai system uses a combination of weighted chances to movement patterns similar to the system I used in Avalon. The weightings change when the ai actor is frightened, angry, confident etc. Thus a frightened ai ship tends to choose actions that keep them at a distance and will try furtive sneak attacks. Bold ai ships tend to fly straight at their enemy. Each of the actions then has a state system that takes it through a set procedure. For example if attack is chosen the ai ship chooses an approach direction navigates out of range to the approach then makes an attack run. During the attack run their is a chance of breaking off depending on the Ai properties and the actions of the player. Quite naturally the ai does not like being in the players gunsights. My aim was to create ai that was indistinguishable from human players.  As I tweaked the Ai getting rid of any bugs the became much better at fighting than I was. That is good, it is easier to dumb down  Ai than to increase it.  I let the Ai players have a range of experience like live players and this affects the rate of their decisions. Rookie pilots tend to fly in the same direction for longer and that makes them much easier to deal with. The experienced pilots dodge and weave more and are quicker to press home an attack if they get an opportunity.

There comes a time in every game where you find you are playing it for more time than you are programming it. Irritatingly I was finding the test scenario more fun than the main game. That is easily changed , I need to bring the action nearer to the start of the game. Also it was time to get the players defence systems sorted out. I had coded shields but there was no HUD display for them. I had front,back left right shields for each ships and needed controls to switch them on, adjust the shield power to any desired direction, or equalise the shields. It had to show the shield strength in each direction. I opted for a display similar to that used in Simulcra where the shield is represented in several layers that change colour as they get depleted.  When I had this all working it showed
that the shield and poser distribution system were not quite working with each other correctly. The shields should not be drawing power when off.   I fixed this and tweaked the shield charging routine so it took a suitable amount of time and power.

To match the 4 way shield display I decided to change the existing gun bank controls. These were distributed around the screen in the gunbank direction. I thought they would work better as a cluster that was the same size as the shield control. I wanted the player to be able to select any combination of gunbanks and assign them to a target or to switch them to AI mode. This was needed for big ships with multiple turrets and would allow the ship to defend against multiple attackers. Also I needed buttons to select guns or missiles.
Two Seiddab attack in formation. New shield display near bottom right



To help with dogfighting I thought I would try a gunsight that adjusts with the range and speed of the target. The player has to fly so the gunsight is over the enemy. Then the guns point ahead of the enemy so the bullets will collide with the enemies future position assuming they do not change direction.

This has lead me to targeting. At the moment the player has to click on an enemy ship to target it, extremely hard during a dogfight. I am going to try an auto lock on system whereby the radar will constantly seek for targets in range and in front of the ship. I need to work out the rules when to switch off the target, If the enemy is not in sight the gunsight appears to move in a strange manner as the enemy ship changes direction. The sight needs to switch off when the target is not visible. It would be irritating if the system changes target automatically while the player is chasing a target, It pays to fight 1 ship at a time to wear its shields down.
I had a go at designing a whizzy HUD gunsight using Blender. I have not touched Blender in years so had completely forgotten simple things like how to move the camera. I followed a YouTube tutorial and managed to model a gunsight with a radial dial to show closing distance. It looked fine in the object viewer but became really weird when I rendered it. Perhaps I drew it at too small a scale, I intend to try again. I went through a couple of starter trainers for Blender to relearn the basic controls to make it easier.
Destroyer hunts an enemy freighter 

One of the big TODOs is to get the joypad working with the console controls. There are just too many controls to give them all a button but my aim is to get it so the game plays just using the joypad. One idea is to use one of the joysticks to control the cursor Ai it plays in a similar way to using a mouse/touchpad.


Programming Tips.

Player controls.

In addition to various direct controls such as keys, mouse buttons, joypad buttons there are a host of indirect or virtual controls represented by things on the screen like buttons, sliders, checkboxes etc.
When I package up the user input I also add data if the focused control has been actioned. The good thing is that only one control at a time can be used, however it may need a few bytes of data. I disassociate the action of the control from the routine that services the action by using the package of input data rather like an event.   This enforces separation between the code supervising the control process and the routine that responds to the control. This practice keeps each encapsulated and make you think about the interface between them. The real big advantage comes when you want to network the game. The players virtual action being packaged with the direct controls can be sent to a receiving machine to duplicate the players action on another machine.   Each of my screen controls that directly affects the game has an action number. I also have a couple of data values that I send with the controls action number. For a simple toggle switch one data value is used and just signals, switched on or switched off. A simple press button does not need any data its action number is sufficient. A slider would send a number representing the fraction of the length of the slider where the control is.

Sometimes the objects in the game screen itself can act as controls. For example suppose the player targets an enemy ship by clicking on it and that affects the targets behaviour, the action needs to take place just like a player clicking on a button. Again the principal stands that only one control is ever "in focus" at a time so only one control has to be actioned at any time.

Comments

  1. "I use a touchpad and there is a strange delay after typing a key."

    Check touchpad settings. Some of the touchpads are blocked for a few seconds after the last key has been pressed, so it won't register random touches. It's actually more annoying than helpful.

    Breakpoints might be put somewhere else because the code is optimised. I end up compiling non-debug (as then it is dramatically slow) and putting #pragma optimize("", off) in each source file I need to debug. This also allows to check the variables.

    MSVS is sometimes a mess but it's still much better or convenient at times than other options. A while ago I decided to make a Dragontorc remake/tribute in vala. Using vim and gdb. Vala itself is a cross between C and C# (looks sort of like C# but compiles to C). Dropped that to use C++. Sadly, I dropped Dragontorc remake and started to work on something else. There are still bits of code that I wrote for that game. Thankfully porting from vala to C++ was easy.

    I have dozens of tools to inspect what NPCs are doing etc. Putting breakpoints in a multithreaded code is sometimes.. um.. fun? ;) Oh, this is the other post.

    I kind of thought that the games on ZX Spectrum (and C64) were created on those computers and only many years later I learned that it was far from the truth. I would never thought about such a setup. Is it easier now? There are many more things happening in the game to just inspect memory all the time. For example, some of the npcs that I have, just the gameplay logic, may take up an amount of memory that would be half of what was available back then. Or more. It's a bit easier to communicate with consoles and other devices, though.

    I am now working on a VR game and being able to use commercial headset for development makes it much easier... and cheaper. And you know that if it works on your device, it works on other people's too.

    But sometimes I think that if I would do the game the old way, with the memory tigthly arranged, dropping dynamic memory management altogether (well, keeping it just for assets) and working round the limitations, maybe my life would be much simpler... If I got past "why can't I just add a new instance using dynamic memory allocation".

    ReplyDelete

Post a Comment

Popular Posts