Tsutarja wrote:
You probably don't need to make example code out of the whole process
Oops, too late! Long ass post ahead! I started coding the important parts but ended up doing the whole thing because I wanted to see it everything fit together. It's all untested though, so follow with caution. You don't have to follow everything I did step by step, this is just an example of how I'd do things considering the compression format you've chosen. You might want to do things differently in some places, but it's important that you understand what's going on. I also separated the actual metatile decoding to its own subroutine, so it can be used for rendering the initial screen too.
Code:
;decide whether to render a metatile
lda OldCameraX+0
eor CameraX+0
and #%00000100
beq Done
;detect the direction of the movement
lda OldCameraX+0
cmp CameraX+0
lda OldCameraX+1
sbc CameraX+1
bcc MovedRight
MovedLeft:
;calculate the position of the column to the left of the camera
;sec (omitted because the carry is always set by this point)
lda CameraX+0 ;subtract 32 ($0020)
sbc #$20
sta ColumnX+0
lda CameraX+1
sbc #$00
sta ColumnX+1
jmp ColumnReady
MovedRight:
;calculate the position of the column to the right of the camera
;clc (omitted because the carry is always clear by this point)
lda CameraX+0 ;add 255 + 32 = 287 ($011F)
adc #$1F
sta ColumnX+0
lda CameraX+1
adc #$01
sta ColumnX+1
ColumnReady:
;go decode the metatile
jsr DecodeMetatile
Done:
And here's the subroutine:
Code:
DecodeMetatile:
;return if the column is out of bounds
lda ColumnX+1
cmp LevelLength
bcc Continue
rts
Continue:
;use the screen index to set up a pointer to the screen we'll read from
asl
tay
lda (LevelPointer), y
sta ScreenPointer+0
iny
lda (LevelPointer), y
sta ScreenPointer+1
;set up the indices needed to access pre-calculated values
lda ColumnX+0
and #%11111100
lsr
tay ;index for tables of words
lsr
tax ;index for tables of bytes
;prepare the bit that will be used for name table selection
lda ColumnX+1
and #%00000001 ;keep only the bit that selects 1 of the 2 name tables
asl
asl
sta Temp
;prepare the name table address
lda NameTableAddresses+0, y
sta NameTableAddress+0
lda NameTableAddresses+1, y
ora Temp
sta NameTableAddress+1
;prepare the attribute table address
lda AttributeTableAddresses, x
sta AttributeTableAddress+0
lda #%00100011
ora Temp
sta AttributeTableAddress+1
;get the index of the metatile within the screen
ldy MetatileIndices, x
;set up a pointer to the metatile
lda (ScreenPointer), y
sta MetatilePointer+0
iny
lda (ScreenPointer), y
sta MetatilePointer+1
;OMITTED: SWITCH TO BUFFER STACK;
;OMITTED: SET UP VRAM UPDATE USING THE ATTRIBUTE TABLE ADDRESS;
;put the attribute byte in the buffer
ldy #ATTRIBUTE_OFFSET
lda (MetatilePointer), y
pha
;prepare to read the first tile of the metatile
ldy #$00
BufferRow:
;OMITTED: SET UP VRAM UPDATE USING THE NAME TABLE ADDRESS;
;put 4 tiles in the buffer
lda (MetatilePointer), y
pha
iny
lda (MetatilePointer), y
pha
iny
lda (MetatilePointer), y
pha
iny
lda (MetatilePointer), y
pha
iny
;check if all 16 tiles have already been processed
cpy #$10
beq Done
;move the output position one row down
clc
lda NameTableAddress+0
adc #$20
sta NameTableAddress+0
lda NameTableAddress+1
adc #$00
sta NameTableAddress+1
;process another row if we didn't invade the attribute tables (happens with the last metatile)
lda NameTableAddress+0
and #%11000000
cmp #%11000000
bne BufferRow
lda NameTableAddress+1
and #%00000011
cmp #%00000011
bne BufferRow
Done:
;OMITTED: SWITCH TO THE NORMAL STACK;
;return
rts
Note that this is reading pointers stored as words, not split into bytes like you originally intended. This means you can define levels like this:
Code:
Level0:
.dw Screen00, Screen01, Screen00, Screen03, Screen03, Screen01
And screens like this:
Code:
Screen00:
.dw Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00
.dw Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00
.dw Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00
.dw Metatile00, Metatile00, Metatile00, Metatile00, Metatile03, Metatile03, Metatile00, Metatile03
.dw Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00
.dw Metatile03, Metatile03, Metatile03, Metatile00, Metatile03, Metatile03, Metatile03, Metatile03
.dw Metatile02, Metatile02, Metatile02, Metatile00, Metatile02, Metatile02, Metatile02, Metatile02
.dw Metatile02, Metatile02, Metatile02, Metatile00, Metatile02, Metatile02, Metatile02, Metatile02
Which is much easier to write by hand than splitting the high and low bytes of each pointer. You'd probably go crazy trying to design levels the split way!
Also, I owe you some explanations about how the addresses are set up, since I used tables to solve this part. First I'm gonna tell you how we're finding the position of the metatile that will be updated, and then how all the addresses and indices are calculated from that position.
There are only 8 columns of metatiles per screen, but since updating all 8 metatiles of a column would take much more time than available during VBlank, we'll only update 1 metatile at a time. This means we need 8 updates to complete a column, and since each column is 32 pixels wide, we'll have to update one metatile every 32 / 8 = 4 pixels.
We could use a separate counter to keep track of how many metatiles we've already updated, but we already have this info implied in the column coordinate. You can think of the bits in the column coordinate like this:
Code:
SSSSSSSS MMM444PP
SSSSSSSS: index of the screen within the level (0 to 255);
MMM: index of the metatile column (0 to 7);
444: index of the 4-pixel column within the metatile column (0 to 7);
PP: index of the pixel column within the 4-pixel column (0 to 3);
OK, so we already know the column where the metatile we'll be processing is, but we still need to figure out its row. We have absolutely no use for the pixel index, but the 4-pixel column index can be REPURPOSED as the row of the metatile, since it will count from 0 to 7 during the course of one metatile column, which is exactly what we need. We'll be using this to count rows instead of a separate variable, so we'll treat the coordinate like this:
Code:
SSSSSSSS CCCRRR**
SSSSSSSS: index of the screen within the level (0 to 255);
CCC: column of the metatile within the screen;
RRR: row of the metatile within the screen;
**: not used;
Since we now know the column and the row of the metatile being processed, we can use that information to generate all the pointers and indices necessary to access it, we just need to move the bits around. This could be done with bit shifting and bitwise operations, but that could be slow and difficult to understand. To avoid that, I decided to use tables. To index these tables I used the position of the metatile exactly like it's arranged in the low byte of the column index, but shifted according to the size of the entries of each table (
0CCCRRR0 for words,
00CCCRRR for bytes).
To read from the screens, you have to think of the format in which they're stored: a grid of 8x8 words. This means that indices to read screens are in the following format:
Code:
0RRRCCC0
The formula used to find this format is
(y * 8 + x) * 2. This is the good old formula used to convert 2D to 1D, and the multiplication by 2 is there because each entry is 2 bytes (a pointer). So there's one table (MetatileIndices) that converts
00CCCRRR into
0RRRCCC0.
Another thing we need to find out is the target address for the metatile in the name tables. Name table addresses are in the following format (this was defined by Nintendo):
Code:
0010YXYY YYYXXXXX
This is a bit more complicated because we have a base address (name tables start at $2000, not $0000), and because the top X and Y bits are separated from the rest and given more relevance, for name table selection. We can ignore that when creating the tables (to keep their size down) and assume that all addresses are in the first name table ($2000), and change the name table bit after reading the address from the table. Anyway, the conversion goes like this:
Code:
0CCCRRR0 (index)
001000RR R00CCC00 (NT address)
The last table we need converts the index into an attribute table address. AT addresses are in the folowing format (again, defined by Nintendo):
Code:
0010YX11 11YYYXXX
Which means that the conversion goes like this:
Code:
0CCCRRR0 (index)
00100011 11RRRCCC (AT address)
Like I said before, you could do these conversions in real time instead of using tables, but I wanted to keep the routine fast, and didn't want to confuse you with all the bit shifting. But you can decide to go that route if you don't want to waste space with these tables. The important thing is that the bits end up where they have to be.
I'll give you all the tables, but there might be errors since I wrote them manually instead of writing a script to generate them. Each table has 64 entries, but one uses words while the other 2 use bytes, for a total of 256 btes worth of tables.
Code:
NameTableAddresses:
.dw %0010000000000000, %0010000010000000, %0010000100000000, %0010000110000000, %0010001000000000, %0010001010000000, %0010001100000000, %0010001110000000
.dw %0010000000000100, %0010000010000100, %0010000100000100, %0010000110000100, %0010001000000100, %0010001010000100, %0010001100000100, %0010001110000100
.dw %0010000000001000, %0010000010001000, %0010000100001000, %0010000110001000, %0010001000001000, %0010001010001000, %0010001100001000, %0010001110001000
.dw %0010000000001100, %0010000010001100, %0010000100001100, %0010000110001100, %0010001000001100, %0010001010001100, %0010001100001100, %0010001110001100
.dw %0010000000010000, %0010000010010000, %0010000100010000, %0010000110010000, %0010001000010000, %0010001010010000, %0010001100010000, %0010001110010000
.dw %0010000000010100, %0010000010010100, %0010000100010100, %0010000110010100, %0010001000010100, %0010001010010100, %0010001100010100, %0010001110010100
.dw %0010000000011000, %0010000010011000, %0010000100011000, %0010000110011000, %0010001000011000, %0010001010011000, %0010001100011000, %0010001110011000
.dw %0010000000011100, %0010000010011100, %0010000100011100, %0010000110011100, %0010001000011100, %0010001010011100, %0010001100011100, %0010001110011100
AttributeTableAddresses:
.db %11000000, %11001000, %11010000, %11011000, %11100000, %11101000, %11110000, %11111000
.db %11000001, %11001001, %11010001, %11011001, %11100001, %11101001, %11110001, %11111001
.db %11000010, %11001010, %11010010, %11011010, %11100010, %11101010, %11110010, %11111010
.db %11000011, %11001011, %11010011, %11011011, %11100011, %11101011, %11110011, %11111011
.db %11000100, %11001100, %11010100, %11011100, %11100100, %11101100, %11110100, %11111100
.db %11000101, %11001101, %11010101, %11011101, %11100101, %11101101, %11110101, %11111101
.db %11000110, %11001110, %11010110, %11011110, %11100110, %11101110, %11110110, %11111110
.db %11000111, %11001111, %11010111, %11011111, %11100111, %11101111, %11110111, %11111111
MetatileIndices:
.db %00000000, %00010000, %00100000, %00110000, %01000000, %01010000, %01100000, %01110000
.db %00000010, %00010010, %00100010, %00110010, %01000010, %01010010, %01100010, %01110010
.db %00000100, %00010100, %00100100, %00110100, %01000100, %01010100, %01100100, %01110100
.db %00000110, %00010110, %00100110, %00110110, %01000110, %01010110, %01100110, %01110110
.db %00001000, %00011000, %00101000, %00111000, %01001000, %01011000, %01101000, %01111000
.db %00001010, %00011010, %00101010, %00111010, %01001010, %01011010, %01101010, %01111010
.db %00001100, %00011100, %00101100, %00111100, %01001100, %01011100, %01101100, %01111100
.db %00001110, %00011110, %00101110, %00111110, %01001110, %01011110, %01101110, %01111110