A Brief z80 Assembly Tutorial

Chapter 10

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.

Furthermore,

  • 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.

More Rapid Physics

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

(duh)

Water

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.

A World of Goo

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.

One Eyed Purple People Eater

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.

Stone and Gem Updates 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.