Oldschool Gaming - reviewing new games on classic computers
The Hex Files :: Part 9 :: written by Jason Kelk :: added 01 Jul 2007

Ah, there you are. I wondered where you'd got to. Ready to start playing with that new project I promised last issue then? Good, lets get on with it, shall we? We're going to write a little game. Don't worry, it's not going to be Armalyte or anything complex like that, we're just going to move a few sprites around for a starter. As usual, there is some source on the covermount, the filename is game_1.asm, so fire up your text editor and load it in. The majority of the source is variations on routines we've covered in the past so you should be happy with them, so instead of dissecting the entire program a line at a time I'm just going to cover the points of interest.

So, starting from the top of the source and working down, the first stop is a routine called makespr which fills the last sprite of bank 0 ($3FC0 - $3FCF) with $FF to make it a solid block. This is just a temporary measure, eventually there will be real definitions but for now we just want to see where the sprites are. A little later on in the source (after the raster setup) is a loop called setsdp that points all eight sprites to $2800 for their data.

Right, now we have to set everything up. First, a quick call to xpand (which we'll cover in more detail soon), which is a routine that sets all the sprite positions up. Then comes something we've not used before:

		lda #$00
		sta sync
strtwait	cmp sync
		beq strtwait

Now at first sight this seems totally pointless, doesn't it? If sync is set to $00 it's not suddenly going to change whilst we're sitting in a loop ... Well, actually it is because the second raster split, which is at line $FC, sets sync to $01 once a frame, this routine actually synchronises the runtime code up with the raster, which is where the label gets it's name. After that we read $D01E, which is the sprite to sprite collision register, but we don't do anything with the values since we're merely reading it in order to clear it for when the game starts.

Next up is the main loop of the game and it uses another loop to wait for sync to get the game movement synchronised to the raster interrupt; then we have three calls to subroutines, joyread reads the joystick in port 2, sprmove moves the sprite data and, again, xpand puts the sprite data into the registers. After the main processes we have another read of $D01E but this time we're going to actually be doing something with the data. As with other sprite registers, $D01E represents each sprite with a bit, there are eight sprites and eight bits just like the sprite enable register $D015 (covered in part 4) but these bits become set if two sprites collide. So if sprites 0 and 1 bump into each other, $D01E is set to $03 (sprite 0 being the first bit and having a value of $01, sprite 1 being the second bit and representing the value $02) and if sprites 0 and 7 have a pile-up, $D01E reads $81.

There is a problem with $D01E though; if sprites 0 and 3 hit each other and at the same time sprites 1 and 6 have a prang, we can only see that sprite 0 has hit at least one of the others, not which one(s). In fact, if all four sprites hit each other the value in $D01E will be the same as when they collide as two pairs so whilst that's not a problem for what we're doing right now (since we just need to know if sprite 0 has hit something) later on it will get in the way so this is only a temporary measure and we'll be dropping the use of hardware collisions fairly soon.

Anyway, back to the source and after that read from $D01E we have a new command called LSR, which means Logical Shift Right. LSR moves all the bits in a byte down one, so the highest bit, the one that represents 128 moves down to the 64 position, the 64 to the 32 and so forth. The lowest bit, the one representing 1, falls off the end and into the carry flag and the highest bit is left un-set by this operation. LSR has a number of uses, not least of which is that it basically divides any 8 bit number by two. In our code we have an LSR A which is C64Asm's way of saying that the Logical Shift Right will happen to the value in the accumulator, but LSR $4000 will work just as well and perform the operation on the appropriate byte in memory.

But why are we using LSR here? Well, it's a quick and dirty way of checking if the lowest bit of $D01E (the one representing sprite 0, our "player" sprite) is set. As I said, that low bit will fall into the carry flag and the BCC (Branch on Carry Clear) simply causes the program to move back to the label main if the carry flag is empty after that operation. If the carry isn't empty that means sprite 0 is touching another sprite, so we do a quick INC $D027 to change the sprite colour for now to indicate that a collision has happened and then head back to main again.

The next part of the source is just the stock raster routines we've covered before, with raster2 setting sync to let the runtime code know it's time to start work before it calls $EA31. Then we arrive at xpand... this is the most complex routine in the entire piece of code, it takes the sprite co-ordinates from a table called sprtpos and puts them into $D000 onwards, but it does a little trickery to make handling the MSB easy for the rest of the program as well. This needs a complete breakdown of the code, but I need to cover two more new commands before that, ASL and ROR.

Accumulator Shift Left, or ASL to it's friends, is basically the reverse of LSR in that all the bits of a byte move up a place; the 1 bit is left un-set and the 128 bit gets nudged into the carry. And, since it's the opposite of LSR, it can also be thought of as multiplying an 8 bit number by two (although anything over $7f will need the ninth bit in the carry taken care of to get a correct answer). ROR, or ROtate Right, is almost the same as LSR, except that the the previous contents of the carry get pushed into the 128 bit before the other end falls into it. Both commands can work directly to the accumulator or to memory as with LSR. (There is a fourth command to this set, ROL, which works like ASL but has the contents of the carry pushed onto the end like ROR and we'll come across that at another time.)

Okay, so now for a line by line look at xpand. the sprtpos table contains sixteen bytes and they are stored as sprite 0's X and Y, sprite 1's X and Y and so forth, the same order as $D000 onwards uses. The first couple of lines just copy sprtpos+1 (the first sprite's Y position) straight into the Y register:

xpand		ldx #$00		We know this...

xpndloop	lda sprtpos+$01,X	Read the Y co-ordinate
		sta $d001,x		Set it into the sprite Y position

But the next bit to read sprtpos+$00 (the first sprite's X position) is a little more fiddly:

 		lda sprtpos+$00,x	Read the X co-ordinate
 		asl a			Multiply by 2

Here we are using a trick that the Commodore designers came up with ages ago (the same system is used for the X position of the light pen register). Because the screen is 320 pixels across we have to use the MSB (as explained in issue 10) but MSB handling is fiddly at the best of times. So we're taking the value in SPRTPOS and multiplying it by two using the ASL. But that doesn't actually sort out the MSB, does it? So...

		ror $d010		Roll $d010 a bit to the right

...we move the contents of the carry into the top of the MSB (at the 128 position). Because the loop runs eight times the first bit in ends up in the lowest position, the second at the second lowest and so on, until the bit that needs to represent sprite 7 is at the highest position. Then we...

		sta $d000,X		Write to the sprite X position

...write back the contents of the accumulator to the sprite. Finally we manage the loop:

		inx			Just a standard loop counter, but
		inx			we're going up in steps of 2...
		cpx #$10		...until X reaches $10.
		bne xpndloop

And to finish, we count up in steps of 2 until we reach $10 and have, therefore, run the loop eight times - since two bytes are transferred each iteration of the loop, that's all 16 bytes of sprtpos transferred during those eight passes. This loop may seem a little complex, but MSB handling is a tricky job anyway and although it sacrifices some of our movement control horizontally, this technique means that from here onwards all sprite X co-ordinates are just a value from $00 to $FF so we can do quick and simple mathematics to them in order to shift the sprites; a simple INC to the first byte of sprpos once a frame will move the first sprite right all the way across the screen with no extra work needed. There are situations where it's necessary to make things move at a single pixel a frame, but for general useage, this goes and if it's good enough for Commodore's design team it's good enough for us!

After the xpand routine comes joyread, a routine to scan the joystick. First up is a read from $DC00 and we're using LSR again to move the bits off into the carry one at a time to see what state they're in. $DC00 actually works in reverse, if the first bit is clear that means that the joystich has been pushed up so our sprite needs to react. How do we test that? With a Branch on Carry Set (BCS) command; if the carry is set after the LSR then the stick isn't being pushed up and we branch over the routine that moves the sprite up so that it doesn't move that way. The same goes for the other directions and then fire. The five joystick bits represent (lowest to highest) up, down, left, right and fire.

So we move the sprite up like this:

		ldx sprtpos+$01		Read the sprite Y position
		dex			Subtract four

		cpx #$32		$32 is the top line of the screen
		bcs setup

BCS again? Well yes, due to the way the 6510 does mathematics, BCS will act as a "greater or equal to" command here; if the contents of the X register are greater than or equal to $32 it'll branch to setup.

		ldx #$32

Since this will only happen if X is less than $32 it makes sure X will always be $32 or greater, so not into the upper border.

setup		stx sprtpos+$01		Store the X position back

The BASIC equivalent to the CMP, BCS and LDX there is IF Y<50 THEN Y=50. The down works in a similar manner, except that it's adding to the X register with four INXs and uses a BCC to branch over the LDX #$E5 (the lowest position a sprite can be at without being under the lower border) since BCC only works as a "less than" when used after the compare, not "less than or equal". (These tricks with BCC and BCS work after any compare command, not just those for the X register and can be very useful.) Again, left and right are the same basic block of code as up and down, except that it has different stop positions for the edges of the screen and, because the X position of the sprites gets multiplied by two when they're displayed by xpand, the X position is only changed by 2 rather than 4.

Okay, we have one final loop to look at, sprmove takes the contents of sprtpos+$04 (sprite 3's X position) and adds the contents of another table called sprtspd to it, then goes through the sprite positions until it does sprite 7's Y position. Since sprtpos contains the present positions of the sprites and sprtspd contains their "speed" settings, the value that is added to each X or Y co-ordinate in turn to make the sprites move. This sprtspd table uses some trickery, adding $FF, for example, to a byte will actually cause the byte to "wrap" around so the value goes down by 1. This is where xpand really comes into it's own, instead of having to know which direction the sprite is moving and have a routine to handle it we just perform an add! Sneaky, eh?

Righto, that's your lot for this installment, but next time we'll add some new features to our game, like some better collisions, an ingame soundtrack and some nice graphics. Load up game_2.asm to get the little game-ette going with a sprite and some backing music (as per how we did these things with the demo previously, so you should be able to work out for yourself how). As always, contact me with any questions and I'll see you next time, matey.

The source code for the routines above can be downloaded here for easier reference.

Content copyright © 2004-2014 Oldschool Gaming     Designed and hosted by Enisoc Design