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