-
Notifications
You must be signed in to change notification settings - Fork 4
User Interface (Part 10)
We will score the effort of the player and also tell them when they run out of game-lives, because we are terrible persons and we want to make some players feel incompetent (also we want to give them chance to feel better when they actually improve).
Seriously, let's not overthink this and do what the classic arcade games did, just as a tribute to them.
In this part we will add pseudo-random number generator (will be needed to hurl snowballs at player in unpredictable patterns and we will also use it to test the add-score routines), routines to initialize the game state explicitly depending on the game phase (new game → new level (stage) → new life) and User Interface (UI) routines to initialize and refresh it.
(here is a good spot to open the SpecBong.asm
and try to match the source with this text)
(you can also check the total difference between "Part 10" and "Part 9")
Let's deal with the random generator first, because it's not even my own work, I just copied it from Baze's web which contains multiple small beautiful snippets of Z80 assembly. The generator got label Rand16
to be called by, and there are two new places in the initialization part of code to seed the pseudo-random sequence with somewhat random number taken from the R
register of Z80 CPU (it keeps advancing every memory-refresh cycle, so it's not random value at all, but depending when the user did launch the SpecBong, it will sample the linear sequence of numbers in R
at different spot and add some randomness to our generator). This is very flawed and cheap way to add entropy into your code, if there would be some kind of halt
or other synchronization in the code path of NEX loader, it may lead to sampling the same value of R
every time. If you want to improve the entropy of your pseudo-random generators, you should add more randomness input at the beginning, like for example counting some counter while waiting for user inputs starting the game, using the precise delay as another seed init value or reading some noise if such source is available. I'm not aware of any good source of noise data in ZX Next, most of the stuff I could recall boils down to the entropy level of register R
(i.e. not very noisy or random), but maybe something will be discovered over time.
Never re-seed your pseudo-random generator for "better randomness", this is mistake I do sometimes see in assembly questions when people re-seed the generator for example every frame, wondering later why they get similar values out of it. You should re-seed the generator only when you have particular reason for it, like for example replaying recording of some game which has deterministic internal logic, and saving the random seed + user inputs is enough to re-create the gameplay of user.
But that's about the initial entropy of our generator, every generator has also runtime characteristic about the produced sequence of numbers. The generator copied from Baze has only short period of 65536 values before it will start to repeat the whole sequence. This is on the very weak/bad pseudo-random generators side, but it is sufficient for the SpecBong. For better random generators, the NextBASIC itself just few days back improved the implementation a lot, and you can ask Myopian at Next discord server for even stronger (in terms of "randomness") generator. The weaker generators tend to be faster than stronger ones, so if you plan to call random generator really often, you may need to scale down on its power to get good performance. (SpecBong is using weak generator just because of my laziness, and convenience of copying the code from Baze)
There's some more code added in the initialization part, setting the Next to crawl at 3.5MHz only (to see better with the colours in border area, which routines are slowest and by how much). Setting the layers order from SLU to USL (classic ZX graphics being above sprites and Layer 2 background image - we will make most of the ULA graphics transparent, so the game will become visible again under it, but the parts of the UI drawn in the ULA graphics will display on top of everything). Setting the clip-windows for all three layers explicitly (in case they contain some unexpected values from previous SW running at Next). Calling the InitUi
routine to setup the UI graphics (more on this later) and finally instead of setting the player sprite to hard-coded position, the specialized routine GameStateInit_NewGame
is called, to group all the re-initialization of game state at single place.
And let's look at the GameStateInit_NewGame
first, what it does. It will first reset current "score" to eight '0'
ASCII characters (the game will track player's score as ASCII string, not as integer value!), reset player's lives counter to 3 and fall-through into GameStateInit_NewLevel
routine. This is intentional, so when new game starts, it will initialize also the "level". But the SpecBong has only single level without any variables at all, so this one just falls through into GameStateInit_NewLife
routine, which resets "bonus" counter to be again "5000" (again stored in memory as ASCII four digit string), resets player position, helper variables of player controls, ladder/jump/fall helper variables and exits (not resetting enemies AI and positions in current stage yet - something to be done later).
The InitUi
routine does set ULA palette to use for "white" paper (both bright 0 or 1) transparent colour $E3 and also sets that as global transparency color and does the classic "CLS" manually. This makes whole ULA layer transparent and background Layer 2 image visible.
Then particular areas of ULA screen where the score/bonus/labels are displayed get their attributes modified to display the text in particular colour and the static texts for labels are printed into the video-RAM memory.
The helper .FillAttribs
subroutine is probably not worth a comment (just using ldir
to fill few ULA attributes with particular colour).
Right below this there is...
The RefreshUi
routine is called every frame from the game-loop code, and it will keep the displayed score, bonus and lives in sync with the current values in memory.
Because we keep the score and bonus values as ASCII strings (and not integers), to display them is actually as simple as printing any other ASCII string, just using PrintStringHlAtDe
subroutine (will get maybe some comment later, for now "it displays ASCII string" of length B
).
Then there are few sprites reserved to display amount of lives, so these are set up - clamping the maximum displayed value at 6, in case the player has more lives, the correct value is tracked internally (as integer!), but only six small-player sprites are displayed.
And that's all what is needed to refresh the user interface between two frames.
As we keep score and bonus as ASCII strings and not integer values, we can't use simple add/sub/inc/dec
arithmetic instructions. This is where the specialized routine like AddScore
comes to the rescue. It accepts value 0 to 255 to add to the score, but to make feel player better about their scores being huuuuge, the bottom two zeroes are actually just static text never updated and all the arithmetic is done in hundreds and above. So A=3 means to add +300 to the score visually.
We do subtract from the value in A
in a loop hundreds and tens, counting how many of them were subtracted, left with value 0..9 in A
(and tens and hundreds counts on the stack). These three digits are then added to the original ASCII digits of score string, going from hundreds to tens of millions - for the latter digits the score addition is zero, but there may be dangling plus-one when the previous digit wrapped over digit '9'
(carry).
Similarly the DecreaseBonus
does decrement the "bonus" string by -100 every call, replacing initial zero with space for last ten 000..900 values and stopping the decrement when the bonus did reach the final value 000.
You may wonder why calculating with strings and not integers. It's a trade-off, making the printing routine super fast, as it has to only print the characters without any formatting. There's extra "carry-over" logic in the addition loop updating the higher digits of score, but as the score values don't fit the 16 bit register (not even when using the trick of fake "00" at the end, that's still "only" 6,553,500 as maximum score), you would have to deal with multi-word arithmetic anyway, resolving the carry-over logic for those, making the text addition only somewhat slower than integer one. Still if you would be adding many sub-scores over the single frame to the total score, the integer approach (postponing the decomposing of integer to digits) would bring some performance advantage, but in case of such usage you can also just add all the scores from single frame as integer first and then add it to the string in one go (if the sum fits the 8 bit register), or use some form of number storage which is half-way between binary and string to make the integer-to-string conversion simpler but keep the arithmetic reasonably simple too (like BCD encoding of numbers).
It's just question of doing some math ... (pun intended) ... to evaluate which form of numbers gives you less headache and better performance.
The PrintStringHlAtDe
takes string address in HL
, target video memory address in DE
and length of string in B
.
It does use font graphics stored in the ROM area from address $3D00
, so this is only valid when the low 16ki of memory is mapped with ROM which has the font char-set available (this is a bit weak spot of SpecBong, not enforcing the correct ROM in the Next machine mode, but so far it always works as expected... when one day after NextZXOS distro update it will display garbage instead of characters, it will be the time to add explicit selection of ROM into the initialization code).
Then the pixel data are transferred to video ram with eight ldws
instructions which are perfect for this task (similar to ldi
but advancing target address by +256, which is what we need while drawing into ULA characters). Then the video-ram address of next character is calculated in DE
and the code loops for next string character, until all of them are printed.
So the new subroutines were introduced, now take a look at the game-loop, how it is calling RefreshUi
right after sprite positions are updated (to finish the ULA layer redraw before the display beam starts showing it).
Then just ahead of the end of game-loop there is debug code adding random value to the score every 64th frame and decrement the bonus value, for test/debug purposes.
Build the fresh NEX file, run it, and observe the bonus value ticking away while score gets random bumps, with three small "players" displaying the amount of lives.
Now it may feel like the game is almost complete, but there is one more non-trivial topic to cover before we will do the final assembling of the puzzle. When the player jumps over a snowball, we should reward him for the skillful move, right? And what about jumping over two or three of them with a single jump...