Jump to content
IGNORED

Techniques for drawing player sprites


Recommended Posts

Hey everyone,

I'm currently following a fantastic tutorial by Spiceware, and I've reached step four, which is linked below. In this step, Spiceware mentions a method, or perhaps more accurately, a technique for drawing player sprites called DoDraw. He also provided a link to a thread where this technique was introduced.

The maybe-creator of DoDraw, a user named vdub_bobby, has also discussed the pros and cons of three other methods in that linked forum thread: SwitchDraw, SkipDraw, and FlipDraw. As someone who learns best by seeing how things work, I've tried to research these methods both on this forum and through Google. However, despite finding mentions of these techniques by various homebrewers, I'm struggling to find concrete information on what they actually entail.

So, I was hoping that some of you could shed some light on this subject for me. Any insights would be greatly appreciated.
Thanks!
Elijah

 

  • Like 1
Link to comment
Share on other sites

I'm also new-ish to Atari 2600 programming, and while I've seen a few names for drawing routines being thrown around, I hadn't really looked too deeply into the implementation details (I've been working on something quite different), and I thought it was time to correct that. I've had a rummage around and put what I found into this one post, with explanations and code samples. And what a rabbit hole it turned out to be! I found lots of posts full of discussions and alterations but very few examples of "original" or "complete" code to help make sense of it. There were also plenty of links to links to further links which turned out to be broken, or missing attachments which contained the actual information, and the occasional example which had been typed from memory with so many errors that it was no longer clear how it could have worked in the first place. So everyone, please forgive me if I get some things wrong, and correct me if you know better.

 

NOTE: I haven't actually tried assembling and running these yet, so there may be some errors. I'll try to give them a go (and correct any mistakes) soon.

 

Almost everywhere that a static SPRITE_HEIGHT definition has been used, you could of course instead get some other, dynamic value into that register beforehand, for a few extra cycles.

 

I've tried to give the names/pseudonyms of the people who generally seem to be credited with first documenting these approaches.

 

 

SkipDraw - Thomas Jentzsch
There are a few versions of this, so I'll go through each seperately. Since FlipDraw and DoDraw are both evolutions of SkipDraw, I've included them here.

 

Vanilla SkipDraw:
This is nice because we don't need to set up specially offset pointers as with many other routines, and also it cleans up after itself (it blanks the player pattern), so we don't need to store an extra $00 byte at the end of the pattern data in ROM. Has a constant runtime of 22 cycles.

 

The X register is a scanline counter, decreasing as it moves down the screen.
Sprite_Y is a value in RAM, which stores the number of the TOP (first) scanline where the sprite should appear.
SPRITE_HEIGHT is the height of the sprite in scanlines (static).
Sprite_Pattern_Pointer is a RAM address which contains a pointer to the appropriate data in ROM.

 

    txa            ; 2 Copy the current scanline into the A register
    sec            ; 2 (May or may not be necessary depending on previously executed code)
    sbc Sprite_Y        ; 3 If Sprite_Y >= current scanline then we might draw, and carry = 1
    adc #SPRITE_HEIGHT-1    ; 3
    bcc .skipDraw        ; 2/3 If we need to draw, the SBC would've taken us "below 0", and the ADC would've brought us "back above 0", setting carry = 1
; (12 cycles)

    tay            ; 2 Transfer A (which is now the current sprite line, counting from 0, top to bottom) to Y
    lda (Sprite_Pattern_Pointer),y    ; 5
    sta GRPx        ; 3
; (total 22 cycles)
.continue:
; (final total 22 cycles)
    ...
; Rest of kernel
    ...
.skipDraw:
; (13 cycles)
    lda #0            ; 2 Load a blank sprite pattern
    sta.w GRPx        ; 4 Force absolute addressing to use 4 cycles instead of 3, for timing
    beq .continue        ; 3 Always true because we just loaded an immediate 0
; (total 22 cycles)

 

