Search code examples
assemblyartificial-intelligence6502c646510

Enemy bullets targeting player on a C64


I'm scanning the internet and old C64 books for the question without finding an answer, so in the end I just had to post it here. I love the good old times of C64 coding and while I'm not currently programming a game on this platform I would like to know how some hardware limitations were overcomed at that times.

In all modern game programming books and tutorials the way to find the right direction to launch enemy bullets towards the player is to use vector math with floats, more or less like in this pseudo code:

bullet_velocity = (player.position - bullet.position).normalize();

Now, taking into account the C64 limitations, the massive use of sine tables for speed I saw in source codes and, maybe I'm distracted, but I never saw a word about vector math when reading old C64 books or commented programs from C64 programmers, I'm wondering how the same goal was obtained at the times.

Please answer, I have thousand of doubts like this, but answering this question maybe I can find myself a response for the rest! :-)

EDIT: Just a note: examples of C64 games with bullets targeting players are Silkworm and Cybernoid.


Solution

  • Supposing you're happy with a sufficiently small number of output directions, it's easiest to do via a lookup table. E.g. for 64 output directions, grab the vector (x, y) from source to destination, and if both are positive then shift both left until one of them fills the sign bit, then form a four-bit table index from the top two bits of each and look out the output vector.

    Assuming you're at 160x200 then I guess you'll need to throw away a bit of precision before going in.

    Mirror appropriately to deal with the other quadrants. Assuming 8.8 fixed point for object locations and a bullet velocity of 1 then that's a 32-byte lookup table.

    So, naively, something like:

    xPosYPos:
    
        ; assuming ($00) has the positive x difference
        ; and ($01) has the positive y difference
    
        lda $00
        ora $01
    
    shiftLoop:
        asl $00
        asl $01
        asl a
        bpl shiftLoop
    
        ; load A with table index
        lda #$00
        rol $00
        rol a
        rol $00
        rol a
        rol $01
        rol a
        rol $01
        rol a
    
        ; look up vector
        tax
        lda xVec, x
        ; ... put somewhere ...
        lda yVec, x
        ; ... put somewhere ...
    

    ... with a smarter solution probably involving something more like:

        lda $00
        ora $01
    
        asl a
        bmi shift1
        asl a
        bmi shift2
        ... etc ...
    
    shift1:
    
    ... etc, but you can shift directly to form
    the table index rather than going to all the work
    of shifting the vector then piecing together from
    the top bits ...
    

    Or you could create a 256-byte lookup table to look up a routine address based on x|y (which will always be at most 127 because they're both positive) and jump directly to the shifting without counting out bits.


    Primer on object locations and fixed point:

    Assuming you're in 160x200 mode then you can store each component of an object's location as a single byte. So one byte for x, one byte for y. What many games do is instead store each location as two bytes. So four bytes in total for x and y.

    One of those bytes is the same as in a single byte format — it's the integer position. The other is a fractional part. So if you have a position and a velocity (and Euler integration) then you do a 16-bit addition of velocity to position. Then you just use the top byte for position.

    This is usually called fixed point arithmetic. Unlike floating point, the location within the integer where the decimal point rests is fixed. In the scheme described here it's always eight bits in.

    So, e.g. to add an offset with just byte quantities:

    clc
    lda xPosition
    adc xVelocity
    sta xPosition
    
    sta SomeHardwareRegisterForSpritePosition
    

    To add an offset with a fixed point scheme:

    clc
    
    lda xFractionalPosition
    adc xFractionalVelocity
    sta xFractionalPosition
    
    lda xPosition
    adc xVelocity
    sta xPosition
    
    sta SomeHardwareRegisterForSpritePosition
    

    The advantage is that your velocity vector can now be as small as 1/256th of a pixel in any direction. So e.g. you can store a velocity that says that each frame your bullet will move one pixel to the left and 32/256ths of a pixel down. And all it costs to move that bullet with subpixel precision is an extra couple of bytes of storage per vector and an extra couple of ADCs.

    With the above suggestion you'd get the vector from source to destination by subtracting one byte of one from one byte of the other. The result would be a two single bytes, both of which would be the fractional parts of the output. So e.g. you might decide to fire off a bullet with a vector of (87/256, 239/256), i.e. an angle of 20 degrees.