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.

Filtering by Tag: Assembler

Diary of a Game: Part 4 - Item Activation, Interpolation and... Bugs

Since my last update work has been a bit quieter post-ship and I enjoyed an unexpectedly low key Christmas break, so surely I've made a lot of progress, right? Well... life decided to intervene which resulted in a lot less time to work on the game than I had hoped. But I have managed to tick off a few things on the long to-do list...

Item Activation

The main - and, I must admit, only - playing facing addition is items that need to be activated before they can be matched. Activation occurs when a neighbour - regardless of colour - is matched and removed. I've reserved the 4th bit to indicate whether an item requires activation, which means I have 7 possible item types. So far I'm only using 4, so I think this will be enough. However, I should be able to take the 5th bit to give 15 item types (each with an unactivated variant) if need be.

The item bits at present are:

.const ITEM_TYPE_BITS = %00001111
.const ITEM_AWAITING_ACTIVATION = %00001000
.const ITEM_MOVING = %00100000
.const ITEM_DONT_DRAW = %01000000

.enum {
    ITEM_TYPE_NONE = 0,
    ITEM_TYPE_WHITE_BALL = 1,
    ITEM_TYPE_RED_BALL = 2,
    ITEM_TYPE_CYAN_BALL = 3,
    ITEM_TYPE_PURPLE_BALL = 4,

    ITEM_TYPE_PLACEHOLDER_1 = 5,
    ITEM_TYPE_PLACEHOLDER_2 = 6,
    ITEM_TYPE_PLACEHOLDER_3 = 7,

    ITEM_TYPE_NONE_UNACTIVATED = ITEM_TYPE_NONE + ITEM_AWAITING_ACTIVATION,

    ITEM_TYPE_WHITE_BALL_UNACTIVATED = ITEM_TYPE_WHITE_BALL + ITEM_AWAITING_ACTIVATION,
    ITEM_TYPE_RED_BALL_UNACTIVATED = ITEM_TYPE_RED_BALL + ITEM_AWAITING_ACTIVATION,
    ITEM_TYPE_CYAN_BALL_UNACTIVATED = ITEM_TYPE_CYAN_BALL + ITEM_AWAITING_ACTIVATION,
    ITEM_TYPE_PURPLE_BALL_UNACTIVATED =ITEM_TYPE_PURPLE_BALL + ITEM_AWAITING_ACTIVATION,


    ITEM_NUM_TYPES = 13
}

Interpolation

Up until a couple of months ago, the code to handle the sprite movement when the player fires an item was specific to that use case. Rather than having items jump immediately to their new positions when neighbours are removed, I want to utilise this sprite iterpolation to make it nice and smooth. So the first step in the process was to refactor the code into something more generic.

The resulting routines are super simplistic - the interpolation speeds are specified as pixels per update (whole pixels only), and the X and Y coordinates are interpolated at the same speed, which doesn't look great under some circumstances. I may revisit this in the future.

The following clip shows this code in action:

test_lerp:
    :test_push_lerp(0, 0, 100, 200, 1)
    :test_push_lerp(200, 0, 50, 200, 2)
    :test_push_lerp(20, 0, 70, 200, 3)
    :test_push_lerp(60, 0, 40, 200, 4)
    :test_push_lerp(90, 0, 120, 200, 5)
    :test_push_lerp(0, 0, 100, 200, 1)
    :test_push_lerp(200, 0, 50, 200, 2)
    :test_push_lerp(20, 0, 70, 200, 3)
    :test_push_lerp(60, 0, 40, 200, 4)
    :test_push_lerp(90, 0, 120, 200, 5)

!loop:
    :start_frame_update(1, !profiling)
    jsr item_lerper.tick

where the test_push_lerp parameters are the start X & Y, target X & Y and colour.

Bugs

While writing the activation and interpolation code, I ran into two separate bugs that had me scatching my head for an embarassingly long time.

The first occurred when matched items were being removed - occasionally the bottom left item would disappear when it shouldn't. I suspected this had something to do with the new code which finds any neighbours of an item being removed and activates them. The 64 Debugger was a huge help in tracking the culprit down, especially since it recently added the ability to read KickAssembler debug symbols. This is brilliant as it gives you your full source code along side the instructions:

Sure enough the bug was in the routine to find occupied neighbours - if there were no neighbours, I was neglecting to set the count of neighbours found to zero. This meant it would try to activate whatever items happened to be lying around in a buffer.

The second bug was considerably harder to find...

The Perils of Macros

KickAssembler has a rich scripting language to assist with creating code and data. For my first pass on the interpolation routines, I relied heavily on the scripting - particularly macros and pseudocommands. This made things like operating over a buffer of data structures very straightforward, for example:

