Skip to content
Peter Helcmanovsky (Ped) edited this page Jun 12, 2020 · 1 revision

Few more things and we are done

Ever heard about Pareto principle?

Never mind, we need only to add proper player death sequence, adding bonus to score when player reaches top platform and restart new stage. And hurling snowballs at player in some game-like pattern.

Step by step marching ahead

(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 12" and "Part 11")

The changes in code start with disabling the performance-measuring by border color and defining structure S_UI_STRING_DATA (explained later) and removing the CSpect breakpoint at start.

Then we will remove the big init block setting up the test-snowballs and leave all of them at invisible state for a start.

The code of game-loop dealing with sprite attributes upload and UI refresh (and any other graphics redraw) was extracted into separate GameLoop_BaseThings routine to make game-loop somewhat easier on eye of programmer reading the source, and there is completely new call EndOfLevelLogic routine after the player-vs-snowballs collisions are resolved (probably a good time to die).

The "Init" functions chain has some new pieces of code, first the "new game" init-level gets init of stage number init to string "00" (it will be incremented to "01" just few instructions below, but we will keep this redundancy to maintain the chaining of init routines, enforcing call of new-stage and new-life automatically by calling new-game, etc). And LevelDifficulty variable is reset to 150 - this will affect the amount and frequency of snowballs emitted toward player.

The following init-level "new stage" will increment the LevelDifficulty variable by 25 every new stage (so first stage starts actually at value 175, again letting it to do adjustment ahead just to keep the chain-like logic of init functions). Then the stage number string LevelNumberTxt is incremented by +1 using the code written for score addition. The stage increment is not protected against stage being already "99", as the difficulty of the game should allow nobody to survive to that point (BWAHAHAHAHA).

And finally the "new life" init-level will set temporary difficulty variable CurrentDifficulty to quite high value making the first snowball emitted reasonably early after the stage starts, and resets the emit-snowball cool-down timers. Then it does also set up all snowball sprites to be invisible and have alternating sprite patterns 52 or 53 and exit through RefreshUi_Level routine to refresh the new stage/score/lives status also in the displayed UI on screen.

Some more refactoring

The InitUi code is rewritten a bit to further shift away from constants-in-code toward constants-in-data. It will use the new S_UI_STRING_DATA structure to define where particular string should be displayed, and which ULA attribute bytes should be set for it. The long code calling the separate .FillAttribs and PrintStringHlAtDe is then collapsed to simple loading of table pointer (the table contains all the data about the UI strings) and jumping to PrintStrings routine which will process them.

This new system is used also by RefreshUi_Level and RefreshUi. We arrange the UI-strings data to end the whole table with the dynamic items like score/bonus which need refresh every frame (the table starts with static labels which are drawn only during InitUi), and the refresh routine then calls the same PrintStrings processing routine with pointer toward latter part of the data table. The RefreshUi_Level will refresh also stage number, the RefreshUi does redraw only score, bonus and player lives.

Those little things

The AddScore has also some new code, remembering the ten-thousands digit before calling the original "AddScore" code, then afterwards if the the ten-thousands digit is different and it is equal to '0' or '5', the bonus life is added to Player1Lives variable. This will add extra life every 50,000 points.

(Try to modify it to add new life every ten thousand points just by deleting code without any line added)

And the brand new EndOfLevelLogic routine, dealing with various ends of current stage. It checks first player y coordinate to see if player did reach the top platform, not doing anything if not.

Once the player did reach the top platform, there is new custom game-loop dealing with end of stage animation, it does call the same redraw of sprites and UI as the main game-loop. Then it does switch jump-bonus detector off and calls JumpBonusLogic routine to keep any remaining "star" sprites animating (and there will be more of them). Then the remaining "bonus" value is slowly transferred into "score" value, every 2nd frame 100 points are transferred from bonus to score, until bonus value is zeroed (string is equal "0 " value, which in machine code is just 16 bit number as any other, so single sbc hl,de is enough to compare current bonus value with the final one).

Also every fourth frame there is new "star" sprite emitted somewhere near the bottom of the Santa bag (trembling the coordinates somewhat by the TotalFrames counter) with different palette offset to make the star differ also by their colour.

After full "bonus" is added to "score" (with "stars" flying from Santa's bag), there are another 50 frames (one second in 50Hz video mode) just animating the star sprites without any modification of game-state, creating sort of "freeze screen" to give player some time before the new stage is initialized and regular game-loop is resumed (starting with new stage initialized).

Losing my life here

One more completely new routine is PlayerLosesLife. It will first play half a second long animation of player's death again in its own custom game-loop code (keeping almost all of the game state "frozen"). The animation is again just "incrementing" the three-bit pattern for mirrors/rotate to create chaotic spinning of sprite, then the amount of lives is decremented and the final pattern of death is displayed for another half a second.

After that the game checks if this is "game over" situation with zero remaining lives. If not, the init routine for "new life" is called and regular game-loop resumed. But if it's game over, there is "GAME OVER" string displayed char by char in middle of the screen in delayed way.

Then the game waits for the fire key, erases the "GAME OVER" string from the screen and jumps to init routine for "new game" (resuming original game-loop afterward again).

Also the SnowballvsPlayerCollision routine is adjusted to not tamper with player sprite palette offset (no more sick colours), and jumping to new PlayerLosesLife routine whenever some collision with snowball is detected. Also the collision sensitivity was tuned down from 11 pixels radius to 10 pixels, to give player even a bit more leeway before he is "snowballed".

And the Player1MoveByControls routine also does jump to new PlayerLosesLife in case of too-hard landing on platform, and ability to cancel ladder-climb-up pose by using jump (at the top/bottom of ladder only) is added, it was originally intentionally disabled waiting for left/right input, but it felt too punishing to the gameplay.

One more thing

Do you still remember we did want to just "hurl snowballs at player"? Yeah, that part is still missing.

In the SnowballsAI routine after advancing all snowballs rolling over the stage, there is new piece of code spawning new snowball every now and then.

First the EmitBallCoolDown1 cool-down counter is processed, if non-zero, no new ball could be spawned. Otherwise the Rand16 routine is called to get pseudo-random 16 bit value in register HL. Then the current-difficulty variable is increased by two and loaded into DE (so the current "difficulty" value keeps growing every frame until snowball is thrown). If the EmitBallCoolDown2 - second cool-down counter - is non zero, the difficulty value in DE is further multiplied by four (to make launches of snowballs in groups more likely).

After all these adjustments to the "difficulty" value, it is added together with the random, and if the sum is less than 65535, the routine just returns, not launching new snowball. But if the sum is more than 65535, it will try to launch new snowball. First it will loop through the sprites reserved for snowballs, looking for first one with visibility set to 0 (not used currently).

If some unused snowball is found, the CurrentDifficulty variable is reset back to LevelDifficulty variable value (so the launch of next snowball is less likely, but the value will keep growing every frame, raising the probability over time). And finally the sprite is set up to initial position just at the left edge of the screen on the top floor, and resets both cool-down timers of emitter to value 4 (four frames no other snowball can be emitted, and then four more the probability is four times higher) and finally it returns back to game-loop through DecreaseBonus routine. So each hurled snowball lessens the bonus value by 100 points (bonus starts at 5000 at beginning of stage).

Are we there yet?

There is also the new PrintStrings routine to help printing UI strings, this one is quite straightforward, reading the data from table and printing the characters to screen and setting the attributes according to the data.

There are all the new variables and UI strings table definition in the data area, and snowballs have now 80 sprites reserved for them (to fill up the stage quite nicely if difficulty is high enough), and that's all.

If you build the fresh NEX file and run it, it will work as ZX Spectrum Next game, you can play it, get "GAME OVER" after losing all lives or finish the stage with the stars-animation over the Santa's bag:

Stage finished

Final stretch

Now this probably feels like more work than you expected initially (at least it did to me, was supposed to be quite short job of just putting everything together), but that's common with any SW development. The final changes putting the whole SW together which feel like 20% of project often take lot more time and effort than just 20%.

With game projects, this is often just half-time moment or less. Yes, SpecBong is now technically fully working game, but is it good game? It feels a bit lacking to me, lacking variety, levels, but even more polish just within this single stage presented. And the changes described in this part of the tutorial didn't happen in such straightforward way as they are described here and committed in git history. Some code (like difficulty calculations of snowball emitter) are like fourth variant of code, until I did have something reasonably simple as example yet at least somewhat working as game. So this last part did take me lot more time than you may think by comparing it to some "hard" parts like writing sprite collision detection.

Yet SpecBong is in its current state still close to what modern game developers call "vertical slice", having all major game system working, but most of them in their initial version, and requiring at least the same amount of development time (or more) to bring it to certain level of excellence.

This is the point where true labor of love and talent will show, and change the initial raw experience to something amazing.

But this is also the point where SpecBong example/tutorial project ends, no more love from me for this project. But maybe you may want to experiment with it some more, maybe you already have some idea what should be added to it to... Or maybe your fingers are already itching to write some completely different game you dream about. If the SpecBong project did help you to better understand what it takes to write a game, then I will be happy about it. It was not meant to play amazingly well, but to show basic concepts and one of the possible ways how to develop for ZX Spectrum Next.

Good luck in your projects, I'm looking forward to see your dream-SW.

Peter "Ped" Helcmanovsky / 7 Gods demo group