mailto: blog -at- heyrick -dot- eu
Advent 2021 day 3
It's a short one this time, as I have to be up for half three(ish) tomorrow, to leave here at 4.20am to start work at 5am. Until 12.30pm. Thankfully it's the last Saturday this year. But I won't feel any less of a zombie...
Mamie Fletcher's House 3
The test code
Yesterday we looked at how tiles are used in games, and mockup example displays to get an idea of the look of the game.
The thing is, while the visual appearance is important, something equally (if not more) important is the behaviour. For this, we need to begin writing some actual code.
The initial code for the game was written in BASIC. This is because BASIC allows for a lot of quick gratification when fiddling around. "Should it work like this?" and "Maybe this would be better?", sections of code can be put together, tweaked, and "played" rapidly.
However, this isn't to say that BASIC isn't without its problems. Lacking anything that even resembles a definable structure, there was a lot of horribleness in the code. The map, the data that describes the attributes of each tile, used the lower two bytes to represent which tile it was, and then the upper two bytes to represent "furniture", such as "this is a floor tile with the left side of the arched window". There was a lot of mundane bit shifting to sort out whether it was the tile number that was wanted, or the tile attributes.
Likewise the ghosts, a negative Y value meant the ghost was active, and setting bit 16 of the animation cycle counter meant that it was the more powerful ghost. Which meant a lot of tedious junk to have to take care of that would have been more nicely handled with structures and a bitfield.
Some BASIC code.
Floating Point - Just Say No!
For jumping, if you jump whilst holding either left or right, the character will continue jumping in that direction. She will rise up 70 pixels, and then fall down 70 pixels (unless she lands on a step). The reason for 70 pixels is because the steps are 50 pixels up from each other, and the floor is 16 pixels tall, so she has to be able to jump just a little higher to land on the step.
Incidently, at this point, I realised that I needed to scale Lucy down as at the size she originally was her head went through the floor above when she jumped!
y% = INT(SIN(loop) * 200)
I had calculated the graphics necessary to create a realistic arc, however in play testing I was decided that the visual difference between "realistic" and "just go up then go down" was not really that noticable, while the difference in instructions executed was considerable.
It is not wise to use any floating point on RISC OS because whilst certain versions of BASIC can use the hardware VFP, and BASIC's own routines are extremely good, when using C the Norcroft/DDE compiler (along with SharedCLibrary itself) emits code for the VLSI FPA10 which was a floating point chip from the mid '80s that was intended to be used in the original Archimedes as an expansion. Wow, that was a long sentence! Let's break it down.
Aside: If you're not a nerd and/or you don't care to learn esoteric things about floating point numbers, click here to skip over this little aside...
For the non geeks that might be bored enough to actually try reading this, you'll know that there are two sorts of numbers. The first sort of number is what is referred to as a "whole" number. If you have two apples and I give you another, then you have three apples. In computing, this sort of number is known as an "integer".
The other sort of number mostly turns up in money. If you have €1,25 and I give you a fifty centime coin, you'll have €1,75. These sorts of numbers are usually referred to as "decimal numbers", or "fractions" if written slightly differently. 1.75 and 1¾ are the same value. Usually in computing, these sorts of numbers are called "floating point". I'll explain why the "floating" below.
These are whole numbers. 1, 2, 5, 7, 42, 1973 and so on. These are fairly easy for both computers and humans to deal with. Four times five is twenty - we learn this sort of thing at a young age.
Processors have different ideas of what the largest number is. Home computers of the eighties counted up to 255, the first PC counted up to 65,535, RISC OS machines count up to 4,294,967,295, and modern 64 bit machines (like your smartphone) can count up to 18,446,744,073,709,551,615!
This is important, as processors can deal with integers extremely quickly. The simple eight bit processor (the one that could count to 255) can handle the exact same maths as all of the other examples (fifty billion multiplied by seventeen, for example), however it has to break the number into pieces and do each part in turn, which is slower.
- Floating point (real)
Floating point numbers, also known as "reals", are numbers with a decimal point in them. 2.71828 and 3.141 are two well known examples. They are known as floating point because the position of the decimal point can move: 1.2345 and 1234.5 for example. These sorts of numbers are quite complicated for computers to cope with (humans too!) and it wasn't until the latter years of the 486 era (the 486DX in 1992) when floating point hardware began to be fitted as standard within PCs. The only machine that Acorn ever produced with built-in FP was the A7000+ released in 1997. It was possible to fit floating point to earlier machines (like the 80387 chip or the FPA10) but most people didn't because the things were crazy-expensive. In September 1989, floating point expansion for your Archimedes would set you back €688,85 (inc. VAT), which was only fifty five pounds cheaper than an entire new A3000 computer. In July 1988, InfoWorld magazine did a feature suggesting that the new 80387-25 (FP co-processor for a 386) would retail for around $1,395; which seems really scary, but then in those days a 16MHz 386 machine with 65MB harddisc and 14" flat-face CRT monitor would set you back a mere $3458.50!
So... most people did without actual floating point hardware and just faked it in some manner.
Floating Point options in TurboC++.
- Fixed point
This is a hybrid system where values that have a decimal point can be stored as integers by using one integer for the part before the point, and one integer for the part afterwards. The position of the point is fixed. A common example could be for processing currency units ($xx.yy, £xx.yy, €xx.yy, etc) where the main currency unit can have a fairly large representation, whilst the cents/pennies/centimes has exactly two places of accuracy.
There are other uses of fixed point, however typically neither BASIC nor C supports it natively, so it is only mentioned as a "there's this too".
One final thing - it is bad to actually use floating point for currency. This is because there is no exact binary representation of 0.1 which means that accumulation errors can quickly occur. How much of an error depends upon your system. For regular BASIC, this happened:
>PRINT 1.01 - 0.99
The FPA and VFP versions of BASIC return the expected answer.
Several programming languages have a special "monetary" data type for handling currency. Often this is either a scaled integer (VB's Currency type is an integer in 64 bits that is multiplied by 10,000, giving fifteen digits to the left of the decimal point, and five to the right, and zero rounding) or a version of fixed point that uses two integers (one for the left of the decimal point, and one for the right). Standard C, however, doesn't offer any of this. As long as you know you'll be working with normal currencies such as US/Aus/NZ/Can dollars, pounds, or euros then you could just multiply values by a hundred to remove the decimal point from the calculation.
All bets are off, however, if you're working with a currency like the Iranian Rial. Right now, as I write this, €21 will make me a millionaire in Iran.
And let's just not talk about what the currency of Zimbabwe dollar did in 2008! Okay, actually, let's do exactly that. Inflation was stupid. The government issued a 500 billion dollar note...that was worth about one single American dollar. They even issued a one hundred trillion dollar note. Surely there's a point where you just give up? At any rate, a Zimbabwe $1 coin would have been roughly equivalent to $0.000000000002 in American terms. Can your currency format cope with that? ☺
For machines without the hardware, a module known as "FPEmulator" would intercept the FP calls and fake them. It does a good job and it is really accurate, but the way it works is by intercepting failed instructions for an unknown co-processor. You see, the FP hardware is implemented using ARM co-processor instructions. If the hardware exists, then it will signal the processor to say "I've got this", and handle the instructions.
If the hardware does not exist, then the instruction will abort. The FP emulator will catch this abort and look to see if the instruction is something that it knows what to do with. If it does, it will (using regular ARM code) go through all the hoops necessary to pretend to be the floating point chip. Once it has done its work, which can potentially be hundreds of instructions, it will tell the machine to restart execution after the instruction that failed.
On a modern machine it seems ridiculous to be relying upon a slow emulation of an old maths chip when there's a modern one built into the device!
To put this into context, I wrote a bit of code to multiply two numbers (123.456 and 654.321) 4,096,000 times. When using code generated by the compiler, it took 3.88 seconds. When using FP code written in assembler, it took 3.88 seconds (because my code was fundamentally the same as the compiler). When using VFP code written in assembler, it took 0.07 seconds.
Yes, nearly four seconds versus under a tenth of a second.
That's the fundamental difference between using the built-in hardware floating point routines, and an emulation of an ancient chip that nobody uses any more. So using floating point in C (with the DDE compiler, GCC can emit VFP) means you get screwed. A lot.
It is for the above reason that I decided not to use "gravity" in when falling. I had thought about having a little value increment with each movement (say, 0.05) and this would be added to the movement offset (and rounded to nearest whole number). The effect would be a slight speed increase as she fell.
However, the use of floating point would make the code execute more slowly for... actually... barely noticable difference. It takes a falling human about 12 seconds to reach terminal velocity. In the game, we're only falling two floors maximum. Gravity acceleration is about 9.8 metres per second. Their average velocity is half that, or about 4.9 metres per second. So after the first second, a person will have fallen nearly five metres (½ × 9.8 ×1², or in BASIC
0.5 * 9.8 * SQR(1)). Whatever, it's more than two floor heights.
Falling is a little slower in the game so you can savour her demise. But there's no gravity. It just didn't seem worth adding.
Just another little aside, LD50 when falling (that's to say, 50% of examples died) for humans is a mere 12.1 metres. LD100 (everybody dies) is 15.2 metres. That's about the fifth or sixth floor of a residential block. Any more than merely 3 metres onto a solid surface may potentially result in broken bones. In this case, it depends upon the landing. Over about five metres, broken bones and possible internal damage are expected.
Yup, I did the homework here, and then decided for the purposes of the game to ignore it all and have falling one floor hurt and falling two floors kill.
Where are you?
Something that quickly became evident with the use of larger tiles was the need to track exactly where the player was within a tile. It might be possible if the player was a block that had the same dimensions as the tiles (like 8×8, 16×16, etc) to make for fairly simple detection. Has she run out of floor? Well, either it's under her or it isn't.
But the player character is a certain size, and so are the tiles, so one needs to look to see where in the tile the player actually is.
The game pays attention to the tile just in front of the character, because if you are moving, that's the part that will be of importance. In hindsight, this was not a great decision. It complicated the detection of where things were. There are a number of exception handlers in the code to deal with special cases. Mostly triggered by Vince, as he's the one who thought up things like putting multiple floor-with-hole tiles one after the other. Or ladders that can go up, but with no top to allow going down. I was perfectly happy to fix all the quirks Vince found, as his level designs were more than worth it, but I can't help but feel things might have been better had I tried a different method of detecting what's around Lucy. But since I didn't have any real idea of what this better idea was, and there was a load of code that was quirky but more or less working with the existing setup, I kept it.
The position check marker is the yellow line just in front of Lucy
As far as I'm aware, there are only two primary positional problems that remain. The first you'll never see. Development versions of the game had a number of F-key options to do things like disabling clipping (so she can walk through walls or just float across nothingness without falling), and one called "TurboLucy™" which cranked up her speed. These things were intended to get Lucy around the map quickly in order to test specific things. If, for example, I wanted to see how ghosts behave upon reaching the right side of the screen, I didn't want to have to walk across each time... But TurboLucy™ had some, uh, interesting side effects.
The other problem is that if you try to jump across multiple holes but you don't get your position just right, you are probably dead. This is because the next jump will miss and Lucy will fall, or because you will turn around and... Lucy will fall. She cannot turn in that manner, as the position detection will flip over to the other side, and will then be over a hole and... eeek!
I gloss over this issue by saying "turning in such a situation will cause her to slip". It sounds better. ☺
BASIC encourages hacking
Now, the benefit of BASIC was that it was possible to throw together something "that works" and then, once it is working, fiddle around to try to make something "that works better". So a lot of experimentation was involved. How fast should this move, or that. What about when this happens? And so on. Writing the initial version of the game in BASIC took almost as long as the C rewrite, mostly because I've never done anything like this before and I didn't find any tutorials on-line (those that I did find were for writing platform games with some sort of Java (I think?) library that seems quite popular)...but no help for RISC OS, hell, I'd even have settled for info on how games for a console like the SMS or SNES did things, but alas nothing.
So I basically went to work thinking about an issue, and thought about it while cleaning the staff break room (I can mostly do that on autopilot), and then came home and wrote some code to see if my idea worked. Some did, some required more use of the autopilot.
Once the program was written and playtesting worked how I wanted it, I set about rewriting it in C. It was always intended to be written in C. Compiled code beats interpreted. There's no question. Also, it's a lot nicer to have the more advanced variable facilities.
Some C code that's the same as the BASIC code above.
But sometimes C is better
A trick that I had used in the BASIC version was to load all of the right-facing sprites, and automatically create left-facing ones by simply flipping them. But with a sprite file embedded into the program itself, I needed to do this conversion prior to embedding the sprites.
The sprites, texts, and messages were all to be embedded into the executable to make it one single thing that needs to be loaded. This wasn't entirely possible as I use the RDSP module to play sounds, and there is no way of saying "there's a sample of X bytes at address Y, play it", so I needed to load the samples into RDSP one by one.
Along the way, a lot of the BASIC nastiness could be tidied up, with C's ability to handle bitfields meaning all that shifting and ANDing could be done away with. I also refactored an amount of code in order to reduce code duplication. Something I had noted was cropping up in the BASIC source, but not bothered fixing as I knew I was going to rewrite it all.
And, of course, extending a few things. Making things better. Redefinable keys, for example. Plus, thanks to a lot of DADebug output (a circular buffer held in memory for logging), I tracked down numerous movement quirks that were difficult to diagnose in the BASIC version.
I should note, here, that the game uses screen buffering. That is to say, it is drawing one frame of display whilst a different frame is actually being shown on the screen. This is a common technique that stops nasty visual effects resulting from having some screen activity happening whilst that part of the screen is being output to the monitor. Suffice to say, it is very very normal to use either double buffering (draw one, show the other) or triple buffering (draw one, one pending, show the last).
The problem is, if the game should exit in an unexpected way with screen buffering enabled, RISC OS will not cope. You'll get a blank screen and an unresponsive machine.
It was fairly simple in C to register an atexit() handler to do certain things as the program quit, and also to trap various signals in order to have a tidy exit if the program crashed.
But in BASIC, error handling is limited and you're pretty much out of the loop when it comes to serious errors. Which, when using buffering, made it that much more difficult to debug stuff. So it was actually a lot nicer to develop and debug in C. Certainly, I wasn't resetting the machine every time something screwed up and didn't restore the VDU state correctly.
But I wouldn't have been without that early BASIC version. It helped me sort out the ideas in my head into actual code, and refine them into something that almost looked like a game.
In tomorrow's part, we shall look at creating Lucy.
Please note that while I check this page every so often, I am not able to control what users write; therefore I disclaim all liability for unpleasant and/or infringing and/or defamatory material. Undesired content will be removed as soon as it is noticed. By leaving a comment, you agree not to post material that is illegal or in bad taste, and you should be aware that the time and your IP address are both recorded, should it be necessary to find out who you are. Oh, and don't bother trying to inline HTML. I'm not that stupid! ☺ ADDING COMMENTS DOES NOT WORK IF READING TRANSLATED VERSIONS.
You can now follow comment additions with the comment RSS feed. This is distinct from the b.log RSS feed, so you can subscribe to one or both as you wish.
|Gavin Wraith, 4th December 2021, 11:48|
Very interesting article Rick. Thanks.
(Felicity? Marte? Find out!)
List all b.log entries
Return to the site index
PS: Don't try to be clever.
It's a simple substring match.
Last read at 21:28 on 2022/01/26.
© 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.