Part 22 Building a shader editor
Graftgold Memories.
Spectrum Music.
Making sound on the original Spectrum was a challenge because it had no sound chip just a port which you could set to 1 or 0 to make a faint click. Sound is made up of waves so to make a sound you had to switch the sound port on and off in a regular pattern. For example setting the port to 0 for a while then 1 for a while would produce a square wave. The pitch if the note is related to the time interval of the cycle of on/off. Here lies the big problem for a sound programmer. A musical note mid keyboard is in the area of 512 cycles per second, about 2 milliseconds. To produce a tone you need quite a few cycles so the pattern is established. In computer terms that is a huge timescale.
A smooth game needs to run at least 15 frames a second. If the CPU is used just to make a note it is not doing any of the vital graphic processing let alone the game processing. With no graphic chips the poor old CPU had to do everything itself.
There are a couple of ways to solve this problem. The first just gives the sound routine a certain time to make a note or sound effect each game cycle. So instead of a steady note a series of short notes is produced. Remarkably the ear seems to paste all these notelets together and a continuous note is heard.
A second really clever way used for the first time I believe in Technician Ted was do to something else useful while the sound routine marks time whizzing round a counter loop. Instead of just looping it can do simple repetitive tasks at the same time. The sort of thing I mean is clearing the screen or copying a back buffer to the screen.
Getting Better Sound.
Just switching the port on or off produces a beep which is pretty uninteresting. If you change the proportion of 1s to 0s you can change the square wave to alter the harmonics the ear hears. The volume also changes slightly being loudest when there was equal length of 1s to 0s. The other thing easiest to change was the frequency by just slightly altering the loop length.
More Than One Note At once.
On the face of it the beeper could only produce one note at once but I figured if you superimpose two notes on top of each other and did a logical operation you could simulate two notes.
For example An octave harmonic.
Note 1. 0000000000111111111100000000001111111111
Note 2. 0000011111000001111100000111110000011111
Result 0000011111111111111100000111111111111111
I did this in the Avalon music, devoting the CPU to the music and having a loop that counted down two different times simultaneously. I then flipped the sound port value whenever only one of the counters reached zero. I had to be careful not to change the time the code took when a counter reached zero so made each path through the code the same number of machine ticks (T states).
I think I was the first programmer to get the beeper to play proper harmonies. I found the routine sounded different on newer Spectrums , I didn't find out why, and some of the harmonies were distorted. I used the same routine for later games but changed the two notes to be almost the same note. This gave a thick synthesizer sound were the square wave repeatedly moved in and out of phase. This was my trademark sound on the Spectrum used for all my later games.
Sound Effects.
I was not a big fan of beepy in game music so was not prepared to lose vital CPU time to put low quality music in the game. However I considered sound effects vital, they really enhance game events and make a game come to life. My SFX routine contained a simple loop but was driven by a data table. It had a start wavelength value and a change in wavelength value so each time the routine was called the note changed up or down. It had a cycle count which said how many times to change the note. It also had a type value which said what to do when the cycle count reached zero. It could reverse the sweep or reset the sweep by changing the start note. Finally there was the number of times to repeat the sweep.
These options allowed me to get all sorts of beeps, whoops, warbles etc. Their was also an option to use a random value by taking data from the ROM and applying the low bit to the port. Some ROM
areas contained data tables and gave interesting effects. I would spend ages changing the start point with a monitor while the sound routine was running "tuning" up the sound effects by altering the start point for the ROM or the various data values. We found that accidentally allowing the wavelengths to wrap (ie adding 2000 to E001 would give 0001) gave really interesting effects. The droid voices chattering in Paradroid are an excellent example of this effect.
I sometimes used to amplify my Spectrum sound through a 2 X 12 speaker cabinet (with no amplification) as the beeper sound was very thin and had no bass. The lasers and explosions in 3d Star Wars were superb through the guitar cabinet. I was very disappointed at the marks for sound in the Crash review, if only they had listened on headphones or through a proper speaker.
Creating Tunes.
I played guitar in a rock band and used my guitar to make up most of the tunes that I composed for games. As The Spectrum was best at playing a single note at once I used to put maximum variation of tone by putting in lots of arpeggios across several octaves rather like "tapping" with both hands on a guitar string. The Ranarama theme tune is a prime example of this. I would play a bit on guitar then code the notes into hex codes. These were used as an index to a wavelength table. I just used a crib sheet of note to code values to convert music into data codes. I would then invent the next section and code it in. As music repeats quite a lot I coded each section of the music then coded data that said which sections to play. This saved vital space on the Spectrum. I didn't usually try the code until I had completed the tune. I would then try it out and there would be many mistakes. Also some bits would sound weak on the Spectrum so I would play about with the notes to add interest. A tune would take me a few hours but I would revise the tune over several days to improve it.It was interesting trying to recreate some of the Spectrum tunes on guitar. When I wrote them I never played them end to end just in bits. Then I added notes to make them sound more interesting. This made them the musical equivalent of tongue twisters on guitar and it was fun and challenging learning them as guitar pieces.
Deepest Blue Progress.
Last month I managed to get mega bases appearing in the game but the collisions were not working properly and guns on the base were not working. I debugged the collision system and found some inaccuracies for large objects. I changed the algorithm that imagines a line joining the centres of two objects and loops through their parts to decide whether any part sticks out enough along the line to touch the other object. At the moment I am treating spaceship parts as deformed spheres which can be stretched into lozenges and/or squashed into elliptical cross sections just by having different XYZ max and min sizes.
The next step was to get the guns working. I realised that my power system did not cater for mega structures which were like several ships bolted together. The power was totalled at ship level so I revised it to sum the "ship" power so it would work for a mega base. Hmm what about mega ships, I will have to add those one day but first things first. I tinkered with the power system until the bases had power. I managed to get the turrets turning towards me as I flew by but they refused to fire. I found my friend or foe system was going a bit wrong so started to fix that. I then was ill for a couple of weeks feeling to muddle headed to code safely. You get to the stage in a game where you do not want to muck things up.
Instead I started playing around with shader effects. The online versions of Shader editors all seem while I to be OpenGL based so if I get an effect I like I have to recode it for Direct X shader language. It is pretty similar but annoyingly different in the names of standard calls. After a while of doing this I thought it would be useful to have my own shader editor as part of my graphics editing utility. I wanted to be able to choose a game graphic and then choose a shader to apply to its drawing. I wanted to be able to change the shader code recompile it and see the effect of the change. I already had code to try a shader out and to save out compiled shaders and data tables to I port into the game.
So I added a giant text box to be able to display the text of a shader and added a few buttons to allow me to save or load or compile a shader. I decide against compiling while I type (as some of the online editors do) as I had frozen my graphic card many times while halfway editing a line. I was amazed at how few lines of coding it took to achieve this. I already had coded a multi line textbox control which had basic word processing functions. I had only used this for single line text entry so although tested for multi line when written had a few bugs when things had changed. These were soon fixed and I was tempted to add standard functions such as cut and paste, undo, redo but in the end decided it was unnecessary as large changes could be typed in visual Studio while the editor was running, then swiftly reloaded. The important bit for the online editor was to be able to make small tweaks to numbers or formula and quickly see the effect. Shaders are a devil to debug; I tend to build them up very slowly bit by bit so I know which line is causing a problem. Debugging shaders is a bit like alchemy. The code is run in parallel for lots of pixels and you may be interested in one pixel that goes wrong out of a million on the screen. One of the methods I use is to output intermediate variable values as pixel colours so I can check their range for all pixels at once. I have toyed with creating a shader version of ABMON the online monitor Andrew devised for examining and changing bytes while a game was running but that will have to wait till I am ill again and want another diversion.
Just for fun I created huge asteroid field so I could soak test the collision system. They worked fine in the game and were terrific fun to blast and shunt around but their generation took ages across all the hundreds of sectors. I reduced them down to a more sensible size for now and will look into the cause for the generation delay later. It is often telling to scale up numbers of some part of a game to check the limits. I plan to do time testing systematically when the game is in place. It is fruitless to waste time optimising things that are not going to cause a problem. If need be I will only create the detail of things like asteroid fields for viewed sectors. I often put in two levels of coding for game action. The first for things in view processed at full frame rate and the second for areas not in view where things can be updated in a much coarser manner, say once a second.
So I started systematically tracing the boot strap process. My revising of the bases had disturbed the mechanism or bases using and creating resources. I changed the system to promote the resource stock holding to the mega base level. The sub base parts then added or subtracted from the shared stocks. Pretty soon I had a shipbuilder base building a scout ship. The next step was for the ship to leave the shipyard and to proceed to the base that had ordered the build. This was a pretty recent routine that used the multi level network to find a path across sectors and systems if necessary. Needless to say this too had a few issues mostly due to the recent revision of the way I structured the networks. I test routines after writing them but rarely devise test beds for large scale system testing. I find that best to do in the game itself and for that I needed several supporting systems each written and installed. It is an exciting stage when all the systems start talking to each other as progress happens quickly as long as there are no major design defects.
My scout ship left the shipyard, flew to a safe distance and then stopped. The pathfinder routine could not find a route to the home base which belonged to a different faction. That was because factions do not know each others sections unless they ally and swap data. That was a routine I had forgotten about so was not yet written.
Programming Tips.
A little while back I saw a tweet from a developer who had a dog in a game that attacked sideways and wondered how to fix it. This has inspired me to write a bit on the movement of game sprites in early games and how the methods developed.Game objects moved very simply in the earliest games. The Player sprite usually only moved in a single dimension. Think of the tank in Space Invaders or the bat in Pong. This was because the original games were operated by a few buttons. The movement was just a fixed speed either left or right when the appropriate button was pressed. The lack of acceleration or inertia while unrealistic gives a very immediate control and is very playable. Sometimes simple is best. Simple movement like this can be extremely addictive as it is predictable and is easy to learn.
Another dimension was quickly added with the invention of the joystick. Game playfields were almost all in two dimensions, X and Y, or left/right and up/down. Early games just had fixed speeds for each dimension. Thus if you put the joystick diagonally the sprite would move X+Y along the diagonal. The sprite would move faster diagonally ( for those maths inclined the square root of X squared + Y Squared). The envelope of the range of a single move would be a square shape, assuming the speed of the X move was the same as the Y move. In the real world the envelope should be circular, speed is the same whatever direction you travel. So for these old primitive games the sprite appears to speed up when in the diagonal direction. Most games used this system in the early days. Later acceleration was added so movement starts off slowly and gains speed to a maximum.
Asteroids had a rotating spaceship moving in two dimensions and went a step further.
Its movement is based on vector arithmetic and is the model you can also use in a 3D game for an object such as the dog walking about on a 2D surface. The idea is that instead of having data as to the X and Y speed of an object you store it as a polar vector, that is an amount (speed) and an angle (the direction it is moving). If you want to allow the direction of moving to be different from the facing direction (move,back, sidestep etc) you also need a facing direction. For Asteroids the ship can only accelerate in the direction it faces. You have to reverse the facing direction and apply thrust to decelerate. Something like a car or a dog would accelerate and decelerate in the facing direction in less skidding.
Fig 1 The components of a polar vector (diagonal line) The radius represents the magnitude. |
The first Graftgold game to use polar vector movement was Morpheus by Andrew Braybrook. I thought it was over the top till I saw the results. Things moved in graceful curves and had a very natural look.
In Asteroids the left right buttons change the facing direction . The move direction is worked out using vector addition. The easiest way to do this is to resolve each vector into its X and Y components, add the Xs, add the Ys then work out the angle for a triangle with sides total X and total Y. I have used a look up table to do this in many games. Here is the way I do this if you are maths savvy...
To get the index to use in the lookup I convert X and Y to positive then calculate Y/ (X+Y). This gives an answer in the range of 0 to 1 which can be scaled to match the number of data items in the look up table. The look up table is built thus:
for (i = 0; i<(DIAMOND_DATA_SIZE) ; i++)
{ /* step along x from 0 to 1 while x+Y = 1 */
X = (i * DIAMOND_STEP);
Y = 1.0-X;
Value = X/(sqrt((X*X)+(Y*Y))); /* project to circle */
Angle = (acos(Value)/ (2.0 *PI));/* convert to 0-1 */
*Diamond = (FP)Angle;
Diamond++;
}
Fig 2 Diamond and circle |
The idea behind it is to convert steps along a diagonal line that stretches from x=0,y=1 to x=1,y=0,
to steps along the first quadrant of a circle then for each step find the angle. Then we can look up using Y/Y+X . The division by Y+X normalises Y to our X+Y=1 formula. It keeps the data table compact. If you used Y/X for example the index would be massive when X is very small. I call the table the diamond table as if you consider all the quadrants the diagonal lines draw a diamond shape. After you have retrieved the angle you then consider the signs of the original X and Y to see which quadrant the angle is in, and adjust the angle an appropriate number of quarter turns.
I am proud of the diamond table method, I invented it from scratch and it has been used in many Graftgold games.
In many cases you only want to add acceleration/deceleration in the movement direction so you can just add to the magnitude leaving the angle untouched. Think of directing a person around the room or a car around a track. You could just say turn left turn right, go faster go slower. In this simple model turning does not affect speed and acceleration does not affect turning.
So getting back to the dog what it needed to do was move in the direction it faced. To home in on its target it should turn toward the target (not move toward the target). To home it has to work out whether to turn a bit to the left or the right. There are several ways to do this , you could just work out the angle to the target by using the X distance and Y distance and compare this angle to the dog facing angle. You then turn the dog by his turn speed left or right , then move him forwards.
That sort of homing routine can mean the homer never reaches its target as it cannot turn fast enough. This looks quite good for homing missiles as they pass and then make a huge turn and try again. If you want the homer to reach the target there are a couple of things that need adjusting. Think of parking a car in a bay. As you get near to your target you slow down but may need to turn more rapidly for a bit to get in the right direction. Getting the slowing right in a game can be frustrating, Slow too much and you stop before the target. I usually make the final speed a function of the distance. This can still mean you never reach as your speed slows down to next to nothing. You get very close but not quite there so you need to slightly skew your formula or widen your hit zone.
With floating point arithmetic people generally just use in built sine and cosine routines. Being old school I hate using radians; I do not want to have to keep on dividing or multiplying by Pi. I like an easy to use angle system where 1.0 = 1 revolution clockwise so have my own sine/cosine routine. In 8 bit 256 would represent 1 revolution. Thus an angle would range from 0 to 255. You could just add angles together or subtract them and they would just wrap round and you get the right answer. With floating point 1.0 means 1 rotation, you just do normal arithmetic then take the fractional part to combine angles. However in either system if you want the difference of two angles (say to decide whether to turn left or right) you need a signed answer ranging from almost -1 to almost +1 revolutions.
It still pays to try to eliminate the number of square roots used while manipulating vectors. Never work out two square roots to compare distances, just compare the squared amounts. The worst routine I ever saw was in an arcade game Graftgold converted to home computer formats. The original code wanted to calculate the minimum distance a car could be from a point. It went into a loop that calculated the actual distance by using a square root or the sum of squares , compared the answer to a constant. If the distance was too great it moved the car a bit and went round the loop again. Guess what, the car ended up at a distance of the constant so the complete loop with all it's square roots was completely unnecessary. We replaced it with moving the car to the required distance and it worked perfectly eliminating a very nasty CPU bound bit of code.
One of the complications using polar vectors is deciding the approach to moving backwards. Do you consider this as a vector with a negative magnitude, or do you reverse the angle? You certainly must not do both.
You can just store the vector as X,Y amounts rather than an angle and a magnitude or even as normalise X and Y components and a magnitude. I use a 3d vector X,Y,Z in my space game. I use the objects matrix to convert the vector to the object frame of reference, add forward/back side thrust, limit the speed then convert back to the world frame of reference.
Comments
Post a Comment