Jump to content

Assembly: programmatic sprite offset


rje
 Share

Recommended Posts

Each sprite in VERA has 8 bytes of configuration data.  OK.   VERA supports 128 sprites. OK.

 

GIVEN: the "sprite number" is in memory location $0002, CALCULATE the address of that sprite's configuration data in VERA.

 

PROCESS:

It looks to me like I want to count down to zero, incrementing the LSB as I go, and checking carry to increment MSB.

	        ; Set R8a and R8b to $fc10.
	        LDA $10
	        STA R8a
	        LDA $fc
	        STA R8b

	        LDX R0a   ; Load R0a "sprite num"

sprite_index_loop:      

	        ; loop management
	        CPX #0    
	        BEQ sprite_index_loop_done
	        DEX   
            
	        ; Do three ASLs on R8a. 
	        ASL R8a
	        ASL R8a   ; R8a = R8a x 8
	        ASL R8a
            
	        ; If carry set, then INC R8b.
	        BCC sprite_index_loop
	        INC R8b
	        CLC       
	        BCC sprite_index_loop

sprite_index_loop_done:
	


 

 

 

Edited by rje
Link to comment
Share on other sites

Posted (edited)

See, this is why I'm not a good assembly language programmer, and probably will never be a good one.

It required 16 lines of low-level logic to do this:

 

offset = $fc10 + sprite_num * 8;

 

I am a very lazy person.  I find it a bit tedious to have to write in assembly... I mean, I write a LOT of C when I write C, and it's easy to lose track of THAT code.  Imagine having to keep track of assembly...

 

On the other hand, assembly language can be a black box, which helps.  And I hope it forces modular thinking.

Edited by rje
Link to comment
Share on other sites

16-bit rotations are easily doable without loops because the bit that rotates “off” the register rotates into the carry flag, and the carry flag rotates into the register.

so say you have a 16bit unsigned variable held at myword, and you want to multiply by 4.

ASL myword

ROL myword+1

ASL myword

ROL myword+1

Do it a third time for myword  * 8.

ASL always shifts a zero into the LSB. ROL shifts the carry bit into the LSB. (that’s why it’s ASL on the low-order byte)

so to finish your formula, after the three shifts:

LDA #$10

CLC

ADC myword 

STA myword

LDA myword+1

ADC #$FC

STA myword + 1

This can be done more efficiently but I wanted to use the most straightforward methodology as the example.

Edited by ZeroByte
  • Like 2
  • Thanks 1
Link to comment
Share on other sites

Posted (edited)

OK, I think I grok that.  You're shifting the number directly, no loop needed.  And by doing a ROL with each shift, the carry is preserved (and there will only be one carry at most, so that's OK).

Thus by the time we're done ASLing and ROLing, we have the offset.  

Then you add-with-carry the base address.

 

Edited by rje
Link to comment
Share on other sites

Posted (edited)
3 hours ago, Guybrush said:

Or you could simply build a lookup table 😛

I could, that's true. 

There's probably a small table (say, 4 or 8 or 16 elements) that would cut the logic down a bit.  Like a binary search.  But the logic to make that happen might be more annoying still.  It would have to be clever.

Don't know.  I can't see it, and I wish I could.

I'll have to think about it.

Edited by rje
Link to comment
Share on other sites

I'm not sure what you're trying to do with sprites, specifically, but here's how I handled them in Flappy Bird:

each "object" (not in the C++ sense obviously) has an update function which writes out its current state to an array of shadow registers in main memory. Each object might use multiple sprites, and it returns a pointer to the next sprite slot once complete. So at the end of the update portion of the loop, I have an array of sprite registers in main memory. Since I wasn't using the settings in sprite reg6 (collision, layer, flip), I decided to use this as a marker in the shadow regs. The first unused sprite has FE in reg6. FF = clear sprite (i.e. wasn't on screen for the previous frame).

I have an end_of_frame() function that I call at the end of the main loop, which writes FE to the "current" sprite slot and then waits for vblank().

The IRQ routine simply blits the bytes from the shadow registers into VERA. It goes until it reaches a sprite with reg6 = FE or FF. If FE, it writes all-zeros for each sprite but puts FF for reg6. It continues until it encounters an already-clear sprite (FF) or the end of the array.

This essentially performs sprite multiplexing. During the game run, the bird, scoreboard, and pop-up banners are constantly being switched around between sprites, depending on the order I call their draw() functions. It's probably not the most efficient thing around, especially since it's written in C, but it works well enough, and the VBLANK() routine has next to nothing to do but mindlessly copy bytes into VERA.

Link to comment
Share on other sites

