Ziggurat

Occasional ramblings on games, generally retro related

​When my parents brought home a ZX81 one day (complete with wobbly 16K RAM pack, of course) I discovered the joy of programming. But it wasn't until I got my hands on a ZX Spectrum that my obsession with games really began, which continued with the C64, Amiga, right through to this day. The 80s and early 90s were an amazing time for games, not just for the games themselves but for the fascinating people behind them - it was truly a time of pioneers and creativity.

I myself have spent the last (almost) 20 years working in the games industry on all manner of platforms, most recently iOS. Ziggurat Development Ltd is my company here in NZ that provides contract programming services.

Diary of a Game: Part 3 - Sprites, Character Animation and Input

Good grief, in my mind my last update was 3, maybe 4, months ago... but May?! Well, I guess I've had a decent excuse.

Despite being embarassingly tardy with writing updates, some progress has been made. This is the current state of the game:

20181015.2018-10-15 10_54_13.gif

Sprites

I've continued to stick with the C64's standard character mode - I may well switch to the multicolour character mode, but a character approach seems to be working well. In order to draw the overlapping rows of items on the board, I just change the horizontal scroll value every 8 lines which means I don't have to faff about with creating a bunch of extra in-between chars. The downside to this is that as items/balls/whatever fall down, they jump left and right.

Sprites aren't affected by the scroll registers, so now when an item drops from the top what you're seeing is a sprite rather than a character. Setting up the sprite code was straightforward and as always I highly recommend checking out 64bites.com.

The first step was to be able to position sprites over any of the board's cells. The way the logic for the items works is similar to a cellular automaton, in that each board cell looks at its contents and the contents of its neighbours, and then decides what the next state should be. This means that things move in character sized jumps. So once I was able to cover any item with a sprite as it dropped down, I could start on making this look nice and pixely smooth.

Roughly, the way it now works is:

  1. Create an item at the top, hide it and replace it with a sprite
  2. Tick the item (does it drop down or is it blocked?)
  3. If it's position did change, start a linear interpolation between the current sprite position and the item's new position.
  4. Repeat the interpolation until the sprite has arrived.
  5. Repeat step 2.

Currently this is only used for the item that is fired from the top, but one of the things on my todo list is to expand it to include items when they collapse.

Character Animations

One of the most interesting things I've found while working on this is just how overly confident I am that I know what I'm doing when when approaching certain straightforward tasks. It ended up taking me about 6-8 hours split over several nights to get a simple system for animating arbitrary characters integrated... so many times I was sure I had the answer only to find it was a deadend. To be fair, the difficulty is not that I didn't know what was needed, it was taking it and cramming into the constraints of the 6510.

So I've ended up with an animation pool that take animation sequences and then play them appropriately.

The animation pool along with an example sequence (used when items are matched & removed) looks like:

.macro animation_frame(char, frame_dur) {
  character: .byte char                // character to display
  frame_duration: .byte frame_dur    // how long to show it for
}

animation_sequence3: {
  num_frames: .byte 6
  animation_frame1: {
      :animation_frame(CIRCLE_FILLED_CHAR, 3)        
  }
  animation_frame2: {
      :animation_frame(CIRCLE_OUTLINE_CHAR, 3)        
  }
  animation_frame3: {
      :animation_frame(ASTERISK_CHAR, 3)
  }
  animation_frame4: {
      :animation_frame(PLUS_CHAR, 3)        
  }
  animation_frame5: {
      :animation_frame(PERIOD_CHAR, 3)
  }
  animation_frame6: {
      :animation_frame(BLANK_CHAR, 1)        
  }
}

animation_pool: .for(var i = 0; i < ANIMATION_POOL_SIZE; i++) {
    animation_item: {
        loop_count: .byte 0         // how many loops left - 255 = forever
        dest: .word 0                // screen offset for where to draw
        character: .byte 0            // current character
        current_frame: .byte 0        // current frame in the sequence
        frame_time_left: .byte 0    // how long left for the current frame
        sequence: .word 0            // pointer to the sequence data
        flags: .byte 0                // flags for the anim
    animation_item_end:
    }
}
animation_pool_end:

This gives me a nice, flexible system which I can use to animate any character on the screen. I've been a bit sloppy with my memory usage, but if I run out of RAM there is some easy low hanging fruit to tackle (famous last words!).

Input

Player input is super, super simple in this game - it's literally just one button. For the time being I'm only reading the spacebar state, but I will extend that to joystick buttons and possibly other inputs. Consequently the code is straightforward:

.macro update_spacebar_state(previous_spacebar_state, spacebar_state) { 
    lda spacebar_state 
    sta previous_spacebar_state

    lda #%01111111              // check for space bar pressed
    sta $dc00 
    lda $dc01 
    and #%00010000 
    sta spacebar_state
}

Even though I'd been using raster interrupts to trigger the scroll register changes since the early days, I was still always making sure to call the system IRQ handler when I was done. You can see just how heavy this default handler is in this clip where it screws up drawing when the spacebar is pressed:

However I'm pretty sure I don't need that kernal functionality, so now I skip the system handler. Just make sure to restore the registers when you exit your handler, e.g:

.macro scroll_irq_last(scroll_value) {
  // update scroll register
  lda screen_control_register2
  and #%11111000
  ora #scroll_value
  sta screen_control_register2

  // acknowldge the interrupt
  lda #%00000001
  sta vic2_interrupt_status_register

  // restore the registers 
  pla 
  tay 
  pla 
  tax 
  pla 
  rti      
}

What Next?

Top of the todo list is special items. Unlike the regular coloured balls, these will have particular behaviours like bubbles that disappear when a neighbouring cell becomes empty, or ones that can't be removed by matching but turn into regular balls when directly hit.