Professional Documents
Culture Documents
Italian Sprite Multiplexing Tutorial
Italian Sprite Multiplexing Tutorial
Italian Sprite Multiplexing Tutorial
io/archive/
The first post (the serious one with content), is about sprite multiplexing on the Commodore 64. It is
a technique for displaying more sprites than the hardware can show at the same time. In this first
part, I will make a quick pass at explaining the concept of sprites; in the next post I will move on to
multiplexing.
The C64 produces its output on video through the MOS 6569/8565/8566, better known as the VIC-II.
This is a component, an evolution of the VIC-I used on the Vic-20, that can handle, among other
things, the display of sprites.
Sprites are graphic elements measuring 24x21 pixels. They can be displayed in HiRes mode (1 color +
the transparency color) or Multicolor mode (3 colors + the transparency color). There are limitations
in using the 3 colors: two of them are common to all sprites, while one is specific to the individual
sprite. Also, in this mode, the pixels are twice the size.
The VIC-II can handle up to 8 sprites simultaneously on the screen, and their behavior is governed by
a series of memory registers that allow you to:
move each sprite on the screen (addresses $d000-$d00f and address $d010 to move beyond the x
coordinate = 255)
declare sprites HiRes or Multicolor ($d01c) ...and then set colors, detect collisions, and many other
properties.
In this example I will make use of a sprite created by hand with SpritePad
As you can see from the image, it is a HiRes sprite with yellow main color (7) and blue transparent
color (6).
The following Assembly listing (for use with KickAssembler) is used to set all 8 sprites with the same
image and color, on line 150.
In the first block I set the color, in the second I indicate the reference in memory where to find the
sprite to be drawn, then I set the X and Y coordinate of the sprites, finally I color the screen black and
enable all the sprites. At the bottom of the listing is the definition of the sprite mapped from memory
location $0940.
:BasicUpstart2($0810)
* = $0810
Init:
// Color of sprites
lda #YELLOW
sta $d027
sta $d028
sta $d029
sta $d02a
sta $d02b
sta $d02c
sta $d02d
sta $d02e
// Sprite pointer
lda #$25
sta $07f8
sta $07f9
sta $07fa
sta $07fb
sta $07fc
sta $07fd
sta $07fe
sta $07ff
lda #31
sta $d000
lda #62
sta $d002
lda #93
sta $d004
lda #124
sta $d006
lda #155
sta $d008
lda #186
sta $d00a
lda #217
sta $d00c
lda #248
sta $d00e
lda #150
sta $d001
sta $d003
sta $d005
sta $d007
sta $d009
sta $d00B
sta $d00D
sta $d00F
lda #0
sta $d020
sta $d021
lda #%11111111
sta $d015
rts
* = $0940
Sprites:
.byte $00,$00,$00,$00,$00,$00,$03,$f8,$00,$07,$f8,$00,$0f,$f8,$00,$1f
.byte $f8,$00,$1f,$07,$f0,$3e,$07,$e0,$3c,$07,$c0,$3c,$07,$80,$3c,$00
.byte $00,$3c,$07,$80,$3c,$07,$c0,$3e,$07,$e0,$1f,$07,$f0,$1f,$f8,$00
.byte $0f,$f8,$00,$07,$f8,$00,$03,$f8,$00,$00,$00,$00,$00,$00,$00,$07
Okay, that's the starting point. In the next post, we will do the interesting stuff!
Want to make a comment or discuss the contents of this article? I'll be waiting for you on Gitter!
Okay, well, having understood the concept of sprites and reiterated that the C64 cannot show more
than 8 at once, let's try to understand how more can be added. To do this, we need to introduce the
tool behind the technique. It is called interrupt raster.
One of the special features of the Vic-II is the ability to generate a "signal" to the processor that can
interrupt (hence the word interrupt) program execution at the moment the image is physically
generated on the screen. It is a bit like the Vic-II saying to the processor, "Hey, finish the instruction
you are executing right now, then give me some time." The processor, diligently, completes the
instruction in progress then transfers control to a known memory location that contains a script.
Upon completion of this script, the processor resumes execution where it left off. There are actually
other steps and activities performed to make this process possible but, simplifying, the flow is this.
The screen grid on which the image is drawn is called a raster, the horizontal lines are called
scanlines, and the components of the grid are called pixels. Through certain sequences of commands,
it is possible to tell the Vic-II to generate an interrupt when it is about to draw a given scanline.
This is all well and good, but what is the basic idea behind the multiplexing mechanism? It is to take
advantage of the 8 available sprites, deluding those who are looking at the image into thinking that
they have more by simply moving them around while drawing the screenline.
To understand this, if I want to draw 8 sprites on line 150 and as many on line 200, thus getting 16
sprites visible at the same time, the following steps should be followed:
the routine executed during the interrupt moves all sprites to line 150
the routine executed during the interrupt (different from the previous one) moves all the sprites to
line 200
an interrupt is set again at line 149 meanwhile the sprites have been drawn at line 200 (along with
those in line 150)
Q: Why is the interrupt set to line 149 and the sprites are placed on line 150?
A: The placement of sprites on a given area must occur before that area is drawn. If the interrupt
were set after line 150, the screen area prior to the scanline has already been drawn and a
repositioning of the sprites to that area would have no effect. It is also not possible to set the
interrupt on scanline 150 because there must be time to finish the move operations and you risk
having a partial drawing of the sprites.
The activities to indicate at which line the Vic-II should throw an interrupt are:
lda #149
sta $d012
lda #<Irq
sta $0314
lda #>Irq
sta $0315
The first two instructions load the value 149 (the scanline of the interrupt we want) into the
accumulator and, from there, into the address $d012 (this is the address of the Vic-II designated for
this purpose). The next 4 instructions load the address of the routine to be executed when the
interrupt is thrown. It is a 16-bit address to be loaded into two 8-bit locations, so in $0314 there will
be the low (least significant) part of the address of the routine and in $0315 there will be the high
(most significant) part. In our example, the routine is labeled Irq.
Full Listing
:BasicUpstart2($0810)
* = $0810
Init:
// interrupted sei
// Sprite pointer
lda #$28
sta $07f8
sta $07f9
sta $07fa
sta $07fb
sta $07fc
sta $07fd
sta $07fe
sta $07ff
lda #31
sta $d000
lda #62
sta $d002
lda #93
sta $d004
lda #124
sta $d006
lda #155
sta $d008
lda #186
sta $d00a
lda #217
sta $d00c
lda #248
sta $d00e
lda #0
sta $d020
sta $d021
lda #%01111111
sta $dc0d
and $d011
sta $d011
lda $dc0d
lda $dd0d
lda #149
sta $d012
lda #<Irq
sta $0314
lda #>Irq
sta $0315
// Enables Vic-II to launch interrupts
lda #%00000001
sta $d01a
lda #%11111111
sta $d015
cli
rts
Irq:
lda #150
sta $d001
sta $d003
sta $d005
sta $d007
sta $d009
sta $d00B
sta $d00D
sta $d00F
lda #GREEN
sta $d027
sta $d028
sta $d029
sta $d02a
sta $d02b
sta $d02c
sta $d02d
sta $d02e
lda #199
sta $d012
lda #<Irq2
sta $0314
lda #>Irq2
sta $0315
asl $d019
jmp $ea81
Irq2:
lda #200
sta $d001
sta $d003
sta $d005
sta $d007
sta $d009
sta $d00B
sta $d00D
sta $d00F
lda #RED
sta $d027
sta $d028
sta $d029
sta $d02a
sta $d02b
sta $d02c
sta $d02d
sta $d02e
lda #149
sta $d012
lda #<Irq
sta $0314
lda #>Irq
sta $0315
asl $d019
jmp $ea31
* = $0a00
Sprites:
.byte $00,$00,$00,$00,$00,$00,$03,$f8,$00,$07,$f8,$00,$0f,$f8,$00,$1f
.byte $f8,$00,$1f,$07,$f0,$3e,$07,$e0,$3c,$07,$c0,$3c,$07,$80,$3c,$00
.byte $00,$3c,$07,$80,$3c,$07,$c0,$3e,$07,$e0,$1f,$07,$f0,$1f,$f8,$00
.byte $0f,$f8,$00,$07,$f8,$00,$03,$f8,$00,$00,$00,$00,$00,$00,$00,$07
Compared to the previous listing, it can be seen that in the Init part, the parts that set the color of
the sprites and the positioning on the line have been removed. The color was removed because it is
set dynamically on the two interrupt routines, which we will see in detail later. Vertical positioning is
no longer present on the Init because it has become redundant as each sprite is repositioned on two
different rows each time the screen is refreshed.
Immediately after the border/background color setting there are some outline instructions to
properly trigger the system when interrupts are thrown. Let's take them as they are; they are not
relevant to the technique.
We then come to the Irq routine. From the way the program is set up, we know that it will be
launched on scanline 149. At this point we can move the sprites to scanline 150, set the color to
green, and prepare the next interrupt. End.
Right after that is the Irq2 routine. It will start at scanline 199 so, as with the previous one, we move
the sprites to line 200 and update the color.
Q: should you set the scanline equal to one pixel less than the sprites?
A: no, as long as it is prior to the position of the sprites. Sometimes it is necessary to use a margin
greater than 1 because if the routine is complex it may take longer to execute...while meanwhile the
drawing is proceeding expeditiously.
More examples
Poking around in the Vic-II registers we find $d017 and $d01d which allow us to duplicate,
respectively, the height and width of sprites. The effect is that of a "stretched" sprite similarly to
what happens when we enlarge an image in a graphics program.
If we add these three instructions in the Irq routine after setting the color
lda #%00000000
sta $d017
sta $d01d
lda #%11111111
sta $d017
sta $d01d
The first bit in Irq disables hardware doubling of height and width for all sprites, while the bit in Irq2
activates the feature.
Each bit is tied to a sprite: bit #0 (the rightmost bit) commands sprite #0, the last bit (the leftmost bit)
commands sprite #7. Clearly you can enable the functionality for single sprite, for height only or for
width only.
lda #WHITE
sta $d027
lda #RED
sta $d028
lda #CYAN
sta $d029
lda #PURPLE
sta $d02a
lda #GREEN
sta $d02b
lda #BLUE
sta $d02c
lda #YELLOW
sta $d02d
lda #ORANGE
sta $d02e
lda #BROWN
sta $d027
lda #LIGHT_RED
sta $d028
lda #DARK_GRAY
sta $d029
lda #GREY
sta $d02a
lda #LIGHT_GREEN
sta $d02b
lda #LIGHT_BLUE
sta $d02c
lda #LIGHT_GRAY
sta $d02d
lda #WHITE
sta $d02e
(in both cases replacing the part of the code that assigns the color) we get this effect
Changing sprites
So far we have always worked with the same sprite, the definition of which is at the bottom of the
source file. It is also possible to change the sprite between two scanlines, and we see this right away
with an example. First, at the bottom of the file, we provide the definition of a second sprite, right
after the definition of the first one:
* = $0a00
Sprites:
.byte $00,$00,$00,$00,$00,$00,$03,$f8,$00,$07,$f8,$00,$0f,$f8,$00,$1f
.byte $f8,$00,$1f,$07,$f0,$3e,$07,$e0,$3c,$07,$c0,$3c,$07,$80,$3c,$00
.byte $00,$3c,$07,$80,$3c,$07,$c0,$3e,$07,$e0,$1f,$07,$f0,$1f,$f8,$00
.byte $0f,$f8,$00,$07,$f8,$00,$03,$f8,$00,$00,$00,$00,$00,$00,$00,$07
Baloon:
.byte $00,$7f,$00,$01,$ff,$c0,$03,$ff,$e0,$03,$e7,$e0,$07,$d9,$f0,$07
.byte $df,$f0,$02,$d9,$f0,$03,$e7,$e0,$03,$ff,$e0,$03,$ff,$e0,$02,$ff
.byte $a0,$01,$7f,$40,$01,$3e,$40,$00,$9c,$80,$00,$9c,$80,$00,$49,$00
.byte $00,$49,$00,$00,$3e,$00,$00,$3e,$00,$00,$3e,$00,$00,$1c,$00
Recall, as written in post 1, the sprite can be generated with SpritePad, exported to Raw, inserted as
a sequence of bytes (as in this example) or included via .import directive (see the Kick Assembler
guide reference here).
Then, suppose we want to show this new sprite in the second scanline then in Irq2, we change the
line from
lda #$28
in
lda #$29
while also maintaining the color definition from the previous point we will get this output:
If you sharpen your eyesight a bit, you will notice that on the dome of the last two hot air balloons
are two stripes of different colors and, coincidentally, the same color as the sprite above. Why?
Well it is a problem related to the last Q/A in the previous post namely: on the second scanline, the
Vic-II started drawing the hot air balloon before the color change command for sprite #6 and #7 was
completed. The Vic's redrawing was faster than the execution of instructions on the processor, so a
portion was drawn with the old color.
How to get around this problem? The solution is to give the processor more time to complete its
work. So, either move the sprites further down or throw the interrupt a few lines in advance. If we
try to throw the interrupts on 145 and 195 (which is plenty of advance), the problem is gone.
It is not easy to have good control, so you need to do a lot of testing to see if you can get a result in
line with expectations.
Conclusions
From the few examples, you can see that a number of things can be done with the raster interrupt to
manipulate sprites and get around physical limitations. The tradeoff is that what the hardware
cannot provide must be handled "manually" by the program. These are the basics for understanding
how the flow works and, roughly, what the potential is. To you the invitation to take these examples
and experiment.
Foreword
I have done some research and analyzed various solutions. What I am proposing is an interpretation
of a possible solution to handle more than 8 sprites on screen, it is not THE solution but just one of
many. Take your cue, elaborate or ignore what you will see from now on. This is my point of view,
nothing more. That said, I invite you to send suggestions or counter suggestions.
I have taken some code on GitHub (link shortly) and analyzed it a bit trying to make it easier to
understand, I hope I have succeeded.
Basic idea
As laid out in the conclusions of the previous posts, if we want to go beyond the canonical 8
hardware sprites, we have to trick the user by taking advantage of the graphical capabilities of the
system.
This is accomplished by using the raster interrupt near the Y coordinate at which we want to draw
the sprites. In fact, the same sprite is continuously moved from one position to another during the
drawing of each image. The mechanism becomes complex because our program must take charge of
keeping track of all the sprites to be displayed (which from now on we will call "virtual" sprites), since
it cannot rely on the registers intended to track the 8 in hardware.
It is therefore a matter of doing collaborative work between the main program and the interrupt
(henceforth irq) raster:
the main has to deal with program logic and positioning (but also color, size, shape, etc...in short, any
aspect of any sprite in the program). It must basically specify how things are to work.
the raster interrupt must deal with visualization, specifically the mapping of the virtual sprites to the
8 hardware sprites.
The main will do the init of all the variables used, the sprites, and prepare the launch of the irq raster.
It will also, for teaching purposes, move the sprites randomly throughout the program.
one will take care of sorting the sprites by Y-coordinate (we will explain why later in the post)
another will take care of displaying the sprites and will start on multiple scanlines based on the
sprites to be displayed.
Well, with the sufficient degree of messiness generated by these lines, let us begin to say something
about the main program.
Accessory functions
Meanwhile, let us define the subroutine that prepares the launch of the irq raster; many of the things
have already been seen in previous installments. We note only the launch scanline set to
IrqSortingLine (a label equal to $fc => 252):
InitRaster: {
sei
lda #<IrqSorting
sta $0314
lda #>IrqSorting
sta $0315
lda #$7f
sta $dc0d
lda #$01
sta $d01a
lda #27
sta $d011
lda #IrqSortingLine
sta $d012
lda $dc0d
cli
rts
lda #minNumber
sta GetRandom.GeneratorMin
lda #maxNumber
sta GetRandom.GeneratorMax
jsr GetRandom
GetRandom: {
Loop:
lda $d012
eor $dc04
sbc $dc05
cmp GeneratorMax
bcs Loop
cmp GeneratorMin
bcc Loop
rts
The GetRandom subroutine generates a random number (reading the current scanline) and makes
sure it is between two limit values. The macro allows you to set the two limit values before calling
the subroutine.
Below is another macro to set the initial state of the virtual sprites:
.macro InitSpritesData() {
ldy #1
dex
initloop:
GetRandomNumberInRange(25, 255)
sta sprx,x
jsr GetRandom
sta spry,x
lda #$3f
sta sprf,x
tya
sta sprc,x
iny
cpy #16
bne nextcolorok
ldy #1
nextcolorok:
dex
bpl initloop
you set the initial x and y positions of the virtual sprites by inserting them into the sprx and spry
vectors. Then you set the structure of the sprites by retrieving it from the memory area where it is
defined ($3f) and inserting it into sprf Finally you set the color of each sprite in sprc. The color must
be other than black otherwise it will blend in with the background.
Main program
Start: {
jsr InitSprites
jsr InitRaster
lda #00
sta $d020
sta $d021
ldx #MAXSPR
stx numsprites
InitSpritesData()
MainLoop:
inc SpriteUpdateFlag
MoveLoop:
txa
lsr
lsr
lsr
adc sprx,x
sta sprx,x
lda Direction
eor #$ff
bpl rewind
inc spry,x
jmp EndLoop
rewind:
dec spry,x
EndLoop:
sta Direction
dex
bpl MoveLoop
jmp MainLoop
Direction: .byte 0
As can be seen, the entire part preceding MainLoop is devoted to initializing the program. The label
MAXSPR indicates the maximum number of sprites to be generated. Once the init part is finished, the
MainLoop of the program begins and is repeated ad infinitum.
The first thing that is done at the beginning of a new loop is to make sure that the sprites are sorted
correctly in the vector. This is not the place where this is done but in the IrqSorting. Therefore, at the
main, all that is left to do is to request a sorting (by setting the SpriteUpdateFlag "guard" variable to
1) and wait for this to be done in the appropriate irq (which will reset the guard when the job is
finished).
Assuming we have gotten the go-ahead to proceed, now the main is in charge of moving the sprites,
and it does:
transfer x to a, executing three times lsr (logical shift to the right, which is equivalent to dividing by 2)
for y, adding or removing 1 to the previous y coordinate The process is repeated for all sprites after
which we return to MainLoop and start over.
In a serious, noninstructional program such as this, positioning may be determined by a more refined
function, or it may depend on what is pressed on the keyboard or how the joystick is used. Also there
would be all the logic of operation. There is obviously none of that here, the important thing is to
understand the breakdown of the code and which parts have the different tasks.
Well, now we come to the two most important as well as complicated parts. The first of the two is
the Irq raster sprite sorting subroutine and it starts on scanline 252, well beyond visible space.
So why does it become necessary to reorder sprites? First, let's say that by "sprite sorting" we mean
reading sprites from the arrays that contain their positions (spry, sprx, etc.) and creating as many
arrays by sorting by y position. The subroutine will insert the sprites in order from highest to lowest.
This sorting is to simplify the work of the subroutine that will draw the sprites on the screen. The
drawing subroutine has very little time to work (about 63 clock cycles per scanline - if you want to
understand why this number is so high, I'll leave you the link to this post on Lemon64
https://www.lemon64.com/forum/viewtopic.php?
t=71390&sid=a0e7eca10fd18beae6e00a8e63cb8152 ) and therefore must have everything ready to
display.
In its top-to-bottom scrolling, the drawing subroutine only needs to draw the sprites that are on the
current scanline. If there were no sorting step, at each scanline it would have to check which sprite,
of all those present, should be drawn on the current scanline. It takes time for this verification, time
that is not there, so sorting is necessary.
The sorting job then is done when the scanline is beyond the visible screen, thus starting as
mentioned before on line 252. Again, there is not all the time in the world but surely we can take
things relatively slowly so that we are ready for the next rendering.
Of course, sorting will handle not only the y position (in the sortspry array) but also the x position
(sortsprx), keep track of the color (sortsprc) and the pointer to the image to be shown (sortsprf). In
the irq code there is also the scanline setting at which the irq raster should be launched, which will
collect the sorted arrays and arrange the display.
Sorting
IrqSorting: {
dec $d019
lda #RED
sta $d020
sta $d005
sta $d007
sta $d009
sta $d00b
sta $d00d
sta $d00f
beq irq1_nonewsprites
lda #$00
sta SpriteUpdateFlag
irq1_nonewsprites:
ldx SortedSprites
cpx #$09
bcc irq1_notmorethan8
ldx #$08
irq1_notmorethan8:
sta $d015
beq irq1_nospritesatall
sta sprirqcounter
lda #<IrqDisplay
sta $0314
lda #>IrqDisplay
sta $0315
irq1_nospritesatall:
lda #BLACK
sta $d020
jmp $ea81
irq1_beginsort:
ldx #MAXSPR
dex
cpx SortedSprites
bcc irq1_cleardone
irq1_clearloop:
bcs irq1_clearloop
irq1_cleardone:
ldx #$00
irq1_sortloop:
cmp spry,y
bcs irq1_sortskip
stx irq1_sortreload+1
irq1_sortswap:
lda sortorder+1,x
sta sortorder,x
sty sortorder+1,x
cpx #$00
beq irq1_sortreload
dex
ldy sortorder+1,x
lda spry,y
ldy sortorder,x
cmp spry,y
bcc irq1_sortswap
irq1_sortreload:
ldx #$00
irq1_sortskip:
inx
cpx #MAXSPR-1
bcc irq1_sortloop
ldx SortedSprites
ldx #$00
irq1_sortloop3:
lda sprx,y
sta sortsprx,x
lda sprf,y
sta sortsprf,x
lda sprc,y
sta sortsprc,x
inx
cpx SortedSprites
bcc irq1_sortloop3
jmp irq1_nonewsprites
I have to be honest, the algorithm is complex and I have not spent much time on it to understand and
dissect it. If, like me, you prefer to stay out of it and take it as dogma, just know that at the end of
processing, we will find the sort arrays (i.e., all those beginning with sort*) ready to be used in the
display phase.
In developing a game, I would tend not to make this subroutine and the subsequent display
subroutine the subject of modification. I would rather focus on the logic and rules that govern
everything.
Display
IrqDisplay: {
dec $d019
lda #GREEN
sta $d020
irq2_direct:
clc
irq2_notover:
sta tempvariable
irq2_spriteloop:
lda sortspry,y
bcs irq2_endspr
ldx physicalsprtbl2,y
sta $d001,x
lda sortsprx,y
asl
sta $d000,x
bcc irq2_lowmsb
lda $d010
ora ortbl,x
sta $d010
jmp irq2_msbok
irq2_lowmsb:
lda $d010
and andtbl,x
sta $d010
irq2_msbok:
ldx physicalsprtbl1,y
lda sortsprf,y
sta $07f8,x
lda sortsprc,y
sta $d027,x
iny
bne irq2_spriteloop
irq2_endspr:
beq irq2_lastspr
sty sprirqcounter
sta $d012
lda #BLACK
sta $d020
jmp $ea81
irq2_lastspr:
lda #>IrqSorting
sta $0315
lda #IrqSortingLine
sta $d012
lda #BLACK
sta $d020
jmp $ea81
This is the listing of the subroutine that performs the sprite display. There is a lot of stuff so let's
analyze it slowly.
The first instruction performs an Irq launch confirmation, then follows a border color change to
green. It serves to indicate on the screen, for debugging purposes, when the irq has started. The
same mechanism was present in the sorting subroutine.
The general operation is to place, at each scanline, all the sprites that are nearby. Once the group of
nearby sprites has been drawn, the algorithm considers whether to launch an irq on a new scanline
further down or to launch a sorting irq (in case all sprites have already been drawn).
The first block takes the sprite that is to be drawn (whose index is sprirqcounter) and retrieves its y-
coordinate. To this coordinate it adds the value 16 and the result will be the line at which the irq will
end. Immediately afterwards we make sure that the ending coordinate of the irq is no more than
255.
If the sprite to be drawn is not beyond the end coordinate of the irq, we proceed with the drawing by
retrieving coordinates, colors and shape from the arrays. This is done in the part from irq2_spriteloop
to irq2_endspr. In this block:
the x register "points" to the physical sprites (thus allowing access to the coordinates, color, and
shape)
Below (starting with irq2_endspr) the indexes are updated and the next tasks to be done are checked
(terminate the subroutine, set the next irq etc...)
If the routine has completed the last sprite, it sets the start of a new sorting.
*=$0801
.byte $0E, $08, $0A, $00, $9E, $20, $28, $34, $30, $39, $36, $29, $00,
$00, $00
*= $0fc0
//The Sprite DATA (Enter any single colour sprite code here, to apply to ALL
generated sprites):
.byte 0,0,0
.byte 124,241,1
.byte 253,251,1
.byte 193,131,3
.byte 193,243,99
.byte 193,251,246
.byte 193,155,246
.byte 253,248,100
.byte 124,240,100
.byte 0,0,0
.byte 12,241,224
.byte 13,251,240
.byte 12,27,48
.byte 12,251,240
.byte 13,249,224
.byte 13,131,48
.byte 13,251,240
.byte 13,249,224
.byte 0,0,0
.byte 0,0,0
.byte 0,0,0
.macro InitSpritesData() {
ldy #1
dex
initloop:
GetRandomNumberInRange(25, 255)
sta sprx,x
jsr GetRandom
sta spry,x
lda #$3f
sta sprf,x
tya
sta sprc,x
iny
cpy #16
bne nextcolorok
ldy #1
nextcolorok:
dex
bpl initloop
}
//Main program
Start: {
jsr InitSprites
jsr InitRaster
lda #00
sta $d020
sta $d021
ldx #MAXSPR
stx numsprites
InitSpritesData()
MainLoop:
inc SpriteUpdateFlag
WaitLoop:
lda SpriteUpdateFlag
bne WaitLoop
ldx #MAXSPR-1
MoveLoop:
txa
lsr
lsr
lsr
adc sprx,x
sta sprx,x
lda Direction
eor #$ff
bpl rewind
inc spry,x
jmp EndLoop
rewind:
dec spry,x
EndLoop:
sta Direction
dex
bpl MoveLoop
jmp MainLoop
Direction: .byte 0
}
irq1_nonewsprites:
ldx SortedSprites
cpx #$09
bcc irq1_notmorethan8
ldx #$08
irq1_notmorethan8:
lda d015tbl,x //Now put the right value to
sta $d015 //$d015, based on number of
beq irq1_nospritesatall //sprites
//Now init the sprite-counter
lda #$00 //for the actual sprite display
sta sprirqcounter //routine
lda #<IrqDisplay //Set up the sprite display IRQ
sta $0314
lda #>IrqDisplay
sta $0315
jmp IrqDisplay.irq2_direct //Go directly// we might be late
irq1_nospritesatall:
lda #BLACK
sta $d020
irq1_beginsort:
ldx #MAXSPR
dex
cpx SortedSprites
bcc irq1_cleardone
lda #$ff //Mark unused sprites with the
irq1_clearloop:
sta spry,x //lowest Y-coordinate ($ff)//
dex //these will "fall" to the
cpx SortedSprites //bottom of the sorted table
bcs irq1_clearloop
irq1_cleardone:
ldx #$00
irq1_sortloop:
ldy sortorder+1,x //Sorting code. Algorithm
lda spry,y //ripped from Dragon Breed -)
ldy sortorder,x
cmp spry,y
bcs irq1_sortskip
stx irq1_sortreload+1
irq1_sortswap:
lda sortorder+1,x
sta sortorder,x
sty sortorder+1,x
cpx #$00
beq irq1_sortreload
dex
ldy sortorder+1,x
lda spry,y
ldy sortorder,x
cmp spry,y
bcc irq1_sortswap
irq1_sortreload:
ldx #$00
irq1_sortskip:
inx
cpx #MAXSPR-1
bcc irq1_sortloop
ldx SortedSprites
lda #$ff //$ff is the endmark for the
sta sortspry,x //sprite interrupt routine
ldx #$00
irq1_sortloop3:
ldy sortorder,x //Final loop
lda spry,y //Now copy sprite variables to
sta sortspry,x //the sorted table
lda sprx,y
sta sortsprx,x
lda sprf,y
sta sortsprf,x
lda sprc,y
sta sortsprc,x
inx
cpx SortedSprites
bcc irq1_sortloop3
jmp irq1_nonewsprites
}
IrqDisplay: {
dec $d019 //Acknowledge raster interrupt
lda #GREEN
sta $d020
irq2_direct:
ldy sprirqcounter //Take next sorted sprite number
lda sortspry,y //Take Y-coord of first new sprite
clc
adc #$10 //16 lines down from there is
bcc irq2_notover //the endpoint for this IRQ
lda #$ff //Endpoint canït be more than $ff
irq2_notover:
sta tempvariable
irq2_spriteloop:
lda sortspry,y
cmp tempvariable //End of this IRQ?
bcs irq2_endspr
ldx physicalsprtbl2,y //Physical sprite number x 2
sta $d001,x //for X & Y coordinate
lda sortsprx,y
asl
sta $d000,x
bcc irq2_lowmsb
lda $d010
ora ortbl,x
sta $d010
jmp irq2_msbok
irq2_lowmsb:
lda $d010
and andtbl,x
sta $d010
irq2_msbok:
ldx physicalsprtbl1,y //Physical sprite number x 1
lda sortsprf,y
sta $07f8,x //for color & frame
lda sortsprc,y
sta $d027,x
iny
bne irq2_spriteloop
irq2_endspr:
cmp #$ff //Was it the endmark?
beq irq2_lastspr
sty sprirqcounter
sec //That coordinate - $10 is the
sbc #$10 //position for next interrupt
cmp $d012 //Already late from that?
bcc irq2_direct //Then go directly to next IRQ
sta $d012
lda #BLACK
sta $d020
jmp $ea81
irq2_lastspr:
lda #<IrqSorting //Was the last sprite,
sta $0314 //go back to IrqSorting
lda #>IrqSorting //(sorting interrupt)
sta $0315
lda #IrqSortingLine
sta $d012
lda #BLACK
sta $d020
jmp $ea81 //Continue IRQs
}
GetRandom: {
Loop:
lda $d012
eor $dc04
sbc $dc05
cmp GeneratorMax
bcs Loop
cmp GeneratorMin
bcc Loop
rts
// SPRITE TABLES:
// CONSTANTS
d015tbl:
.byte %00000000 //Table of sprites that are "on"
.byte %00000001 //for $d015
.byte %00000011
.byte %00000111
.byte %00001111
.byte %00011111
.byte %00111111
.byte %01111111
.byte %11111111
physicalsprtbl1:
.byte 0,1,2,3,4,5,6,7 //Indexes to frame & color
.byte 0,1,2,3,4,5,6,7 //registers
.byte 0,1,2,3,4,5,6,7
.byte 0,1,2,3,4,5,6,7
.byte 0,1,2,3,4,5,6,7
.byte 0,1,2,3,4,5,6,7
.byte 0,1,2,3,4,5,6,7
.byte 0,1,2,3,4,5,6,7
physicalsprtbl2:
.byte 0,2,4,6,8,10,12,14
.byte 0,2,4,6,8,10,12,14
.byte 0,2,4,6,8,10,12,14
.byte 0,2,4,6,8,10,12,14
.byte 0,2,4,6,8,10,12,14
.byte 0,2,4,6,8,10,12,14
.byte 0,2,4,6,8,10,12,14
.byte 0,2,4,6,8,10,12,14
ortbl: .byte 1
.byte 255-2
.byte 2
.byte 255-4
.byte 4
.byte 255-8
.byte 8
.byte 255-16
.byte 16
.byte 255-32
.byte 32
.byte 255-64
.byte 64
.byte 255-128
.byte 128
Useful links
I leave you with some interesting links:
https://kodiak64.com/blog/toggleplexing-sprites-c64
https://codebase64.org/doku.php?id=base:sprite_multiplexing
https://forums.tigsource.com/index.php?topic=69477.20
Here we are with a new post devoted to sprites, much simpler than the previous ones, but dealing
with a situation that I have often dealt with: how to read the character that is under a sprite?
Introduction
Let's start by showing a schematic of the C64 screen, in which I placed a sprite at the top-leftmost
point so that it is fully visible.
The coordinates of this sprite are (24, 50). The whole sprite is visible, in the four corners I
intentionally put a bright dot to indicate the vertex.
The purpose of this post is to calculate the position of the underlying character (in this case, it is the
character at position 0,0) and detect which character it is.
Logical operation
The way to get this information is to relate the position of the sprite to the character area. The
formula is trivial and for the two coordinates is:
Character column = (X - 24) / 8
The two 8 values in the formulas are the dimensions in pixels of the individual character.
For clarity, the formula allows you to find the character below the top left point of the sprite.
Suppose we want to calculate which character is on the grid from the x,y coordinates. We can
imagine, for example, that these x,y belong to a sprite that has made a collision with a character on
the screen.
The collision can be detected by several techniques, one of which is to monitor the state of the Vic-II
register reachable at address $d01f (yes, as I said, there are other more efficient, more elegant, more
more more...).
Well, from this address, you can detect which sprite generated the collision, for simplicity let's
assume it is sprite #0.
Code
Okay come on, now one piece at a time and with some support structure, let's go find the underlying
character.
Consider the situation above, we have sprite #0 being defined with these instructions:
lda #$28
sta $07f8
lda #68
sta $d000
lda #80
sta $d001
lda #1
sta $d015
Previously seen code: setting sprite shape, positioning on coordinates (68, 80) and triggering. It is
minimal; I have included the bare minimum for the example.
With this starting condition, we proceed to calculate the character. Let's evaluate the Y coordinate:
lda $d001
sec
sbc #50
lsr
lsr
lsr
The first instruction reads the Y coordinate of the sprite and inserts the value into the A register.
Immediately after that, I perform the subtraction of 50 (equal to the top edge). Following this are
three lsr instructions. Each lsr instruction performs a logical bit shift to the right of the contents of
the A register.
As can be seen, shifting the bits to the right generates a binary value that is half of the previous one.
It is equivalent, therefore, to integer division by 2, with loss of remainder. Replicating the division
three times results in division by 8.
The initial intent begins to show; the Y coordinate of the character is 3 (just as we expected). But let's
proceed further with the calculation of X.
lda $d000
sec
sbc #24
lsr
lsr
lsr
Similar to the above, first the X coordinate of the sprite is retrieved and inserted into the A register.
Then the ground is prepared for the subtraction of 24 (size in pixels of the right edge). Again division
by 8 is performed, obtained by three consecutive lsr instructions.
In this case the result is 5, which corresponds to the X position of the character we are looking for.
Putting the two code snippets together, we get that, given a sprite at position (68, 80), we can detect
the coordinates of the screen character, which are (5, 3).
To detect the character present at these coordinates, we insert these snippets into a more complex
block.
lda $d001
sec
sbc #50
lsr
lsr
lsr
tay
lda ScreenMemTableL, y
sta ScreenPositionAddress
lda ScreenMemTableH, y
sta ScreenPositionAddress + 1
lda $d000
sec
sbc #24
lsr
lsr
lsr
clc
adc ScreenPositionAddress
sta ScreenPositionAddress
lda ScreenPositionAddress + 1
adc #0
sta ScreenPositionAddress + 1
To detect the character present at a given coordinate, we have to try to locate the memory address
in the ScreenRam and, from there, perform a read. The address is a 16-bit value so we will use a
variable so defined to handle it:
The first block of code, as already seen is in charge of finding the Y coordinate of the character on the
screen. After transferring the value from register A to Y, you use it as an index to access two support
tables that, against a Y coordinate, return the hi-byte (ScreenMemTableH) and lo-byte
(ScreenMemTableL) of the coordinate in the screen ram relative to the row of interest.
Next, with the code already seen, the X coordinate of the character is calculated. The value, found in
the A register, is added to the variable ScreenPositionAddress (the first adc does the summing, the
second adc, adds any carryover to the hi-byte).
At the end of these instructions, inside ScreenPositionAddress we have the memory location in the
screen ram of the character we are interested in.
Final Listing
For better understanding, below I have written a listing that positions a sprite and moves it one pixel
at a time along the horizontal axis (with a rudimentary wait loop to make the movement smooth and
visible). With each move, a subroutine is called that detects the character underneath the sprite and
writes it to another location on the screen.
To perform the read from the screen ram, self-mod coding is required. This involves modifying the
program code at runtime. It is a means of having something similar to the pointer in C (forgive me
purists for the arrogant simplification).
:BasicUpstart2($0810)
* = $0810
Init: {
lda #$28
sta $07f8
lda #30
sta $d000
lda #80
sta $d001
lda #1
sta $d015
WaitLoop:
dex
bne WaitLoop
dey
bne WaitLoop2
rts
GetCharacter: {
lda $d001 // I get the Y coordinate
sec
sbc #50
lsr
lsr
lsr
lda ScreenMemTableL, y
sta ScreenPositionAddress
lda ScreenMemTableH, y
sta ScreenPositionAddress + 1
sec
sbc #24
lsr
lsr
lsr
adc ScreenPositionAddress
sta ScreenPositionAddress
lda ScreenPositionAddress + 1
adc #0
sta ScreenPositionAddress + 1
// ScreenPositionAddress
lda ScreenPositionAddress
sta CheckChar + 1
lda ScreenPositionAddress + 1
sta CheckChar + 2
CheckChar:
rts
* = $0a00
Sprites:
.byte $81,$ff,$01,$07,$ff,$00,$0f,$ff,$00,$1f,$ff,$00,$3f,$ff,$00,$7f
.byte $e0,$ff,$7f,$80,$fe,$7f,$00,$fc,$fe,$00,$f8,$fe,$00,$f0,$fe,$00
.byte $00,$fe,$00,$f0,$fe,$00,$f8,$ff,$00,$fc,$7f,$80,$fe,$7f,$e0,$ff
.byte $3f,$ff,$00,$1f,$ff,$00,$0f,$ff,$00,$07,$ff,$00,$81,$ff,$01,$05
* = * "ScreenMemTableL"
ScreenMemTableL:
* = * "ScreenMemTableH"
ScreenMemTableH:
Welcome to this new post. The sprite thread continues and this time we talk about collisions.
Introduction
The C64, on the subject of sprite collisions, provides two registers on the Vic-II: $d01e and $d01f. For
both registers, each bit maps to a sprite (#0 to #7) and is set to 1 if the respective sprite participates
in a collision.
The first register maps to collisions between sprites, the second to collisions between sprites and the
background. Of the two, in this post, we are (at least partially) interested in the first of the two.
If we exploit the first register, we are talking about hardware collision. Looking at the register, we
realize that so much collision information is fused together making it impossible for us, in many
situations, to figure out which sprites were involved.
If, when reading the register, the value %10000011 is returned, we can tell that sprite #0 and sprite
#1 are in collision. Up to this point, it is easy.
If, however, after reading we receive the value %00000111, we detect that sprites #2, #1 and #0 are
involved in a collision. The above read occurs for a set of sprites as in the figure.
However, the same reading can also occur for a sprite configuration such as the following.
Therefore, it can be inferred that, given the same read, it is not possible to determine with certainty
between which sprites the collision occurred: if the bit of a sprite is 1 it means that sprite had a
collision, but, when there are more than two sprites involved, there is no way to know which sprite
the collision occurred with.
Logical operation
One can look for a solution by building things in house, choosing a completely software or hybrid
route.
I had this problem in my ForestSaver game, where there are many sprites simultaneously visible and I
need to know between whom collisions occur.
I therefore chose to implement a subroutine to make a software collision detector. My solution could
also be hybrid if it relied on the Vic-II register to optimize execution time.
Basically, the basic idea of my solution is to check whether a point on a sprite (e.g., the top left
vertex) is within the area of another sprite. If it is then the collision is established otherwise...not.
In this post, I reproduce the subroutine used in the game to check whether sprite #0 (the ranger) had
a collision with one of the other sprites of the "little men" in the game.
The solution consists of a first part where you set the sprites to be checked and the subroutine that
does all the work.
Implementation
The example we will see will check whether sprite #1 is in collision with the ranger (sprite #0).
Let's tackle the first part right away, which is setting up the data to perform the verification.
It is quite simple and consists of assigning to two variables (OtherX and OtherY) the coordinates of
the sprite to be checked (in our case, sprite #1). The OtherX variable is a word (16 bits) because the X
coordinate can go beyond 255 pixels.
To know if a sprite is beyond this limit, we make use of the $d010 register, which sets the bits to 1 if
the corresponding sprite is beyond 255 pixels. So, since we are talking about sprite #1, simply use the
%000010 mask and put it in AND with the reading of register $d010.
If the result is non-zero then we are beyond 255 pixels, so the top of OtherX must be set to 1.
This setting is done with the first few lines of the following snippet.
!:
sta SpriteCollision.OtherX + 1
lda $d002
sta SpriteCollision.OtherX
lda $d003
sta SpriteCollision.OtherY
jsr SpriteCollision
The next lines set the lower part of OtherX with the X coordinate from register $d002 and the Y
coordinate from register $d003 in OtherY.
The last instruction calls the subroutine to perform the verification, the code for which is as follows.
SpriteCollision: {
lda $d010
and #%00000001
sta RangerX1 + 1
sta RangerX2 + 1
lda $d000
sta RangerX1
sta RangerX2
add16value($0018, RangerX2)
lda $d001
sta RangerY1
clc
adc #21
sta RangerY2
// This occurs if
bmi16(OtherX, RangerX1)
bmi NoCollisionDetected
bmi16(RangerX2, OtherX)
bmi NoCollisionDetected
// If OtherY < RangerY1 then jump out (no collision)
lda OtherY
cmp RangerY1
bmi NoCollisionDetected
lda RangerY2
cmp OtherY
bmi NoCollisionDetected
CollisionDetected:
lda #$01
jmp Done
NoCollisionDetected:
lda #$00
Done:
rts
// Ranger's container
}
When the subroutine returns, there will be 1 in register A if the collision was detected, otherwise 0.
There is no need to be frightened by some instructions in the listing. The meaning is as follows:
add16value(x, y): adds the value x to the variable y and stores the result (similar to adc but operates
on 16-bit values)
bmi16(x, y) performs comparisons between two 16-bit variables and sets flags for the execution of a
bmi instruction.
Notes
The collision is detected based, as mentioned at the beginning, on the top left vertex of sprite #1. If
we wanted to use the center of the sprite just add 12 to OtherX and 10 to OtherY (be careful to
handle the top of OtherX correctly should the addition exceed 255).
Sprite #0 can be freed by making the subroutine more generic, just enter its coordinates as
parameters.
The initial parameter setting can be transfrom a macro, thus simplifying the code.
Hardware detection checks whether sprites overlap in their colored parts (collision does not occur if
transparent areas overlap). This subroutine is not as refined.
Sprite multiplexing
Entering the topic of multiplexing, the use of a software detector becomes very important if not
essential.
Since in this subroutine, verification is done by coordinate comparison, it can be completely freed
from the Vic-II registers. If we consider the example from one of the previous posts, where the 8
hardware sprites map to the "virtual" sprites, simply replace the instructions that read the sprite
position registers with the structure that stores the positions of the virtual sprites and you are done.
Conclusions
I have talked about a solution closely related to a problem I have had in the past, and you can see
that it is very adherent to the needs.
I have given some ideas to generalize it and adapt it to various contexts, then of course it depends on
the logic of the game/program.