.for (var i = 0; i < lerping_items_count; i++) {
    lda lerping_items[i].active
    beq !skip+

    :lerp(lerping_items[i].current_x, lerping_items[i].target_x, ITEM_DROP_SPEED, lerping_items[i].active)
    :lerp(lerping_items[i].current_y, lerping_items[i].target_y, ITEM_DROP_SPEED, lerping_items[i].active)

    :update_lerping_item_sprite(i)
    ....

But then I ran into a bug where some interpolating items would just disappear when a certain number had been queued. What made this particularly difficult to debug was the reliance on macros and pseudocommands. Because I was calling several macros, which in turn called other macros and pseudocommands, and all of this was wrapped in a for loop, the amount of actual code output was huge. I found stepping through all this code tedious and confusing - particularly since the issue only appeared towards the end of the interpolating items buffer.

The cause turned out to be a classic mixup between an address and the size of a buffer. In the routine to find a free interpolating item to use, I loop over all the items in the buffer to see if there is one that isn't active:

get_free_lerping_item_offset:
    ldx #0
!loop:    
    lda lerping_items,X
    beq !found_free+

    txa
    clc    
    adc #lerping_item_size()
    cmp #lerping_items_end  // <---- d'oh
    bcs !none_free+

    tax
    jmp !loop-
!found_free:
    rts
!none_free:
    ldx #255
    rts

lerping_items_end happens to be the memory address immediately after the lerping items buffer. What I wanted, of course, was to compare the current offset to the size of the buffer.

KickAssembler 5

As well as the ability to output full symbols/source for use in the C64 Debugger, there are a number of other nice improvements in KickAssembler 5. However, the changes to escape characters in strings broke the useful unit test framework 64spec. I've submitted a pull request that fixes these errors.

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.

Diary of a Game: Part 1 - Wha' Happen?

Man, what happened to the last couple of months?! In the first part of this series I mentioned that my work tended to be rather full on.. at that point I knew I was going to be spending the end of January traveling on business, but what I didn't know was that that would be immediately followed by another, longer business trip. I had a whole 4 days at home during February, and not a single weekend. Work-wise it was a very productive and beneficial time, but it did mean that other things slipped through the cracks.

Despite that I did manage to make a bit of progress on the game. The items/balls/bubbles/whatever now fall down and pile up correctly. I'm still just using characters - I want to get the core game state update functional before worrying about making things look nicer.

In order to use characters and get alternating rows offset correctly I'm using a series of raster interrupts to modify the horizontal scroll value. In the video below, you can see how it looks before these interrupts start running, and once they are the coloured lines show the amount of raster time they take (which is pretty long due to them occurring on "bad lines").

The flashing border colours at the top of the screen show the raster time taken for various bits of the game tick routine. The black border is the routine that updates all the cells of what I call the board - each cell in the board is either empty or contains an item. The white shows the time spent re-drawing the board and its cells.

betterrandcapture2.gif

Yes, this is pretty terrible performance wise right now. My philosophy with coding is to do the easiest, quickest thing first and then step back and evaluate. I'm not smart enough to plan entire systems out ahead of time - in my experience once you have something doing what you originally thought you wanted, you often realise that it sucks or that there are issues that you hadn't even thought of beforehand. Being able to iterate rapidly on the things that matter is important.

Random Problems

Speaking of going with the quickest, easiest implementation first and terrible performance... This was my initial super basic pass at a random number generator:

random_num_in_range: 
    sta rand_hi

!rand_loop: 
    jsr random_num 
    cmp rand_hi 
    bcs !rand_loop- 
    rts

random_num: 
    lda $d012 
    eor $dc04 
    sbc $dc05 
    rts

rand_hi: .byte 0

The random_num routine was found on this lemon64 thread.

random_num_in_range took a value in the A register and generated a random number between 0 and that value - 1. This allowed me to quickly get to work on the game update routines, but every few frames it would take a very, very long time. The problem, of course, is how it makes sure the result is within the range the caller wanted. In this case it just kept on trying until it generated a number within the desired range - and when that range is small (in this case I was looking for a value between 0 and 5 for the column, and 1 and 4 for the colour), chances are it's gonna have to try a lot of times.

The typical approach for this kind of thing is to take the modulus of the random number with the upper value (e.g. for a number between 0 and 5 in C, you would do rand() % 6). I was too lazy to implement a general modulus routine (and I suspect it may be heavier than I would want) so I went with this:

.macro random_high(high) {
    jsr random_num
    .var next = pow(2, ceil(log(high)/log(2))) - 1
    and #next
!shift_loop:
    cmp #high    
    bcc !fine+
    beq !fine+
    sec
    sbc #high
!fine: 
}

Thanks to Kickassembler's very nice language features, I calculate the power of 2 value that's either equal to the passed in parameter or the next highest. Then I can subtract 1 to get a bitmask (e.g if the value passed in was 8, then the bitmask would be 7, i.e. %00000111), use the logical and, and if that result happens to be out of range then I just subtract the high value. Once again that'll give me a number between 0 and high- 1.

The problem with this is that if you want, say, a number between 0 and 5, then that's a fraction of the possible numbers returned by the random_num routine (which is a byte, so 0-255). So the distribution of numbers in the desired range is highly dependent on just how good your random number generator is.

This video shows the result of using the original, super cheap & basic routine.

badcapture2.gif

Not ideal, eh? A lot of repetition, with some values hardly ever getting chosen.

In the end I decided to go with the RNG described here.

random_num:
    lda seed
    beq doEor
    asl
    beq noEor //if the input was $80, skip the EOR
    bcc noEor
doEor:
    eor #$1d
noEor:
    sta seed
    rts
seed: .byte 0

Thanks to that you can see the much better distribution in the first video in this post, and it still manages to do it with a low cycle count.

For a much more in depth look at RNGs in 6502, then this is well worth checking out.

What Next?

Obviously an important part of a "match 3" style game is the matching, so I've been working on that. This has been a somewhat humbling exercise so far, as I started it thinking I would knock it out in no time, but then the reality of only having 1 general purpose register, 2 index registers and a very limited set of addressing modes dawned on me yet again. With that said, I've made some good progress and am hopeful it'll all be working nicely in time for the next post.