-
Notifications
You must be signed in to change notification settings - Fork 4
Hurling the snowballs (Part 7)
Back to designing the challenges for player. The game idea of Johny was described as hurling the snowballs at player, the snowballs rolling on the floor from top to bottom. Also the player should be able to run on the floors and jump around. Now if we would have means to tell if certain sprite is at floor (or how much above/below the nearest floor), it could be quite simple to write logic of "rolling on the floor". That sounds like a task for collision detection system.
The collision detection system from previous part does only calculate collision of player sprite vs snowballs, so it has to be either extended to work also with floor vs everything, or we can write new one dedicated only to the floor collisions. The reason why we will actually write separate system is that the floor in SpecBong is completely static and its position and shape is very regular. Its thus possible to come with highly specialized routine calculating the floor collisions in short time without requirements for some huge memory tables or extensive pixel testing (while extending the sprite collision detection system from previous part to collide also everything against hundreds of pieces of floor would be too much to ask from 8bit CPU, if implemented in the naive way of checking everything against everything).
After some looking at background image, instead of defining the floors at pixel resolution, we will do it in much larger chunks. The background image occupies 192x192 pixel area, split into 16 pixel wide columns → that is 12 columns. In each column there are at most seven different floors (at different height). The idea is thus to define table of 12 columns with about 7 floor heights in correct order.
Then to get floor height below particular sprite we will first check its x-coordinate to figure out into which column the sprite belongs to (maybe check two nearby columns to avoid "wide" sprite falling from floor if the floor ends below their side and centre of sprite is above air already), and then go through the list of different floors defined in particular column until one is below the current y-coordinate of sprite. Because column list is at most 7 items long, even 100 sprites requesting this operation will take about 100 to 700 checks against the table value, which should be doable in the within single 50Hz frame with 28MHz CPU (doing about 40-90k instructions per single 50Hz frame at 28MHz).
(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 7" and "Part 6")
Let's do the floor table data first, see toward end of the source file in the data area the table starting in memory at address PlatformsCollisionData
. We are actually adding 14 columns to the table, having one extra column for the coordinate position outside of the background 192x192 pixel area, just in case some sprite wanders a bit too off (like snowball entering the game from edge of screen), and each column has eight floor heights, to make sure there is always some under the sprite, even if it is fake invisible floor below the background image, this will simplify some of the internal logic of the code, as we know the sprite must hit some floor and there's no need to deal with case when there's only void under the sprite and returning that as extra value.
Also each floor has extra second byte value for "flags" to define special features of particular tile of floor. At this moment we will use one bit to encode information about "slope" of the floor, to which direction the snowball should roll if it lands on the particular floor (even the level area of floor in top of the background is marked as "slope right" to make the balls roll toward player). And second bit to mark floors which are near the ladder, to give the logic code cheap hint when it should handle also ladder-logic part of code.
You may notice that even floors with less than seven floors have been padded to eight floors in total with the fake very-bottom floor data copied at the end of the column. This will allow us to calculate starting memory address of column i data by simple multiplication instead of parsing through whole table, for the price of slightly larger table in memory. But the table does still fits into less than 256 bytes (even with padding) which we can afford in SpecBong easily (still plenty of memory available) (and a code handling uneven column sizes would probably quickly consume all the saved bytes on the table side and maybe take even more).
There is new subroutine GetPlatformPosUnder
taking pointer to sprite data which will return Y-coordinate of nearest floor below (well, almost) the sprite and the flag byte of the floor hit. The subroutine will preserve the HL
register in stack and call the implementation part. The implementation part is designed to return back to this caller only if no floor was found, so the main routine will reset result to floor coordinate Y=255 and zero flag in such case and exit. When the implementation part does find actual floor hit, it will skip return to the main subroutine and return directly to the original caller with the result data.
The .implementation
parts starts by checking if the sprite X coordinate is within the defined fourteen columns, the sprite X coordinate has to be in the 8..231 to fit into one of the defined columns (the first column starts 16 pixels left of the background image in the border area, at sprite X coordinate 16, but we will check centre of sprites, and even sprite residing at X coord 8 has its centre X coordinate 16). So the column number is calculated as (sprite.x - 8) / 16 (divided by 16 because column is 16 pixels wide).
But because every column has 16 bytes of memory for all floors defined, the address of column is calculated as column_index * 16. So we will take another binary math shortcut and instead of dividing and multiplying the sprite X coordinate, we will just clear bottom 4 bits of it to truncate the coordinate value to multiply of 16, that's the identical value which you would get by dividing and multiplying. So the sprite X coordinate turned into part of memory address directly by two instructions: sub
+ and
.
As the table PlatformsCollisionData
is defined at memory address aligned to 256B address boundary, it's low 8 bits are always zero and by concatenating top 8 bits of the address and the processed X coordinate into single 16 bit value into HL
register we suddenly have starting address of data for particular column where the sprite belongs (based on its X coordinate).
Then the Y coordinate of sprite is fetched and its "baseline" is calculated. It is +13 from top of the sprite, allowing the sprite to eventually enter the floor a bit with its bottom in case the sprite falls down multiple pixels per frame, to avoid falling through by accident.
This value is used to loop through the list of eight floor height values, looking for first value which is below the baseline and returning that one as the floor below the sprite.
We will modify the SnowballsAI
subroutine to process only visible snowballs (similar small change is added to SnowballvsPlayerCollision
collision detector), so we can switch certain snowball "on/off" by making its sprite visible/invisible.
After the visibility test we will call the new GetPlatformPosUnder
to figure out which is the nearest floor below the snowball (below the "baseline" of sprite which is at +13 from sprite Y coordinate, thus the ball can be 3 pixels deep into the floor to still find it as the nearest one below it). The coordinate of floor is lessened by 16 to be usable as snowball sprite Y coordinate directly (the bottom pixel of snow will just touch floor pixel at +16 pixels below).
Now if the sprite Y coordinate is same or below this value, it will jump to .isInOrAtPlatform
code, which will reset the snowball Y coordinate to this value (raising it to be right at the floor if it fell slightly into it). The new Y coordinate is then checked if it is in the background image area and the sprite is set to be invisible otherwise (making it "off" for collision and logic routines and of course making it invisible in the HW sprite renderer). And then the sprite adopts the movement direction to be +1 or -1 based on the "slope" of the floor which the ball is at. And that's all for balls which reside at the floor.
The other code branch, if the ball is too much above the floor and falling down, will extract the X direction from the "mirrorX" flag to keep the same direction as before the fall (replacing the floor "flag" value in register C
), does adjust the Y coordinate by +1 (in case it just hits the floor by that) or +2 (even more above the floor) and falls through into the .isInOrAtPlatform
code, but with values in register patched enough that the end result is snowball free-falling through the air until it lands at some floor.
So we wrote the floor-collisions detection and adjusted the snowball AI to use the results, now we should maybe setup some test coordinates of snowballs, etc... or we can just build the NEX file from the source and see what happens with the old initialization.
If you did try it, you may notice it actually does hurl three groups of snowballs toward the player testing the result quite nicely. If you are really curious why the initial setup (not designed for this task) ended like this (I was curious!), enable the small piece of new code at the end of game-loop in the conditional IF 0
block (change 0 to 1 to make sjasmplus assemble the block into the output). This will make the game loop wait for the fire button after every frame, so you can "single step" each frame and see yourself how the new snowball hurling logic evaluates the initial state and how three groups of snowballs emerges out of it.
Few screenshots of such single-frame-stepping:
So the snowballs are rolling through the stage as intended now (reset the debug block back to IF 0
to see the code running at normal speed). As we can see on our performance-measuring with the border colours, the new collision detection and floor detection with snowball rolling logic takes some serious amount of CPU time, but it is still only fraction of time available every frame and there's still lot of spare CPU cycles to turn this into actual game by adding some real player logic...