Next up is handling the physics of the rest of the tile types.
Water source generates a tile of water below it if there's space.
Water drops down or moves sideways (right first, then left) depending on empty space.
Goo grows in all directions, given space.
People eater walks forward if there's space, turns right if it can't.
Stone and gem can fall through water, clearing it.
People eater can eat player.
People eater can eat goo.
Stone can splat people eater.
Water turns people eater into stone.
Water and goo turns into gems.
A lot of stuff in this chapter, but fairly little of it new; we primarily just check if we can move and then do so over and over again. Instead of dragging our heels, let's just implement all of the above so we can move on to something more interesting.
But first, the physics runs way too slowly. We need to decrement 176 until we hit zero; 176's divisors are 1, 2, 4, 8, 11, 16, 22, 44 and 88. We're currently using 16, so the candidates are 22 and 44. Both are better than 16; 22 is still quite leisurely, at 44 the stones feel deadly. As a trade-off we'll go with 32 (which is not a divisor of 176), by simply calling physics twice in a frame (since 16 is).
call physics call physics
We need some way to make some of the physics go slower; the stones are fine now, but if everything runs at that speed, it'll be a bit too hectic.
physloopcount: db 0, 0
This counter will increment every time we've finished a full physics loop, like so:
jr nz, physics_notlooped ld hl, (physloopcount) inc hl ld (physloopcount), hl ld hl, 176 physics_notlooped:
We could have used the frame counter, but there's a few problems with that. First, if we (still) tweak the speed the physics runs, everything will blow up. Second, it's entirely possible we'll have so much stuff moving at some point that we spend more than one frame time. Tying the physics to frame count would make it nondeterministic and that's undesirable most of the time.
We'll add handlers for water source and water like before:
cp 9 jp z, physics_stone cp 16 jp z, physics_watersource cp 15 jp z, physics_water
The water source is pretty simple.
physics_watersource: ld hl, (physloopcount) ld a, l and 7 jp nz, physicsdone ld a, (ix+16) cp 0 jp nz, physicsdone ld (ix+16), 0x8f ; generate water jp physicsdone
The primary complication with the water source is that we want to slow it down a lot. We get the physics loop count, do a modulo of 8 (i.e, check if all bottom 3 bits are zero) and bail out if it's not the time. If we are on the 1/8th physics loop, we check if the tile below is empty, and fill it with water if it is. There's no need to check if we're on the bottom border because water sources don't move and adding one to the bottom of the map would be stupid. Let's try not to be stupid.
I considered making a check here that if there's goo below the water source and turn it into a gem, but let's say the goo manages to block the water source.
Water itself is a bit more complicated.
physics_water: ld hl, (physloopcount) ld a, l and 1 jp nz, physicsdone
Skip processing every other physics loop. We'll have to try to see if we can interleave the physics of other objects so that when we skip processing water, we process something else.
ld a, ixl cp 16*11 jr nc, water_sideways
Map bottom check.
ld a, (ix+16) cp 0 jr z, water_down
If we can go down, go down.
water_sideways: ld hl, (physloopcount) ld a, ixl add a add l and 2 jr z, water_right
Take the physics loop count, and sum that with the current tile id * 2 and check if bit 2 is on. We don't use the first bit because we're skipping every second physics loop in any case. Depending on the result pick either to move left or right. This makes the water eventually fill everything it can.
water_left: ld a, ixl and 0xf jp z, physicsdone ld a, (ix-1) ; water+goo ; water+people eater cp 0 jp nz, physicsdone ld (ix), 0x80 ld (ix-1), 0x8f jp physicsdone
No surprises here, map border check and empty tile check. We'll fill in the other interactions alter.
water_right: ld a, ixl and 0xf cp 15 jp z, physicsdone ld a, (ix+1) ; water+goo ; water+people eater cp 0 jp nz, physicsdone ld (ix), 0x80 ld (ix+1), 0x8f jp physicsdone water_down: ld (ix), 0x80 ld (ix+16), 0x8f jp physicsdone
The rest should be familiar by now as well.
Handling collisions with water and the people eater is really repetitive so I'll only cover one case here:
cp 0 jp z, water_left_water cp 4 jr z, water_left_goo cp 11 jr z, water_left_peopleeater cp 12 jr z, water_left_peopleeater cp 13 jr z, water_left_peopleeater cp 14 jr z, water_left_peopleeater jp physicsdone water_left_water: ld (ix), 0x80 ld (ix-1), 0x8f jp physicsdone water_left_goo: ld (ix), 0x80 ld (ix-1), 0x85 jp physicsdone water_left_peopleeater: ld (ix), 0x80 ld (ix-1), 0x86 jp physicsdone
If there's empty space, move there. If there's goo, move there and turn into a gem. If there's peopleeater, move there and turn to stone.
physics_goo: ld hl, (physloopcount) ld a, ixl add l and 15 jp nz, physicsdone
For slowing things down, we take the tile number and add the physics loop count to it, and skip unless all 4 bottom bits are zero. This makes the goo really unpredictable, staying still for a long time and then rushing out.
ld a, ixl and 15 jr z, goo_right ld a, (ix-1) cp 0 jr nz, goo_right ld (ix-1), 0x84
Then we go through all four cardinal directions in the exact same way: check for map border, check if there's space, if all's ok just add more goo, otherwise check the next case.
Note that the goo won't touch water and turn into a gem. It may be slow, but it's not stupid.
The people eater consists of four separate tiles and thus four separate handlers; each of the handers does the same thing: check for map border, check if can move, move if you can, turn (into another tile) if you can't.
physics_peopleeater_left: ld a, ixl and 15 jr z, peopleeater_left_turn ld a, (ix-1) cp 0 jr z, peopleeater_left_move cp 4 ; goo jr z, peopleeater_left_move cp 1 jr z, peopleeater_left_eat peopleeater_left_turn: ld (ix), 0x80 + 11 jp physicsdone peopleeater_left_eat: ld a, 1 ld (playerdone), a ; fallthrough peopleeater_left_move: ld (ix), 0x80 ld (ix-1), 0x80 + 14 jp physicsdone
There's no movement delay. It's supposed to be scary.
As listed in our to-do, rocks and gems should fall through water. This was a bit tedious. There were tons of cases like this:
ld a, (ix-1) cp 15 jr z, dropdiagonal_left_ok1 ; water ok cp 0 jr nz, dropdiagonal_right ; can't move, stuff on the left dropdiagonal_left_ok1:
All of them follow pretty much the same pattern, though; wherever we'd check for zero, we now also check for water.
Splatting the people eater was curious. I went and implemented the handlers as expected..
cp 1 jr z, stone_splat cp 11 jr z, stone_splat_peopleeater cp 12 jr z, stone_splat_peopleeater cp 13 jr z, stone_splat_peopleeater cp 14 jr z, stone_splat_peopleeater
where stone_splat_peopleeater is the same as stone_splat, just without marking the player as done. Compiled, ran, tested, and... the people eater just slipped past the falling stone.
After looking at video capture frame by frame, I noticed that the falling stones would become inert mid-air for no clear reason. The reason was that the people eaters, which move on every cycle, would leave tiles with dirty flags on, and thus the falling rock would see that okay, there's something below me I don't recognize, so let's go inert.
This was simple enough to solve by adding an AND to mask off the dirty flag before checking against the tile numbers. We could do that everywhere we compare against tiles, but I don't think it's as needed elsewhere, because here moving stones are deadly while inert ones are not.
The expected result of doing more physics is that calculating physics takes more time. That's fine, we seem to have plenty of frame time left, and even if we'd peak momentarily to two frames, that's acceptable. Graphical glitches may occur near the top of the screen if a lot of tiles are updated; the fact that we draw our map bottom up actually makes this worse, as the last tiles to be drawn (and thus, later on the raster) are nearer to the top of the screen. It may be a good idea to revisit the map drawing and do it top down.
In any case, that should do it for physics, apart from the inevitable bugs that we'll squash as we come across them.
We still don't do anything special when player picks up gems, the exit door stays shut, and even if it were open, entering doesn't do anything. Level select doesn't let us pick between levels yet as we only have the one. And finally, we don't have sound.
This chapter's version of the source is available here.
Size-wise, we're at 5012 bytes, up 627 bytes. There's several things we could do smaller this time, as there were many repetitive things, which could be organized better. But sometimes getting things done is more important than how well they're made.
Any comments etc. can be emailed to me.