Okay, this one is going to be looong. You have been warned.
Chapter 4: And now our characters
Following the game philosophy, our sprites will also be sectioned in tiles. Skool Daze characters have a maximum size of 24x32 pixels, originally sectioned in a grid of 3x4 tiles (8x8 pixel tiles, of course). In this case we can make an easy equivalence for the Oric and arrange the characters in grids of 4x4 tiles of 6x8 pixels; the same as our Skool tiles.
A sprite graphic will be, then, an array of 4x4 bytes, each one being an ID for a tile graphic and the corresponding mask. This way some graphics can be reused and some memory saved. For each animation frame of the character (walk, jump, fire catapult, sit down, etc) we will have a different 4x4 array. Following the naming conventions in pyskool.ca (see link in my post with chapter 2), we’ll call this the animatory state, and we’ll keep a pointer to this structure for each of our characters in the game that will be updated whenever the character moves.
To make things easy, the character ID will be the index in all the arrays with data for each character, and as it is usual in the 6502 instead of having something like:
Code: Select all
data_character1
.word xxxx ;Pointer to animatory state
.byt xx ;pos_col
.byt xx ;pos_row
...
data_character2
.word xxxx; Pointer to animatory state
.byt xx ;pos_col
.byt xx ;pos_row
...
It is better to have
Code: Select all
as_pointer_high
.byt xx,xx,… ; high byte pointer for character 0,1,2,3..
as_pointer_low
.byt xx,xx,… ; low byte pointer for character 0,1,2,3..
pos_col
.byt xx,xx,... ; column position of character 0,1,2,...
pos_row
.byt xx,xx,... ; row position of character 0,1,2,...
(For C users, do not use arrays of structures struct sprite_data sprites[N_SPRITES], but separate arrays for each field -better if they are all bytes- such as char pos_col[N_SPRITES], char pos_row[N_SPRITES], etc)
So we can have the character ID in reg X (for instance) and access each field with pos_col,x or pos_row,x. So, for our animatory states:
Code: Select all
as_pointer_high
.byt >Eric_anim_states,>Einstein_anim_states,>Angelface_anim_states,>BoyWander_anim_states
.byt >Boy_anim_states,>Boy_anim_states,>Boy_anim_states,>Boy_anim_states,>Boy_anim_states
.byt >Boy_anim_states,>Boy_anim_states,>Boy_anim_states,>Boy_anim_states,>Boy_anim_states
.byt >Boy_anim_states,>Creak_anim_states,>Rockitt_anim_states,>Wacker_anim_states,>Withit_anim_states
.byt >Pellet_anim_states,>Pellet_anim_states
as_pointer_low
.byt <Eric_anim_states,<Einstein_anim_states,<Angelface_anim_states,<BoyWander_anim_states
.byt <Boy_anim_states,<Boy_anim_states,<Boy_anim_states,<Boy_anim_states,<Boy_anim_states
.byt <Boy_anim_states,<Boy_anim_states,<Boy_anim_states,<Boy_anim_states,<Boy_anim_states
.byt <Boy_anim_states,<Creak_anim_states,<Rockitt_anim_states,<Wacker_anim_states,<Withit_anim_states
.byt <Pellet_anim_states,<Pellet_anim_states
Our animatory states for Eric are something like:
Code: Select all
; Animatory states for children
Eric_anim_states
; Animatory state 0 (1-Eric00.png)
.byt 0, 0, 0, 0
.byt 0, 1, 2, 0
.byt 3, 4, 5, 0
.byt 0, 6, 7, 0
; Animatory state 1 (1-Eric01.png)
.byt 0, 0, 0, 0
.byt 15, 16, 17, 0
.byt 18, 19, 20, 0
.byt 21, 22, 23, 0
And we have our graphics and masks aligned this way:
Code: Select all
.dsb 256-(*&255)
.dsb 8
children_tiles
; Tile graphic 1
.byt $0, $f, $7, $5, $0, $7, $2, $7
; Tile graphic 2
.byt $0, $20, $30, $30, $10, $38, $3c, $3e
…
.dsb 256-(*&255)
.dsb 8
children_masks
; Tile mask 1
.byt $70, $60, $60, $70, $70, $70, $78, $70
; Tile mask 2
.byt $5f, $4f, $47, $47, $47, $43, $41, $40
We need another table with data including the base address with the tiles for each character (either children or teacher). Again taking into account the amount of graphics it will exceed 256 different tiles easily, so splitting them is a good idea.
Aligning everything carefully to page boundaries, we only need the high bytes, therefor for our 21 characters:
Code: Select all
; Tables with base pointers to tiles for characters
tab_tiles
.dsb 15, >(children_tiles-8)
.dsb 3, >(teacher_tiles-8)
.dsb 1, >(teacher2_tiles-8)
.dsb 2, >(children_tiles-8)
tab_masks
.dsb 15, >(children_masks-8)
.dsb 3, >(teacher_masks-8)
.dsb 1, >(teacher2_masks-8)
.dsb 2, >(children_masks-8)
Ok. We have everything prepared (I made a small Matlab script to create the graphical data from the png files; else it is quite of a nightmare). Now it is time to see how the sprites are rendered. Remember we only draw those tiles flagged in the SRB. Basically whenever a tile is flagged we perform three steps: drawing the skool background in the backbuffer (draw_skool_tile), adding the sprites (draw_sprites) and dumping the backbuffer. We need to check if any of our sprites intersects with the tile being drawn and, if so, render the corresponding graphic (and mask) for the sprite tile using the animatory state. We will use the position of the sprite (column/row of the upper-left tile of the sprite – column/rows are, of course, in skool tile coordinates).
Here is the code:
Code: Select all
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Loops through the character list and updates the
; back buffer with corresponding the character sprite tile.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
draw_sprites
.(
; Use self-modifying code to have these values
; as immediate addressing and speed the loop up.
lda tile_col
sta smc_tilecol+1
lda tile_row
sta smc_tilerow+1
; Loop thorugh all the characters in the game
ldx #MAX_CHARACTERS-1
loop
; Check if the character overlaps the current tile
; start with the col
smc_tilecol
lda #0 ;tile_col
sec
sbc pos_col,x
bmi skip
cmp #4
bcs skip
sta smc_row+1 ; Save this for later, again self-modifying code
; now the row
smc_tilerow
lda #0 ;tile_row
sec
sbc pos_row,x
bmi skip
cmp #4
bcs skip
If the sprite does overlap, we need to get the ID of the tile which needs to be drawn. We have in register A the row and stored the column using self modifying code in the adc #x instruction below, so it is easy to get the tile code from row*4+column and the pointer to the current animatory state (as_pointer_low/high). If the tile code is zero we will skip drawing. Surely you already noticed there are quite a lot of empty tiles. Else the pointer to the graphic and mask will be the base pointer for tiles/masks plus the ID*8 (8 bytes per tile).
Notice how we obtain the pointer making use of the fact that everything has been aligned in memory at our convenience; and how we store it using self-modifying code as the parameter of the instruction at graphic_p (the ora) and mask_p (the and)
Code: Select all
; This sprite overlaps... time to draw it
; get tile offset in reg Y
asl
asl
; Carry is clear here
smc_row
adc #0
tay
; Get the UDG number
lda as_pointer_low,x
sta tmp
lda as_pointer_high,x
sta tmp+1
lda (tmp),y
beq skip ; If it is 0, then don't do anything
; Good, now get the pointer to the graphic and mask, that is multiplying index by 8
#ifdef FULLTABLEMUL8
tay
lda tab_mul8hi,y
sta tmp+1
lda tab_mul8,y
#else
ldy #0
sty tmp+1
asl
rol tmp+1
asl
rol tmp+1
asl
rol tmp+1
#endif
; If tiles are arranged smartly, we can do this...
; Carry must be clear here
sta graphic_p+1
sta mask_p+1
lda tmp+1
adc tab_tiles,x
sta graphic_p+2
lda tmp+1
adc tab_masks,x
sta mask_p+2
The multiplication by 8 can be done using a table (#define FULLTABLEMUL8), which will take memory or with rotations (which is slower). For now, I am using the former.
Here is the drawing loop. A few of things to notice here: first the usual sprite masking thingy (take background,
and mask and
ora graphic) and second the trick posted by Twilighte in another thread to support AIC coloring mode (basically when the background has the inverse bit set to attain more colors). This is the easy route: remove the bit (done in the mask) and invert the pixels. Some clashing will occur, of course. In fact we want this part of the code to be as fast as possible and those extra cycles do not help, but they are inevitable.
Code: Select all
; ... and copy it
ldy #7
loopcopy
lda backbuffer,y
#ifdef AIC_SUPPORT
bpl ScreenNoInverse
eor #63
ScreenNoInverse
#endif
mask_p
and $1234,y
graphic_p
ora $1234,y
sta backbuffer,y
dey
bpl loopcopy
Now go for the next character. Unfortunately “loop” is beyond a page, so a bpl loop cannot be used.
Code: Select all
; Time to get another character
skip
dex
bmi end
jmp loop
end
rts
.)
As a side note it is desirable to set up the code to avoid the jsr/rts as much as possible.
And that is all… nearly. We want to update the screen when our sprite moves, don’t we? This means marking the SRB correctly. Quite simple: flag the tiles occupied by our sprite as dirty, then move it (update animatory state and coordinates if necessary), and flag the new tiles occupied by our sprite as dirty aswell. Then let the engine do his work. We can move many sprites which may overlap. All of them will flag the needed bits in the SRB. In the rendering step we will redraw what is needed. Quite fast indeed!
This is the routine which flags the SRB bits for a sprite passed in reg X. It is quite straightforward, although may look ugly. Just remember we only flag a bit if the tilecode is not zero and that a sprite is 4x4. We operate column by column, keep two zero page pointers tmp0 pointing to the animatory state and tmp1 to the SRB byte and keep the correct bitmask for flagging the bit in tmp2. For each column we flag the bit for each of the 4 rows (I have unrolled this part).
Code: Select all
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Updates the SRB, marking tiles which need to
; be redrawn for a given sprite (reg X), depending
; on his animatory state.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
update_SRB_sp
.(
; First check if sprite is visible
lda pos_col,x
sec
sbc first_col
cmp #$FD ; (-3)
bmi endme
cmp #LAST_VIS_COL+1
bmi doit
endme
rts
doit
; Save col-first_col for later
sta sav_col+1
; Get the pointer to the animatoy state
lda as_pointer_low,x
sta tmp0
lda as_pointer_high,x
sta tmp0+1
; Prepare pointer to SRB
lda pos_row,x
asl
asl
adc pos_row,x
adc #<SRB
sta smc_srbpl+1
lda #>SRB
adc #0
sta smc_srbph+1
lda #4 ; Iterate through the 4 columns
sta tmp
loop
; Get pointer to SRB at the correct byte
; and also the correct bitmask
sav_col
lda #0
bmi skip
cmp #FIRST_VIS_COL
bcc skip
cmp #LAST_VIS_COL+1
bcs skip
lsr
lsr
lsr
clc
smc_srbpl
adc #0
sta tmp1
lda #0
smc_srbph
adc #0
sta tmp1+1
lda sav_col+1
and #%00000111
tay
lda tab_bit8,y
sta tmp2
ldy #0
lda (tmp0),y
beq skip1 ; Skip if tilecode is zero
; Mark the corresponding bit for this SRB
;y = 0 here
lda (tmp1),y
ora tmp2
sta (tmp1),y
skip1
ldy #4
lda(tmp0),y
beq skip2
; Mark the corresponding bit for this SRB
ldy #5
lda (tmp1),y
ora tmp2
sta (tmp1),y
skip2
ldy #8
lda(tmp0),y
beq skip3
; Mark the corresponding bit for this SRB
ldy #10
lda (tmp1),y
ora tmp2
sta (tmp1),y
skip3
ldy #12
lda(tmp0),y
beq skip
; Mark the corresponding bit for this SRB
ldy #15
lda (tmp1),y
ora tmp2
sta (tmp1),y
skip
inc tmp0
bne nocarry
inc tmp0+1
nocarry
inc sav_col+1
dec tmp
bne loop
end
rts
.)
From here the rest is quite easy. At least in concept. I keep variables with the animation frame number (so I cycle through 0,1,2,3,0,1… increasing/decreasing the pos_col variable at even values and use others for jumping, firing, hitting…) and keep pointers updated. I have a base pointer with animatory state 0 which may point to the one corresponding to the character looking left or looking right. Have a look at step_character or change_direction in engine.s or update_animstate in script.c, which updates the animatory state for a character passed in reg X to the one passed in reg A updating the SRB.
Did I leave anything behind? Any questions or comments? What do you want to read about next?