Illegal SkipDraw:
This one has a constant runtime of 18 cycles, but in order to achieve that, a specially offset pointer must be calculated before starting to draw. Depending on how large you let your scanline counter get, this can push your sprite data out of more than half of each ROM page.

 

The Y register is a scanline counter, decreasing as it moves down the screen.
Sprite_Y is a value in RAM, which starts out before drawing with the number ONE ABOVE the TOP (first) scanline where the sprite should appear, and is decremented every scanline.
SPRITE_HEIGHT is the height of the sprite in scanlines (static).
Sprite_Pattern_Pointer is a RAM address which contains a pointer to the appropriate data in ROM. It must be calculated at the start of each frame to point at an address Sprite_Y bytes BEFORE the actual desired data ENDS (it will be read from highest address to lowest address), and this calculated address must still be on the same 256-byte page, or the LDA[indirect indexed] instruction will take an extra cycle to complete and throw the timing out of alignment.

 

    lda #SPRITE_HEIGHT    ; 2
    dcp Sprite_Y        ; 5 [Illegal opcode: DEC and CMP] Is 0 <= Sprite_Y <= SPRITE_HEIGHT?
    bcc .skipDraw        ; 2/3 True = No
; (9 cycles)

    lda (Sprite_Pattern_Pointer),y    ; 5
    sta.w GRPx        ; 4 Force absolute addressing to use 4 cycles instead of 3, for timing
; (total 18 cycles)
.continue:
; (final total 18 cycles)
    ...
; Rest of kernel
    ...
.skipDraw:
; (10 cycles)
    lda #0            ; 2 Load a blank sprite pattern
    sta GRPx        ; 3
    beq .continue        ; 3 Always true because we just loaded an immediate 0
; (total 18 cycles)

 

Magical 17-cycle SkipDraw:
I've seen several comments to the effect that SkipDraw can have a runtime of 17 cycles, but I didn't find a complete example of it in code. I wracked my brains until I came up with this version, but I don't know if this is how it was done originally. I had to change the indirect indexed pattern load to an absolute indexed load. That means that you're either stuck with a single, unchangeable sprite graphic, or you have to copy this code to RAM and modify it at runtime, or you have to buffer the graphic in RAM before the active drawing period. The first is annoying, the second would mean storing your whole kernel in RAM to keep the savings, and the third could be problematic, given the necessity of using potentially large address offsets. None of them seem worth the single cycle to me.

 

(The implementation comments for "Illegal SkipDraw" above apply equally here.)

 

    lda #SPRITE_HEIGHT    ; 2
    dcp Sprite_Y        ; 5 [Illegal opcode: DEC and CMP] Is 0 <= Sprite_Y <= SPRITE_HEIGHT?
    bcs .dontSkipDraw    ; 2/3 True = Yes
; (9 cycles)
    lda #0            ; 2 Won't interfere with the carry flag
    bcc .continue        ; 3 Always true
.dontSkipDraw
; (10 cycles)
    lda Sprite_Pattern_Pointer,y    ; 4
; (total 14 cycles)
.continue:
    sta GRPx        ; 3
; (final total 17 cycles)

 

FlipDraw version by Manuel Rotschkar:
I couldn't find much information on this one. It may be called FlipDraw because Sprite_Y "flips" from being a scanline counter to being a sprite pattern index. What I found was one mailing list post that was probably meant to be incorporated into another routine as modifications. I also found some source code written by others using essentially a fixed version of the mailing list code on its own, which didn't have a constant runtime. Manuel said that they used it in a "Worm Whomper" demo, but I couldn't get the source, and I wasn't able to find anything that looked like this in a ROM I found, when using Stella in debugger mode. What I've recreated here looks like an interesting idea and should work well, and it doesn't need any special pointers, or separate scanline counter.

 

The scanline counter Sprite_Y counts down, and once it's within range it can be used as a straight index into sprite data.

 

