Introduction
When I first got started on the Amiga I had a lot to learn. It was 1989, and Graftgold`s first foray into the 16-bit world was already underway, and would become Simulcra. Dominic was writing a multi-tasking Operating System whilst also figuring out how to plot 3D objects. He hadn't bothered too much with a plot routine for sprite-type objects as he didn't need one.
My first routine that I wrote was actually a font plotter. It has less complexity than a sprite plotter because text always gets written totally on the screen (not half on and half off) so there's no need to code any clipping, you just check that the X and Y co-ordinates are in-bounds.
Your plot routines have got to be as efficient as possible. Any extraneous instructions in these will affect how many objects you can display in a game frame. When you`ve written a plot routine, even one that uses the blitter, you can see how many lines of assembler code it takes (about 800 in fact) to do what a hardware sprite does almost for free, and you start to appreciate how useful they are, and how gosh-darned clever the C64 graphics chip is.
I can`t remember how long it took us to write the first full plot routine, but it would have been a few days to get everything right. Just about every Amiga game that ever there was had to have one. Should have been a listing in a magazine!
I can`t remember how long it took us to write the first full plot routine, but it would have been a few days to get everything right. Just about every Amiga game that ever there was had to have one. Should have been a listing in a magazine!
Screen formats.
Writing the font-plotter familarised me with the layout of the screen RAM. I started with the Atari ST version, which had each 16 pixels horizontally interleaved into 4 consecutive words (16-bits X 4). This was slightly different than the Amiga could do. You could set up the Amiga bitplanes in completely different areas of video RAM, but then you`d need 4 or 5 pointers to each bitplane. Each bitplane provides one bit per pixel and are combined to work out an index into the palette, which gives each pixel its colour. We chose to interleave the bitplanes by raster line. Each bitplane started 40 bytes apart in one block of RAM. This would allow us to use the blitter to plot all the bitplanes in one operation. To facilitate this, the objects we plotted had to also have interleaved bitplane masks and graphics data.
For a slower blitter operation that used less data we would have to keep resetting the mask pointers for each bitplane and do the bitplanes one at a time. I`m getting ahead of myself though, as we started offf writing thr routines on the Atari ST, and converted those initially to the Amiga using just code, no blitter.
The graphics chip reads the bitplanes that have been used for the display from left to right and then down a line, and starting from bit zero, combines 1 bit from each bitplane to make a colour number for that pixel. The colour number indexes in to your defined palette of usually 16 or 32 colours. This indirection lookup can allow you to do some interesting effects as you can change every instance on the screen of a color just by changing one of the palette colours. Screen fading is really simple.
Once you`ve got the pixels to the screen using the CPU you can try getting to grips with the blitter. This will reduce the amount of code in the plot routine, and make the plotting operations a little bit faster. I do remember making a number of phone calls to Commodore technical support during this time as the blitter was a tricky beast to tame. You do need to make sure that your program checks that the blitter is finished its previous operation before you start to load its registers for the next operation. Thinking about it: 2 blitters would have been really nice, or 3?
The graphics chip reads the bitplanes that have been used for the display from left to right and then down a line, and starting from bit zero, combines 1 bit from each bitplane to make a colour number for that pixel. The colour number indexes in to your defined palette of usually 16 or 32 colours. This indirection lookup can allow you to do some interesting effects as you can change every instance on the screen of a color just by changing one of the palette colours. Screen fading is really simple.
Once you`ve got the pixels to the screen using the CPU you can try getting to grips with the blitter. This will reduce the amount of code in the plot routine, and make the plotting operations a little bit faster. I do remember making a number of phone calls to Commodore technical support during this time as the blitter was a tricky beast to tame. You do need to make sure that your program checks that the blitter is finished its previous operation before you start to load its registers for the next operation. Thinking about it: 2 blitters would have been really nice, or 3?
Graphics
Initially we used the Atari STs as graphics machines. We could save out an individual graphic as a binary file and then include that file, with some extra size information, into an assembled graphics file. That file would ultimately be loaded in at run-time. Later we wrote a sprite cutter that could take in sheets of graphics and generate the graphics and size information in different formats for the different platforms more easily.
We decided to restrict our graphics to being either 16 or 32 pixels wide. If we wanted anything larger we would plot the objects with multiple calls. This would be less efficient at run-time, but take less code. We didn't anticipate a lot of big graphics, we preferred to run more smaller objects.
We tended to use the top left co-ordinate of each object as its origin. Probably that came out of working with characters and the top-left is always where you start. It makes the plotting easier to think about, and the collision detection. Nowadays I would use the gravitational centre of the object as the origin as we tend to use more physics, and since we included pixel offsets for X and Y it didn`t really matter.
Quite often a plotted object wouldn't be exactly 16 by 16 pixels, or 32 by 32. We would always discount top and bottom blank rows to save space, and save time plotting. Since pixels were grouped together in 16s on the screen there wasn't any relevance to removing blank pixels horizontally.
The graphics format for a 16-bit wide graphics object then would be:
A header defining the depth of the object and an offset to the first plotted row, i.e. the number of blank lines to skip before the graphic starts, or back up using negative offsets. There would also be an X offset in case we wanted to slide the graphics frame sideways.
For each raster line then we`d have the data mask of 2 bytes. This would be the logical opposite of the 4 bitplanes ORed together. The save routine in the graphics editor could work that out for us.
For each bitplane then we`d have 2 bytes of graphics data.
The 32-pixel wide plotters needed a source graphic with 4 bytes of mask and 4 bytes of bitplane data per raster line.
The 32-pixel wide plotters needed a source graphic with 4 bytes of mask and 4 bytes of bitplane data per raster line.
Each game object usually had a number of frames for its animation. We'd keep all of those frames consecutive in memory for easy animation. The base frame would be the first one of the set within all of the game graphics. We were able to generate left-right reflected graphics for the side-on games at load-time, and for top-down games the direction an object faced would decide a secondary base number to get usually one of eight sets of graphics. Due to the lighting being directional, the graphics artists had to draw the object and re-light it for 8 directions. We didn't have any 3D software at that time, and even if we did it would likely be overkill for a game that only had 16 colours in which to render. The graphics artists` task was to make something look good in all 8 directions.
Starting Simply
When you`re first trying to write to the screen, you need to be aware that missing the screen and writing somewhere else could spell disaster for the program. It`s vital that you trace the program through one instruction at a time and check all the write destinations as closely as you can. Way back in the 8-bit days we didn`t have single-step tracing, so there were a lot of crashes. Fortunately a flick of the on/off switch got you back up and running in a second. Wonder why they stopped doing that?
A nice simple plot routine to start with is a single line of 16 pixels that are always aligned with the screen words. You start with screen X and Y co-ordinates of where you want to plot your line. Firstly we check that we are aiming to plot the line on the screen. To that end, we just check that our Y co-ordinate is between 0 and 255, and that the X co-ordinate is between 0 and 319. We will actually need to discard the last 4 bits of the X co-ordinate since they represent pixel positions that we won`t support with our aligned-to-the-word plotter. Our co-ordinates for these routines represents the top left of the line or other object we are plotting.
You need to know the format of the screen layout in order to get the correct position and colours from your plot routine. The Amiga screens were usually split into 4 bitplanes for a 16-colour system, and we arranged our bitplanes interleaved per line on the screen. What we want to work out then; is the distance into the screen memory that we have to offset by to get to the word that we want to alter.
Having decided that our line will go onto the screen, we use the formula:
Offset in bytes = (Y co-ord X bytes per line X no of bitplanes) + ((X co-ord / 16) X 2))
= (Y co-ordinate X 160) + (X co-ord / 16) X 2
The 16 / 2 is the number of pixels bundled together in a word (2 bytes) of data, since one pixel can move across 16 bit positions before skipping to the next word. It`s important to be word-aligned when writing out words of data, and we want to work in 16-bit lumps for efficiency. Writing out 32 bits at once only needs to be on a 16-bit boundary since we only really have a 16-bit data bus and it will take 2 CPU operations to write out 32 bits.
Since we used 40 bytes per line and 4 bitplanes, we were multiplying the Y co-ordinate by 160, which can be done by looking up the answer in an array of a 160 times table, or you might chose to multiply Y by 32, store that, then multiply by another 4 and add the two together, since 32 + 128 is 160. We don`t multiply by powers of 2 on the CPU either, you can simply shift the binary value to the left by as many powers of 2 as you need, i.e. 5 for 32 times. However... for a more versatile system there might be a multiply in there now.
If you know that your line is going to be solid and of one colour then you can write out 4 bitplanes of data from your calculated offset into the screen data, adding 40 bytes to get to the next bitplane. The colour you get depends on whether you write zeroes or ones to each particular bitplane. Usually you will have your predefined line colours set up in memory in advance, or you'll have drawn them in an art package and saved them out as binary data.
We wrote a custom "sprite-cutter" that could read in a sheet of graphics and output a series of graphic images in our custom format, with all the X and Y sizes in the format that we needed for the Amiga. It took in a script file that told it which graphic sheets to load, where the images were on the sheet and what sizes they were, and whether we wanted to add offsets to move the graphics off-centre (or maybe bring them on-centre if they had sticky-out bits on one side). The output was a binary file that contained all of the graphics for the game, trimmed to remove blank lines, and an assembler header file that defined all of the image numbers for the program to know which image was where. That made it easy for us to add images of animation to one object and whilst all of the later images moved, the assembler would know the new image numbers at the next build.
Plotting
Rather than start on the Amiga and immediately dive into the blitter to get it to plot the graphics, we started by doing the job in code, mainly because we already had an Atari ST routine that almost fit the bill. You can also check the code line by line in the debugger, whereas if you load the blitter with some bad parameter; it has the power to make a real mess of your video RAM. Start simple.
We already had Dragon 32 and ZX Spectrum plot routines, so we knew what the task was. Since 16 consecutive pixels on each bitplane on the screen are held in one 16-bit word, we have to slide the graphics along within the word to the correct position. This isn't too bad on the Amiga since it has 32-bit registers, we can clear one half of the register and load 16 zero bits into the other half, then rotate the graphic into position, conveniently with one instruction that takes 1-15 as a parameter in another register.
Continuing with the theme of initially sticking with graphics that are nicely aligned with the 16-bit words on the screen, and maybe your background tiles if you`ve used 16X16 blocks to make the background, let`s expand the above example to write out a 16X16 block of colour.
Using the above procedure to check that the X co-ordinate is on the screen, we have 2 additional cases to check against the Y co-ordinate. If the Y co-ordinate is less than 16 pixels from the bottom, we are going to have to stop plotting early, that`s all. Secondly, if the Y co-ordinate is less than 0 but more than -16, we will have to start at the beginning of the screen and again curtail the plotting early. For just plotting a solid block of colour we don`t have to adjust where we start within a more complex graphic.
After having worked out where we are going to start plotting on the screen, as well as an inner loop of going through the 4 (or however many) bitplanes we are using, we then advance the output pointer by another bitplane to get to the next Y line on the screen and have an outer loop to count however many lines we are plotting, either the full 16, or less if we have crossed the top or bottom of the screen.
In reality our graphics can all be different depths and widths, and we have to deal with that efficiently. The graphic data will contain a header telling us for each frame if we can skip any leading blank lines, and exactly how many lines deep we have to plot.
Masking
The simple routine described above just plots a 16 X 16 pixel solid block of one colour. That`s not especially useful. Usually we want an odd-shaped graphic to glide beautifully over the background not disturbing the background around it. To that end we have to do what we call "masking", that is, we cut a shape out of the background picture so that we can apply our object onto the background in whatever shape we need. Our sprite cutter utility was able to work out the mask, which you do by logically ORing all of the 4 or more bitplane words together and reversing the result. This gives you binary 1s where you don`t want to plot a colour, and 0s where you have some colour pixels. Actually, when we came to using the blitter to plot the objects we had to get the sprite cutter to not invert the mask as the blitter wanted to invert it for us. That had us confused for a while because reversing the mask caused black rectangles to appear with cut-outs of odd-ball colours in the shapes of our objects.
Actually our sprite cutter went one step further because it needs to know the difference between a transparent pixel and a real pixel of colour zero. Typically we set colour zero to black, and colour 15 to white, and arranged the other colours in groups by brightness. Usually I also wanted to do something a bit clever with a couple of the other colours, such as glowing effects or a flashing light. We could therefore nominate in the sprite cutter script for each sprite which colour of the 16 was the transparent colour.
Most of our graphics then, for each line of graphic had a mask word or long, then 4 or more bitplanes of colour data, again in words or longs. To plot your object then you have to AND the mask against the background in all the bitplanes, to remove the colour from where you want to plot, and then OR your new graphic onto the background which sets the colour selections in the bitplanes.
Shifting to the right
Once we decide we want individual X pixel positioning, we need to be able to shift the graphics along the words of the screen bitplanes. You would have to read the mask and graphics into registers on the CPU and rotate them into the required positons before applying them to the screen area. Now for speed some people might have done pre-shifted graphics of small objects. If you draw a small bullet in 16 different X pixel positions in order, then you could decide which image to display based on the low half-byte (nybble) of the X co-ordinate low byte. For the zero position we`d be plotting a 16-pixel wide graphic, and for the others we`d be plotting 32-pixels wide. We didn`t ever do that, but if you wanted a lot of the same small graphics on the screen it would save a bit of time.
Clipping
Now things start to get more complicated. Unless we are going to ensure that our graphics don`t leave the bounds of the screen, we`re going to need to also control plotting part of our graphics near the edges of the screen. Plenty of games don`t need to do what we call "clipping", but scrolling games generally do as objects can arrive and leave by the edges.
If your object arrives from the top of the screen; you just have to skip over the missing top of the graphic and start at the right point. If your object arrives from the right, you have to plot the left of your object and not the right.
Additionally you have to be able to deal with the object arriving from the top right corner, so we have to skip some of the graphic AND only plot the left hand side. There are 8 such cases for the four sides. There`s also three other cases for our Amiga scrolling system: as the screen scrolls downwards we restart the screen memory as it`s like a rotating barrel - see the previous blog page for more details. If a graphic needs to cross the boundary we have to plot the top of the graphic at the bottom of the screen, and the bottom of the graphic back at the top! The screen display system will stitch it all back together. Again, our object might be crossing the left or right edges too.
Additionally you have to be able to deal with the object arriving from the top right corner, so we have to skip some of the graphic AND only plot the left hand side. There are 8 such cases for the four sides. There`s also three other cases for our Amiga scrolling system: as the screen scrolls downwards we restart the screen memory as it`s like a rotating barrel - see the previous blog page for more details. If a graphic needs to cross the boundary we have to plot the top of the graphic at the bottom of the screen, and the bottom of the graphic back at the top! The screen display system will stitch it all back together. Again, our object might be crossing the left or right edges too.
Restoration
As we plot graphics on the screen; we have to record the screen memory locations which we have altered so that we can patch the screen up ready for its next display. Whether we have 2 or 3 screen display buffers, we have to keep a restoration list for each. The plot routines have calculated the screen locations that they are altering, so it`s logical that we just record the address in memory, along with the width and the height that we have altered.
Once again, the barrel screen can cause two restoration buffers to be required for one object. There wasn`t a need for a linked list to keep these in, a simple array was all we needed.
The restoration system could again be given to the blitter as it is just copying rectangles from the pristine buffer to the unseen buffer. Some might overlap, but working that out would take longer than just doing overlapping areas more than once. We don`t have time to copy the entire pristine buffer over. The graphics on any one frame might only cover 15% of the screen area if you arranged them all neatly, so we have reduced the task to 15% of what it might be.
Variations
The way that you organise your colours in the palette can help you to do some tricks with the plot routines. Arcade machines had shown us that a palette change from a normal set of colours to, say, an orange-tinted palette can show you when an enemy is damaged, or has been hit. They could do that by just setting the sprite to use a second tinted palette.
Just as choosing your palette in the first place is fundamental to what you`ll be able to draw, the sequence of those colours is also important. Separating the bitplanes out means that you can selectively write to one or some of them, or alter one of them to change the colours that are written out.
I tried to let the graphics artists choose the actual palette colours, but always directed them to arrange the colours neatly rather than arbitrarily. This allowed me to do some graphics trickery in the plot routines.
Shadows
The crude way to do shadows is to plot a black shape of the sprite under the real sprite offset by a few pixels down and to the right, or opposite wherever you`ve put the light-source in your object images. If you plot this every alternate frame, and nothing at all in the frames between, it allows the background colours to show through but they appear darkened. Flickering at 25 frames per second isn`t too bad. By setting black as colour zero in your palette you can plot in colour zero really quickly because all you have to do is apply the mask to the background screen and there`s no data to add. Job done.
If you get your colours all organised in pairs so that the lower one is darker than the higher one all along the palette you can remove the data in the lowest numbered bitplane and leave the others alone and get a darker colour. You can then plot this every frame as it`s pretty quick to do, and your shadow doesn`t flicker any more. I used this in AGA Uridium 2. We had to avoid using colour 1 in the backgrounds as it would clear to the space colour when a shadow passed over it.
White
A common arcade trick is to flash an object, usually they would do it with a palette change, but we can do it by setting all of the bitplane data to the same values, making all pixels the top colour in the palette, which we have set to white. To do this with code we can use the mask data inverted as the graphic data, and we don`t need a mask at all as we are setting every bit of our graphic to 1 to get the top colour where any colour exists in the graphic. For the blitter we can use the already-inverted mask as the data into all bitplanes, it`s also faster than a normal plot as there`s only 2 sources, not 3.
Blue Ice
In a slightly more sophisticated manner to the white plotter, we arranged the colours for Fire and Ice into 4 blocks of 4 colours, again with white as the highest colour, and the blue shades under white, darkening as the colours go down. We could vary what the other groups of 4 colours were by level. To plot an object into just the top 4 blue to white range we plotted the first 2 bitplanes with the mask instead of the data to force the bits to be set, then plot the top 2 bitplanes with the real graphic data. Colours 0, 4, 8 and 12 become colour 12, colours 1, 5, 9 and 13 become 13, etc. This allowed me to have a frozen version of all the objects in any pose without having to draw them or store them.
Pixel Plotter
We also had a plot routine to plot a single pixel, called PlotSpot. It was quicker to plot the pixel in code than to set up the blitter. We had a shadow variant of that too. The pixel plotter was used for the particle weapons in Paradroid 90 and the rain and snow in the Fire and Ice storm clouds.
Priorities and Layers
One final thing about the plot routines is that it`s a good plan to be in control of the order in which you plot your objects on the screen. For a PC or console 3D game the sequence is decided initially by the camera position, and we`d draw things from distance to close. Using a Z buffer allows us to buck that as it holds a distance for the current pixel plotted at each position on the screen, and only pixels closer than the current one get plotted. This works until you start using partial transparency! Any screen overlays are added at the end with the Z buffer switched off.
For 2D games you can control the sequence of plotting a little better. It`s usually best to have the main player at the front all the time, unless you want the graphic to go behind a bit of scenery, which you plot on afterwards. In fact, in Paradroid 90, objects used the display priority as their height above the ground, which then decided the X and Y pixel offset to the shadow.
We implemented a system with 16 arbitrary layers, called display priorities. Each game object generally doesn`t need to change layers so it is defined at object setup time, though it`s fine to alter it in-flight, such as when a Paradroid 90 robot head falls to the ground. All the objects get updated in a largely arbitrary order, though we found it`s a good idea to do the player first as it sets the scroll position, which defines the screen plot position for everything else. As an object is updated, its screen position is calculated and we put it into one of 16 plot linked lists based on its given display priority.
Once we have all the objects updated we can go through the plot linked lists in order, lowest first, and plot the graphics on the screen. This completely controls the plot sequence as everything is single-threaded, more-or-less. Technically, since some plots are done with the blitter and some are done with the CPU we could get some data plotted out of sequence, but since the pre-amble in the plot routines has to do all the clipping checks and address calculations then the previous plotting is likely to be completed. You could put a WaitForBlitter check into the non-Blitter plot routines... I never noticed any issues.
Hardware Sprites in the Mix
The Amiga hardware sprites could be set to go between the playfields, or above them, or below them. They`re best used for one purpose rather than arbitrarily mixed in with the software plotted objects. I used them for the score overlays in Fire and Ice, and I would have used them for the Coyote except that I wanted him to go behind some scenery and in front of other scenery. That was one of those good ideas that got swapped out early on.
In Uridium 2 we had a better sprite multi-plexor and could use the hardware sprites for some 16 pixel wide objects. The plexor has limitations on how many objects you can plot in a line horizontally (likely 3, since I was using 1 for the background stars), so while hardware sprites are preferred for speed, there needs to be a backup plan to plot any objects in software that exceed the plexor`s capabilities. That`s going to be on a frame-by-frame basis.
For a hardware sprite we just had to copy the graphic to the sprite buffer and set the X and Y position. Note that as well as object display priorities, we also have to sort the hardware sprites into Y position sequence down the screen. If we keep all the objects that might be hardware sprites in one layer we can afford to do a specialised search and insert for that plot layer based on the Y position of the object, rather than always at the front of the list.
Conclusions
Don`t forget that with assembler you`re working at a very low level. Each instruction can only read or write 1, 2 or 4 consecutive bytes or combine a couple of numbers in various different ways. Writing a complete game is like building an aeroplane from component parts, possibly without a plan!
All this assembler code is so much easier than just setting an X and Y co-ordinate for a C64 sprite in a couple of chip registers. Sarcasm mode: cancel. Thanks, Kryten. The C64 graphics chip did a lot of display work for us almost for free. Only the arcade machines took it further with their custom chips.
All this assembler code is so much easier than just setting an X and Y co-ordinate for a C64 sprite in a couple of chip registers. Sarcasm mode: cancel. Thanks, Kryten. The C64 graphics chip did a lot of display work for us almost for free. Only the arcade machines took it further with their custom chips.
0 nhận xét:
Đăng nhận xét