Skip to content

BSC aka Big Sprite Collider (Part 6)

Peter Helcmanovsky (Ped) edited this page Jun 11, 2020 · 2 revisions

A proper game should be challenging

Where's the fun in just reacting to user inputs? Let's make player face some actual challenge, so they can feel achievement and joy when they successfully overcome the challenge (or miserably and bored if they can't overcome it at all)!

For example we can hurl snowballs at players and make him lose a life when they don't avoid the snowball... wait a second, how do we know if the ball did hit the player?

Seems we will have to scale down a bit in our enthusiasm about designing challenges for player and first overcome the developer's challenge of knowing whether some sprites collided with player and how much.

HW Sprite collisions

The HW Sprites of ZX Next provide collision bit. Yes, single bit. It says whether any two sprites did had overlapping non-transparent pixel in the time since the previous read of the collision bit (each read will reset it to zero) - the detection of overlapped pixels happens while sprite engine is rendering them (for particular video-line the particular single-line-cutout of the two sprites), again all of this is synchronized with the "display beam".

If you are wondering how to use this for complex collision detection in action game like R-type to blow enemies with players bullets and explode the player's ship when hit by enemy - I am wondering too (there are actually some advanced ways how to leverage even this single bit information in specific scenarios, reading it at precise times in sync with beam to get info about any collision happening between particular video-lines only and then run further SW checks to figure out if the collision was the one of the interest or one to ignore).

To make matter further complicated, the collision bit is not even emulated in some of the emulators, so by using it we would force ourselves to test the code only on the real Next or particular emulator emulating it (and accurately enough).

So we will not use the HW sprite collision detection in SpecBong at all, back to the all-SW solution.

SW Sprite collisions theory

We need in SpecBong just one type of collision between sprites and that is player vs any snowball. This will allow us to get away with very simple "O(N)" loop going through all snowball positions and comparing each of that against the position of player (for "R-type" like game you need to find more types of collisions, exploding the computational complexity of naive solution quickly beyond what the Z80N CPU could handle withing few milliseconds, requiring more sophisticated algorithms).

The collision between player and snowball is detected by calculating distance between centre points of the two sprites. If that distance is smaller than predefined constant, it will evaluate as "hit" of the snowball. You can imagine that as if player has invisible circle in middle of its sprite with radius 11 pixels and if any snowball enters this circle with the snowballs centre point, it counts as "hit". This doesn't account for actual pixel graphics of the sprites and even allows for some overlap of sprite graphics before the hit is being detected (the sensitivity is driven by the collision-circle radius).

This may feel like imperfection if you did expect pixel-perfect collision detection, but it is actually intentional design for better gameplay experience, letting player get away if they touch the snowball only just so slightly (if you know which game was direct inspiration for SpecBong mechanics, you can check the original arcade version to see it does not sport pixel-perfect collisions either and give some margin of error to player too).

This also makes the math behind the curtain very simple, because to calculate the distance between two points we can use Pythagorean Theorem Formula a2 + b2 = c2 - where c is distance between two points, and [a, b] are difference between x/y coordinates (in 2D plane).

So we will subtract players x/y coordinate (of the centre point of player's sprite) from the x/y coordinate of centre of the snowball, multiplying each difference by itself to get the squared value, then we will add the two squared values and calculate the root of the summed square... wait, that sounds not so easy any more.

Luckily we don't need to find the root of the summed square, because we don't need the actual distance (in pixels), we only need to know if the two sprites are close-enough to register the "hit" of them. If the original idea was to compare whether the distance < 11 pixels, we can square the whole comparison to check if distance2 < 112. The distance2 is the result of simple addition, and 112 constant is something the assembler can evaluate for us by writing 11*11 into the source.

SW Sprite collisions code

(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 6" and "Part 5")

The snowballs initialization and logic parts are adjusted to keep last snowball static near the player sprite (for easy testing of the collision detection) and the bug with player movement limited to 255x191 area instead of 256x192 was fixed.

The game-loop is extended with call SnowballvsPlayerCollision (after the player movement was processed) and that's all from the minor changes.

And then there's this new big SnowballvsPlayerCollision subroutine, calculating collisions of player vs snowballs and creating/removing the FX (effect) sprites to make the detected collision visible (by putting "sparks" sprite over the snowball).

First the HL is loaded with position of centre of player with shifted coordinate system where origin [0,0] is at the top left corner of ULA pixels (and background image), where sprite coordinates read [32,32]. This conversion is done to make both X and Y player coordinates fit into 8 bit registers, which will cost us this extra code to convert 9bit X coordinate at beginning, but will simplify/sped-up calculation with 8bit arithmetic (with few more memory addresses and counters loaded into IX, IY and BC).

Then the code enters loop .snowballLoop to try every snowball and calculate its position against the player. The X coordinate of snowball is also transformed from the 0..319 sprite coordinate system into -32..279 rejecting any snowball with centre position outside of the 0..255 8bit range (keeping only the 8 bit X coordinate). The Y coordinate is also transformed to the new coordinate system and also it's visual centre is used (of sprite graphics - it is not circle filling full 16x16 area but only like 16x13 oval in the bottom part of sprite pattern area).

With the Y coordinate of snowball the difference "dY" to Y of player is calculated and preserved in register D, then it is checked to be in -15..+15 range. If it is further away from player, the collision is rejected. Similarly the difference "dX" to X of player is calculated and sanitized to -15..+15 range (otherwise collision is rejected early).

Both dY and dX values are then squared. The negative -15..-1 values produce correct bottom 8 bit of result even with unsigned mul de, only the top 8 bits of the multiply is "incorrect" with regard to the sign of the arguments, and the interesting part of result for -15..+15 values squared fits into bottom 8 bits (you may want to understand binary arithmetic really well if you are trying to program in assembly, otherwise you may completely miss some opportunities for shortcuts and non-intuitive results).

The squares of dX and dY are added together (if the carry is set it means the square of distance was above 255 which means the snowball is still too far from player - even if it is already within +-15 pixels boundary box) and that's the final distance2 value.

That is compared (cp (6+5)*(6+5)) to check if the distance between the sprites is less than 11 pixels. The comment in code suggest the player's circle has 5px radius and snowball has its own collision circle with radius 6px, but technically it doesn't matter how we distribute the 11 pixels mentally, in the end we do cp 11*11 in any case, even if I described it initially as player having 11px circle detecting only single point of snowballs, or even if I would use single point for player and assign 11px circles to snowballs.

If the squared distance is less than 121, then the "hit" is detected, counter of collisions incremented and new "FX" effect-sparks sprite is defined in the sprite area following player sprite (to be rendered above player and snowball).

Following code does advance snowball pointer and loops back to .snowballLoop to process every snowball like this.

After all snowballs were tested, the C contains number of collisions (equal to the number of new sparks-sprites defined). But if there were more collisions in previous frame, there are some FX sprites defined which should become hidden for current result. So there is calculation to figure out how many FX sprites should be made invisible and small loop removing the visibility bit for those. Finally the new number of FX sprites is remembered for next frame.

Also the number of collisions is used as palette-offset for player sprite to make the sprite visually change (to weird colours) depending on the amount of collisions (as another test measure for debugging the code).

And that's all, player was checked against every snowball, snowballs colliding with player got extra "FX" sparks sprite on top of them and the player will get funky colours whenever some collision happens.

Look what we did

Now if you will build the "Part 6" source code and run the NEX file, you can move around the static snowball in the bottom area to test out how sensitive the collision detection is and if it works (you may want to modify the player logic code to move him only by +-1 pixel for finer controls - I used this originally to debug initial versions of the code and there were quite some bugs to be fixed before I got all the math right).

But you can also play the "avoid flying snowballs" game in the upper area of the screen. Probably still not worth of any interesting scores in gaming magazines, but you may feel some fun is already brewing there...

running SpecBong part 6 screenshot

(the player looks a bit sick if you feed him with snowballs)