SPRITE_HEIGHT is the height of the sprite, in scanlines.
Sprite_Y is a counter in RAM, which starts out before drawing with the number of the BOTTOM (last) scanline where the sprite should appear.
Sprite_Pattern_Pointer is a RAM address which contains a pointer to the appropriate data in ROM.

 

    lda #SPRITE_HEIGHT    ; 2
    dcp Sprite_Y        ; 5 [Illegal opcode: DEC and CMP] Is 0 <= Sprite_Y <= SPRITE_HEIGHT?
    bcc .flipDraw        ; 2/3 True = No
; (9 cycles)

    ldy Sprite_Y        ; 3 Use Sprite_Y as the index for loading pattern data
    lda (Sprite_Pattern_Pointer),y    ; 5
; (total 17 cycles)
.continue:
    sta GRPx        ; 3
; (final total 20 cycles)
    ...
; Subsequent kernel code here
    ...
.flipDraw:
; (10 cycles)
    nop            ; 2 Waste 2 cycles for timing
    lda #0            ; 2 Load a blank sprite pattern
    beq .continue        ; 3 Always true because we just loaded an immediate 0
; (total 17 cycles)

 

DoDraw version by vdub_bobby:
Still has a constant runtime of 18 cycles, and still needs a special sprite pattern pointer, but only takes up 14 bytes as opposed to 19 (for Illegal SkipDraw). It's also less prone to timing errors due to page boundaries being crossed, since it doesn't need to branch very far.

 

The Y register is a scanline counter, decreasing as it moves down the screen.
Sprite_Y is a value in RAM, which starts out before drawing with the number ONE ABOVE the TOP (first) scanline where the sprite should appear, and is decremented every scanline.
SPRITE_HEIGHT is the height of the sprite in scanlines (static).
Sprite_Pattern_Pointer (and Sprite_Colour_Pointer, if used) is a RAM address which contains a pointer to the appropriate data in ROM. It must be calculated at the start of each frame to point at an address Sprite_Y bytes BEFORE the actual desired data ENDS (it will be read from highest address to lowest address), and this calculated address must still be on the same 256-byte page, or the LDA[indirect indexed] instruction will take an extra cycle to complete and throw the timing out of alignment.

 

    lda #SPRITE_HEIGHT    ; 2
    dcp Sprite_Y        ; 5 [Illegal opcode: DEC and CMP] Is 0 <= Sprite_Y <= SPRITE_HEIGHT?
    bcs .doDraw        ; 2/3 True = Yes
; (9 cycles)

    lda #0            ; 2 Load a blank sprite pattern
; By not branching, we hit the following BIT[absolute] instruction (4 cycles) which consumes or "skips" the following 2 bytes (the subsequent LDA instruction), so the A register remains set to 0
    .byte $2c        ; -1 (4 - 5) This is the BIT[absolute] opcode
.doDraw:
; (total 10 cycles)
; By branching to here, we execute this LDA instruction normally, and the A register is set to the sprite's graphics pattern
    lda (Sprite_Pattern_Pointer),y    ; 5
    sta GRPx        ; 3 Both branches continue from this point as usual
; (final total 18 cycles)

 


SwitchDraw - Thomas Jentzsch

Possibly named because it uses one main variable to track its progress, which "switches" between counting down to begin drawing, and counting down to finish drawing, at the appropriate scanlines. At first, it simply compares the desired scanline to the scanline as it counts down. When it reaches the appropriate scanline, it switches the comparison value from the starting scanline to (the ending scanline - 128). Because the CMx instructions essentially perform a subtraction without storing the result, this subtracts a negative value, resulting in a net addition. However, because any unsigned value over 127 is also a negative signed value, this results in the BPL branch NOT being taken until the desired finishing scanline is reached, when the result of the CPY instruction drops back below 128 (unsigned), or becomes positive again (signed).

 

The final byte of sprite pattern data should probably be $00, as it is never cleared otherwise and so will continue to be drawn down the screen.

 

