Rick's b.log - 2021/12/13 |
|
It is the 24th of November 2024 You are 18.118.28.217, pleased to meet you! |
|
mailto:
blog -at- heyrick -dot- eu
The next stage, and the one that probably took the most time, was to have Lucy move in a more useful way. Up and down steps and ladders, falling, and of course interacting with the ghosts.
Essentially the game was built up by taking the basic "game loop" and adding things to it, like "today I'm going to add the headstones" or "today I'll sort out the teapots".
The modules are:
There's one more thing.
Generally, I'm quite pleased with how Mamie turned out, however there are two specific points worthy of note.
The first is the location tracking. It might be related to the use of larger tiles (it would likely be quite easy to track Lucy's position with 8×8 tiles) but the current code just seems overly complicated. It wasn't possible to change it because far too many parts of Lucy's movement worked with the setup as it was, and rewriting it all would be sliding down the snake back to square one.
The second? The level designs are actually simple text files that define what each tile is, and if said tile has any object on (almost every object assumes a standard floor tile). While this permitted easy and rapid creation of game levels, it is also limiting. For example, there is no way to have a spider above the top of a ladder.
Tomorrow, a look some of those little touches that help enhance Mamie.
Advent 2021 day 13
Mamie Fletcher's House 13
How Mamie was written
First, in BASIC
The original version, as I have already mentioned, was written in BASIC. The first thing to do was to create the look and feel of the game display. After that, to have basic left-right movement of the ghosts and Lucy.
Then, in C
By the time it came to the rewrite in C, I had a much better idea of how everything interacted. I broke the code into thirteen modules. That's "module" as in modular coding, not as in Relocatable Module.
The source modules.
Handles drawing stuff, both low level (plotting sprites and changing colours) and high level (drawing the top of the screen, tiling up the play area...).
There is some in-line assembly for the OS_SpriteOp calls, for things such as plotting a sprite with optional mask. I have recently added a SpriteOp call to DeskLib (my version) to plot sprites with transparency, but originally it wasn't available, so I figured nine lines of inline assembly was a lot less bother than rebuilding the entirety of DeskLib. The flash effect also uses in-line assembly to call SpriteOp for things like passing different colour tables. The code is in C, it's just the OS call part that is in assembler (it is always thus, it's just easier to hide when it's in a library!).
All of the specific colour setting (that text in cyan, for instance) is performed using ColourTrans, because the 256 colour modes are a horrible mess when it comes to sorting out colours. Rather than having the ability to specify a value to write into memory (this is what you see with the numbered colours in Paint's palette), it usually needs to be broken into colour and tint. It's fairly easy to set the colour, using either OS_SetColour
, or VDU 18, 8, colour
.
But setting the tint? Oh my... VDU 23, 17, 2, 192, 0, 0, 0, 0, 0, 0, 0
for tint 192 (the tint is the upper two bits of a byte, so the possible values are 0, 64, 128, or 192).
Much easier to just ask ColourTrans to sort out the mess.
There is a direct relationship between Paint palette colour and the colour/tint combination, as can be calculated as follows:
INPUT "Paint palette colour number: ";c%
col% = (c% >> 2 AND 1) + (((c% >> 4) AND 1) << 1) : REM Red
col% += (((c% >> 5) AND 3) << 2) : REM Green
col% += (((c% >> 3) AND 1) << 4) + (((c% >> 7) AND 1) << 5) : REM Blue
tnt% = ((c% AND 3) << 6) : REM Tint
PRINT "This is colour ";col%;", tint ";tnt%;"."
There is a proper paletted 256 colour mode on the RiscPC and later (as can be seen with the greyscale palette), but I don't think many people have ever used that. There is also a way to adjust the base colour palettes (the VIDC can actually support 4096 colours) but it's a bit gonzo and actually not terribly useful in 256 colour modes due to a lack of palette registers, so I think even fewer people have ever used that.
There are only 16 colour registers in the VIDC, and in 256 colour modes, the registers are assigned as follows:
The VIDC is weird! (from the VIDC datasheet, ISBN 1 85250 027 1)
For a look at all the varied colour methods (and this doesn't include the VIDC2 15bpp, 16bpp, or 24bpp stuff!), take a look at http://starfighter.acornarcade.com/convert/.
All the font plotting, including the function font_showtext() that will take a pointer to a block of text and will draw it to the screen wrapping when we reach the width of the display. The scenario, credits, and help texts are all output this way.
There's a fair amount of in-line assembly in here too, partly because it's more efficient to directly pass the font handle to the paint SWI (rather than making a call to select the font, and then another to paint the text). It might only amount to, normally, two calls saved per game redraw, but every little bit adds up. It's a lot more than just two SWIs saved when in the menus.
Other things, such as working out string widths and such, were just easier to pull the data from a register and drop it into a variable, as opposed to farting around with structs. Structs of information are useful if you want lots of info, but if there's only one thing to read, it's much simpler to just read that one thing...
Handles the primary game play loop, as well as ancillary functions such as loading in the maps and resetting between games, levels, or lives.
Anything to do with the ghosts that isn't an is-it-touching-Lucy (that's part of Lucy's code).
The ghosts run autonomously. Every screen redraw, a function is called to move the ghosts and draw them (if visible).
Code for reading and interpreting input from a joystick. Reading the joystick is performed digitally, that is to say that we check for the directional input to be less than -31 or greater than 31 (that's about halfway on an analogue stick). Digital sticks will reply with a full range setting. There's no speed control finesse in Mamie. Either Lucy is moving, or she isn't.
A problem with the current implementation is that the API uses an eight bit field for the button presses, however the typical SNES controller replies with a ten bit field. There are two bits unused in a place that would logically indicate support for a second shoulder button on each side (Playstation-like); the side effect of this being that the Select and Start buttons are not visible to Mamie, unless the joystick buttons have been remapped (the !Run does this by default so all of the eaight buttons on an SNES clone controller can be accessed). Work is underway to lift this limit.
The joystick button assignments are not reconfigurable. There's a fair amount of duplication (X dupes Up, Y dupes Down, while RightShoulder and A are for taking a photo, with LeftShoulder and B for reloading the camera. Essentially there are only six controls (up, down, left, right, photo, reload). The duplication is to give more options for how you want to use the controller. I, for instance, prefer to control the camera with the shoulder buttons, and control the direction using the direction toggles. The right side buttons (A,B,X,Y)? Unused. But you might prefer them to the shoulders, so... options are always good.
I have specifically chosen not to provide any joystick reconfiguration, as the mapping command that is used for making the Select and Start buttons visible can also be used to alter what the buttons map to, so it can all be tweaked that way.
All of the code for handling keyboard input, as well as the low level functions for key reassignment. It contains within it a full listing of the keyboard mapping for the UK keyboard, which shows one of the (many) limitations of the RISC OS Internationalisation system. It isn't possible to provide a keyboard low level scan code and get back a higher level (ASCII?) code representing what that key does in the current language. For example, I know that a UK keyboard begins QWERTY on the top row of letters. In French, it's AZERTY. I think it's QWERTZ in German (but don't quote me on it). It seems that the only possibility here is to maintain our own lookup tables for each supported keyboard type. That's really stupid considering that the OS needs to know this itself so it knows what to do when you press key 16
(the one just to the right of Tab).
Surprisingly, there's no in-line assembly here. Just an awful lot of OS_Byte calls, because the RISC OS keyboard handling is basically the BBC MOS keyboard handling with a decade's worth of patches and bodges applied, and, worse, two decades of cobwebs.
Diving too deep into the keyboard handling is the way of madness.
This big wodge of code deals with everything 'Lucy'. Her movements, movement checking, sorting out the floor position (and if she's supposed to fall), camera actions, touching ghosts and spiders, and various other things. It also handles the movement animations.
An amusing "optimisation" is the fear meter. There is no SpriteOp call to plot a rectangle from within a sprite. It can be done, but it is part of the transformed plotting, so there's a lot of maths involved in what ought to be a simple pixel copy. When using transformed plotting to put only the amount of the fear meter that I wanted on the screen, it chewed up 20fps making everything run at around 30fps. This was clearly unacceptable.
So time for some blatant cheating. I gave the fear meter a mask, and use a simple plot-with-mask on the meter. When the fear changes, I simply rebuild the mask accordingly. This was, actually, slower than the transformed plotting (because it has to call the mask set SWI about twenty thousand times - once for every pixel - I don't poke around in the sprite data, it would be faster, but eww!) but you don't notice as this is only done once when the fear changes, not for every single frame.
I have optimised it a little, so that if the fear is increasing, it will only alter the mask pixels from the previous fear point to the current fear point, which means that it will only alter that which is necessary.
When fear goes down, the entire mask is rebuilt, I haven't optimised that, as you'll either lose 5% due to a good photo, 25% due to drinking tea, or all of it due to dying.
This draws the intro and outro screens, and implements the entire menuing system.
There was to be mouse scroll wheel control of the menus, but this turned out to be crashy on the ARMX6. I suspect that something is not using the right registers with the OS_Pointer SWI, however I don't have an ARMX6. All I can say is that it worked fine on my system (a Pi) and crashed on the ARMX6. As mouse control was mostly a "I wonder if I can...", I simply took it out. Well, actually it's there in the source as a conditional option in case I ever feel like looking to see what was going wrong.
Interface to AMPlayer for playing Mamie's theme.
Interface to RDSP for playing the sound effects. Basically, this loads all the samples, and then plays than using a construction similar to SOUND &2X, &X20, 256, 100
using OS_Word 7, the values being determined mostly by trial and error.
This selects an HD (1280×720) screen in 256 colours, sorts out the buffering memory, and handles buffer bank switching and screen clearing, as well as the whizzer effects.
The whizzer works by obtaining the address of all of the screen banks, and then running a double buffered redraw (rather than triple buffered) as it copies words from the third buffer. In other words, that which is to appear on the screen is drawn into the third buffer, and it is incrementally copied into the other two buffers to animate the whizz-in effect.
For the whizz-out effect, this is just whizz-in to a black screen.
The whizzer is entirely written in C. No assembler. It's kind of icky to poke around in memory directly like that, but was necessary due to how it works. It's the only place where memory exernal to the application is fiddled with in this manner.
Anything to do with the spiders that isn't an is-it-touching-Lucy (that's part of Lucy's code).
The spiders run autonomously. Every screen redraw, a function is called to check the spiders and draw them according to their status, if they're visible.
Pretty much all of my projects have a source called 'wrapper'. It's where the main() function lives, and it sets up the environment (signal trapping, seeding the random number generator, disabling ESCape, checking RDSP and AMPlayer, checking the screen mode can be set, initialising everything, drawing the title screen, and then dropping into the menu handler for the rest of the code.
The menu handler never returns. It will handle graceful quits for itself.
The backtrace code for handling unexpected signals (aborts, etc) is here, as is the debug function that outputs stuff to DADebug (though that part isn't included in release versions).
This is a little bit of code that is automatically generated by a short BASIC program when the project is built. It contains the project name, version, and the current date.
It was intended to remember the date, and bump the version number if the date 'now' is different, but I never got around to coding that. It was simpler to just fiddle it manually.
Assembling Mamie
The Mamie executable contains not just the code, but all of the resources required by the game with the exception of the embedded advert (for AmCog's Haunted Tower Hotel) which was left separate so it could be changed if desired, and the audio samples as there is (currently?) no interface to the RDSP module to say "this block of memory of XX bytes is a sample, please play it once". So the samples were kept as files so they could be loaded into RDSP.
Everything else is a part of Mamie, and is loaded at the same time as the game code. While it might seem odd to have a three and a half megabyte game, we can be assured that once it has been loaded, it's all available. No delays seeking and loading in resources. No need for lots of code handling things that might not be present. Instead, one single big wodge to load in (which shouldn't be too long from an SD card or modern harddisc), and once it's loaded it's all available.
Lessons learned?
Part of the process of developing something like this from scratch is in learning from mistakes.
There's nothing in the game code that should prevent a spider from being in such a place. The problem is the use of single characters to represent attributes, so "S" is a spider on the right hand side of a cobweb over a floor tile. A lower case 's', by the way, is the spider on the right hand side of a cobweb in a 'nothing' tile.
This isn't a big issue, admittedly, and it's a trade off between ease of creating levels and their flexibility. It's no good having a brilliant format that can handle all sorts of things, if it's a total pain to actually work with.
Jeff Doggett, 14th December 2021, 19:27 My doom port uses a 256 colour palette as the original doom source code is based on that premise. The palette is stored in a separate lump in the wad file, so is a relative simple matter to read it and pass it into the ColourTrans_WritePalette call.
© 2021 Rick Murray |
This web page is licenced for your personal, private, non-commercial use only. No automated processing by advertising systems is permitted. RIPA notice: No consent is given for interception of page transmission. |