Posted (edited)

WOW, you're multiplexing!   Did you need to with 128 sprites available though??  In the C64 world, this was deep juju to me, involving interrupts during vsync or some such arcane nonsense.

 

So, I'm just putting sprites on the screen and move them around.  I don't need to multiplex.  I did this in BASIC, but I've never done it in C.  I've also never used Michael Steil's sprite ABI, which does the heavy lifting already.

 

If I use the sys() call, I can call his ABI directly.  All I need is the registers set correctly (there's a struct for that) and some of the pseudo-registers in zero page.  I think that's what I'll try.

 

Edited by rje
Link to comment
Share on other sites

I didn't do it because I needed to. I actually did it because I was lazy and because I wanted to use pointers in the hopes of not using any global variables like the sprite regs. (They ended up being global anyway for reasons).

I got tired of keeping track of sprite slots and every time I'd add something or change something, I'd have to go make more #defines and I got sick of it, so I just decided to do it this way. It was very convenient while messing around, because I could literally draw anything at any time I liked w/o having to keep track of stuff. I leveraged this the most by using the scoreboard code as debug displays. I just put 4 or 5 scoreboards on the screen and loaded the "scores" with values I was interested in watching in real time. I made the scoreboard able to show hex to help with this. It was a great "code reuse" exercise. And when I don't #define debug, none of them are drawn to the screen, and I didn't have to save sprite slots for them when not in use.

Also, it made the IRQ routine able to work faster by only needing to set the VERA data port once before striding straight through the sprite regs in order, without having to check anything or skip over un-altered sprites. It just writes bytes into the regs until it hits the first one that "used to be there but is gone now" and blanks them out until it reaches the first one that was already blank. When there's only one sprite on screen, it only has to consider two sprites (the one flagged FE is always going to be written as all-zeroes). Basically, if you put logic in a loop, that logic slows it down, and it can often be faster to just grunt 19*8 bytes into VERA than it would be to say "did this change? No? Did this change? No. Did this change? Yes? Let's see, that's sprite 9, so that sprite's register base is.... 0x1fc48. Okay, let's move the data port to 0x1fc48. Whew! Time to write 8 bytes." (Oh wait, you want me to think about which of those 8 bytes is changed, too?). If this code is too slow, then you can get tearing in your sprites if they're at the top of the screen. The Kernal's VSYNC routines alone will bleed dozens of lines into the display if the user is hitting several keys. (That's why priority #1 for my IRQ handler was to update the scroll register, by the way)

My "multiplexer" basically guarantees that all displayed sprites' data is contiguous from sprite 0 to sprite N, after which is nothing, so it can just write them all in one swoop.

Plus this flexibility was fun to play around with - I used it to make the bird be a trail of birds, for only like 3 or 4 lines of extra code.

I'm not saying this is the best way to do things - it's just what I came up with. My project was more about giving myself something sufficiently complex to do that I'd learn things along the way, but not so complex that I couldn't achieve it.

Link to comment
Share on other sites

For Asteroid Commander,  I'm doing something similar to what @ZeroByte did. The compass is a sprite, but it is one of 10 sprite images and may be flipped vertically or horizontally to give me the range of 30 images needed. It was simplest just to make a lookup table in RAM with 30 entries of 8 bytes each, and just copy the 8 bytes I needed into VRAM. The sprite images themselves are loaded elsewhere in VRAM. 

My subroutine to push the 8 bytes from the sprite attribute table in RAM, SPRA, into sprite #2 in VERA looks like this:

(Y contains a rotation index from 00 to 13 for pentagonal areas or 14 to 1F for hexagonal areas)

LDAY CORI; compass orientation table

TAX; X is SPRA table low byte

LDA# 02; compass is sprite #2

LDY# >SPRA; high byte of sprite attribute table 

STX0 60

STY0 61; 60/61 start address of 8 bytes to send to VERA

STA0 62

STZ0 63

ASL0 62

ASL0 62; NB max 128 sprites

ROL0 63

ASL0 62

ROL0 63

LDA# FC

CLC

ADC0 63

TAY; VERA address mid

LDX0 62; VERA address low

LDA# 11; step size 1 and VERA address high bit is 1

JSR VCH0; shown below

LDY# 00

LDA (60),Y

STA 9F23

INY

CPY# 08

BNE# F6

RTS

 

VCH0:

PHA

LDA# 00

BRAN 03

VCH1:

PHA

LDA# 01

STA 9F25

STX 9F20

STY 9F21

PLA

STA 9F22

RTS

 

Edited by Ed Minchau
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...

Important Information

Please review our Terms of Use