The Y register is used as a scanline counter, working backwards from a maximum of 127 down to 0.
Sprite_Y is a value in RAM, which starts out before drawing with the number ONE ABOVE the TOP (first) scanline where the sprite should appear.
Sprite_End is a value in RAM, which stores the number of the BOTTOM (last) scanline where the sprite should appear, ORed with $80. If read as a signed value, this is equal to the scanline minus 128.
NOTE: None of the above values must ever be more than 127, because the routine relies on signed numbers; 127 is the largest positive number that can be stored in a signed byte.
Sprite_Pattern_Pointer is a RAM address which contains a pointer to the appropriate data in ROM. It must be calculated at the start of each frame to point at an address Sprite_Y bytes BEFORE the actual desired data ENDS (it will be read from highest address to lowest address), and this must still be on the same 256-byte page, or the LDA[indirect indexed] instruction will take an extra cycle to complete and throw the timing out of alignment.

 

    cpy Sprite_Y    ; 3 Are we on the scanline one before the beginning or one after the end of the sprite?
    beq .switch    ; 2/3 True = Yes
    bpl .wait    ; 2/3 True = No, but we're not in the middle of drawing it, either
; We can only reach this point while the sprite is being drawn
    lda (Sprite_Pattern_Pointer),y    ; 5
    sta GRPx    ; 3
; (15 cycles)
.continue:
; (final total 15 cycles)
    ...
; Subsequent kernel code here
    ...
.switch:
; (total 6 cycles)
    lda Sprite_End    ; 3 Replace the current Sprite_Y value with Sprite_End
    sta Sprite_Y    ; 3
    bcs .continue    ; 3
; (9 cycles, total 15 cycles)

.wait:
; (total 8 cycles)
    nop        ; 2 Waste 4 cycles for timing
    nop        ; 2
    bpl .continue    ; 3
; (7 cycles, total 15 cycles)

 

 

MaskDraw - SpiceWare
Named because it uses a block of mask bytes stored in ROM to modulate whether or not the sprite is drawn on the current scanline. It eats up quite a bit of ROM space, requires several pointers to be calculated every frame, and puts significant limits where you can store your data (to avoid crossing page boundaries on the indirect loads), but for high speed with a constant runtime it looks hard to beat.

 

Sprite_Pattern_Pointer, Sprite_Mask_Pointer, and Sprite_Colour_Pointer are RAM addresses which contain pointers to the appropriate data in ROM. They must be offset (from the true address) by the correct amount to cause register Y (the scanline counter) to index the correct bytes when it reaches the desired scanlines.

    lda (Sprite_Pattern_Pointer),y    ; 5
    and (Sprite_Mask_Pointer),y    ; 5
    sta GRPx            ; 3
; (total 13 cycles)
    lda (Sprite_Colour_Pointer),y    ; 5 Optional for multicolour
    sta COLUPx            ; 3 Optional for multicolour
; (total 21 cycles)

You'll also need something like the following somewhere in your ROM, where SPRITE_HEIGHT is the height of your sprite (static), and SPACE_BEFORE and SPACE_AFTER should be the maximum number of scanlines you will need to leave the sprite undrawn before and after it appears.

    align 256
baseMaskAddress:
    ds.b SPACE_BEFORE, $00
spriteMaskAddress:
    ds.b SPRITE_HEIGHT, $ff
    ds.b SPACE_AFTER, $00

 

Edited by Verdant
A few typos
  • Like 3
Link to comment
Share on other sites

A lot of things about the Atari were figured out back in the day via the Stella Mailing List.

 

The site MiniDig links to a lot of useful info found in the mailing list.

 

MiniDig's Tricks page has a link to @DEBRO's explanation of @Thomas Jentzsch's SkipDraw routine, which y'all might find helpful.

 

Note: Users are notified if you @ them like I did above for Dennis and Thomas. They might be able to provide more information.

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

Thanks for being interested into my old code.

On 3/20/2024 at 4:45 PM, Verdant said:

Magical 17-cycle SkipDraw:
I've seen several comments to the effect that SkipDraw can have a runtime of 17 cycles, but I didn't find a complete example of it in code. 

IIRC the 17 cycle version simply assumes a 0 byte at the end of the graphics data. That way the skipDraw part doesn't have to update GRPx.

Edited by Thomas Jentzsch
  • Like 3
  • Thanks 1
Link to comment
Share on other sites

Hi there,

 

Thanks @SpiceWare for bringing this to my attention. 😁

 

I have always used the illegal skipDraw in my work. The one used in Climber5 also changes color of the climber each scanline and gives a 1LK. I finally placed the source on my GitHub if you would like to see it.

 

Keep in mind that for skipDraw, your sprite must reside above its max allowed vertical position in ROM. You can see this in the Climber5 source. It may make more since seeing how the graphics are stored in the code. 
 

https://github.com/DNSDEBRO/Climber5

  • Like 4
Link to comment
Share on other sites

  • 3 weeks later...

Thanks @Thomas Jentzsch, @SpiceWare, and @DEBRO for your advice, and to everyone who responded!

 

I've written a DASM file which can be set to use any of the outlined drawing routines, so that learners can see them in full action, with pointer shifts and all. It also helped me to learn a few things! Additionally, I have some changes to make to my previous post because there's new information to add, and I've also realized that there were one or two errors in there. Unfortunately it seems to be too old for me to edit, so here are the changes:

  • Ignore "Magical 17-cycle SkipDraw" because it's actually even less useful than I realised. It remains in the drawrout.asm as "SkipDraw_2"/"Silly 17-cycle SkipDraw".
  • Add "Sensible 17-cycle SkipDraw" based on Thomas' comment that pointed me in the right direction (details below).
  • For every "SkipDraw" routine except "Vanilla SkipDraw", change the Sprite_Y description to: "Sprite_Y is a value in RAM, which starts out before drawing with the number of the BOTTOM (last) scanline where the sprite should appear, and is decremented every scanline (i.e. the scanline number is counted starting from 0 at the top of the display)." The dangers of copy-paste!

 

SkipDraw

Sensible 17-cycle SkipDraw

Thanks to Thomas Jentzsch for clearing this one up. Here's an actually useful 17-cycle SkipDraw routine.

 

The Y register is a scanline counter, decreasing as it moves down the screen.
Sprite_Y is a value in RAM, which starts out before drawing with the number of the BOTTOM (last) scanline where the sprite should appear, and is decremented every scanline (i.e. the scanline number is counted starting from 0 at the top of the display).
SPRITE_HEIGHT is the height of the sprite in scanlines (static).
Sprite_Pattern_Pointer is a RAM address which contains a pointer to the appropriate data in ROM. It must be calculated at the start of each frame to point at an address Sprite_Y bytes BEFORE the actual desired data ENDS (it will be read from highest address to lowest address), and this calculated address must still be on the same 256-byte page, or the LDA[indirect indexed] instruction will take an extra cycle to complete and throw the timing out of alignment.

 

The final byte of sprite pattern data should probably be $00, as it is never cleared otherwise and so will continue to be drawn down the screen.

 

	lda #[SPRITE_HEIGHT-1]	; 2
	dcp Sprite_Y		; 5 [Illegal opcode: DEC and CMP] Is 0 <= (--Sprite_Y) < (SPRITE_HEIGHT)?
	bcc .skipDraw		; 2/3 True = No
	; (9 ICycles)

	lda (Sprite_Pattern_Pointer),y	; 5
	sta GRPx
	; (total 17 ICycles)
.continue:
	; (final total 17 ICycles)
...
	; Subsequent kernal code here
...

.skipDraw:
	; (10 ICycles)
	nop			; 2 Waste 4 ICycles for timing
	nop			; 2
	bcc .continue		; 3 Always true because we just got here via a BCC and the status flags haven't changed
	; (total 17 ICycles)

 

The example assembly file is attached to this post.

 

 

drawrout.asm

Edited by Verdant
I noticed a new error!
  • Like 2
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.
Note: Your post will require moderator approval before it will be visible.

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.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...