Metatiles and Dynamic Nametable Management [SOLVED]
Moderator: Moderators
Metatiles and Dynamic Nametable Management [SOLVED]
Hello everyone!
I'm trying to make a NES vertical shoot'em up and i'm facing challenges in the implementation of metatiles within the background. My goal is to establish a flexible method that allows for the management of metatiles of various sizes, not limited to 2x2 ( that is what i'm trying to do right now)
My main question is, how can I effectively and dynamically handle the offset of these metatiles to position them correctly in the Nametable? Is there a standard practice or a recommended technique for managing them?
If anyone has insights, advice, or techniques to share in this area, I would be greatly appreciative. Any tips or practical examples would be incredibly valuable. Thank you in advance for your assistance.
I'm trying to make a NES vertical shoot'em up and i'm facing challenges in the implementation of metatiles within the background. My goal is to establish a flexible method that allows for the management of metatiles of various sizes, not limited to 2x2 ( that is what i'm trying to do right now)
My main question is, how can I effectively and dynamically handle the offset of these metatiles to position them correctly in the Nametable? Is there a standard practice or a recommended technique for managing them?
If anyone has insights, advice, or techniques to share in this area, I would be greatly appreciative. Any tips or practical examples would be incredibly valuable. Thank you in advance for your assistance.
Last edited by rmonic79 on Thu Nov 23, 2023 1:59 am, edited 1 time in total.
Re: Metatiles and Dynamic Nametable Management
As with many things that are hardware-specific, this is largely a matter of converting "what the software uses" into "what the hardware needs", so the first thing you have to decide is what format you're gonna use for your metatiles. The most basic implementation of 2x2 metatiles is requires that you have 4 tile indices (1 byte each) plus 2 bits for the palette. It's not uncommon to assign logic attributes to metatiles too, such as solidity or special behaviors, and if 6 bits are enough for this this, you might want to pack that along with the palette bits. To make this data easier to index, a common approach is to use 5 separate arrays, so that you can use the same index to access any byte of a particular metatile. So now that we can use indices 0 to 255 to refer to metatiles, you can build your level using these indices. For the sake of simplicity we'll assume that levels are just a collection of 16-metatile rows, but in practice you might need to compress things further (RLE or LZ unpacking to RAM, pointers to rows you can resuse, or whatever).
Now, on the hardware side, in a vertical scrolling game you know you're gonna have to write rows of 32 tiles to the name tables, as well as rows of 8 attribute bytes. On the NES, one of the great complicators is the attribute table format, and the fact that the height of the name table is not a power of 2 or a multiple of the height of an attribute block, but there are ways to work around that.
Let's begin with the easy part - the tiles. Generally, you don't decode stuff straight to VRAM, because decoding logic can be complex and slow, so you're gonna need some buffers in RAM where you can temporarily put stuff. Since metatiles are 2 tiles high, I recommend you use 2 32-tile buffers, to save you from the hassle of splitting the tile updates in half.
So, as the game scrolls, you're gonna need to read metatiles from the level map, decode them into tiles, then write the tiles to VRAM. The first thing is to detect WHEN to do this. Generally, you look at the camera's (it's a scrolling game, so I assume you have a camera!) vertical coordinate: save a copy of CameraY before the camera moves, then, after moving, EOR the new CameraY to the old one. If bit 4 is set, this means that the camera crossed a 16-pixel boundary and you have to begin the update process.
To figure out what part of the level map you have to read, you can again use the camera's vertical coordinate (assuming it's at the top of the screen). If your level map is just a bunch of 16-metatile rows, each row will take 16 bytes, and the formula to calculate the address of the row you need is: MapBase + CameraY / METATILE_HEIGHT * ROW_SIZE. In this particular case, METATILE_HEIGHT is 16 pixels and ROW_SIZE is 16 bytes, so you don't even have to do any shifts, just clearing the lower 4 bits of CameraY will do the trick (do keep in mind that the actual calculations will be different with other metatiles sizes and level formats). Once you have the address, you need te read a byte from the row, use it as an index and copy the 4 tiles indices for that metatile to your RAM buffer. Here's a straightforward implementation of this:
Now that the buffers are full, you need to prepare to write them to VRAM. Where in VRAM? Well, this is where things start to complicate a bit. If the name tables were 32 tiles tall, we wouldn't have a problem, you could just use CameraY again. But as it turns out, Nintendo made the name tables 30 tiles tall, so the conversion between world coordinates and nametable coordinates is much more complex than it needed to be (basically you need a division by 15). Luckily we can opt to not do the conversion at all of we just maintain a second version of the camera's vertical coordinate, but this one is relative to the name tables rather then to the level map. As long as you update them in sync, all will be fine. The only difference with this one is that every time you update it, you have to test whether it landed on the "inexistent" nametable area (i.e. lower 8 bits > 239) and skip an extra 16 units if that's the case. Here's you can use this variable to calculate the target address for your rows of tiles in VRAM:
TopRowAddress = NTBase + (NTCameraY / 8) * 32
BottomRowAddress = TopRowAddress + 32
Now you have everything you need to update the name tables in your vblank handler.
We still need to tackle the attribute tables, but this post is already pretty long as it is, so let's make sure that we're good on everything regarding tiles before we move on to the more complicated part.
Now, on the hardware side, in a vertical scrolling game you know you're gonna have to write rows of 32 tiles to the name tables, as well as rows of 8 attribute bytes. On the NES, one of the great complicators is the attribute table format, and the fact that the height of the name table is not a power of 2 or a multiple of the height of an attribute block, but there are ways to work around that.
Let's begin with the easy part - the tiles. Generally, you don't decode stuff straight to VRAM, because decoding logic can be complex and slow, so you're gonna need some buffers in RAM where you can temporarily put stuff. Since metatiles are 2 tiles high, I recommend you use 2 32-tile buffers, to save you from the hassle of splitting the tile updates in half.
So, as the game scrolls, you're gonna need to read metatiles from the level map, decode them into tiles, then write the tiles to VRAM. The first thing is to detect WHEN to do this. Generally, you look at the camera's (it's a scrolling game, so I assume you have a camera!) vertical coordinate: save a copy of CameraY before the camera moves, then, after moving, EOR the new CameraY to the old one. If bit 4 is set, this means that the camera crossed a 16-pixel boundary and you have to begin the update process.
To figure out what part of the level map you have to read, you can again use the camera's vertical coordinate (assuming it's at the top of the screen). If your level map is just a bunch of 16-metatile rows, each row will take 16 bytes, and the formula to calculate the address of the row you need is: MapBase + CameraY / METATILE_HEIGHT * ROW_SIZE. In this particular case, METATILE_HEIGHT is 16 pixels and ROW_SIZE is 16 bytes, so you don't even have to do any shifts, just clearing the lower 4 bits of CameraY will do the trick (do keep in mind that the actual calculations will be different with other metatiles sizes and level formats). Once you have the address, you need te read a byte from the row, use it as an index and copy the 4 tiles indices for that metatile to your RAM buffer. Here's a straightforward implementation of this:
Code: Select all
ldx #$00
stx BlockIndex
DecodeBlock:
ldy BlockIndex
lda (RowAddress), y ;reads block index from map
iny
sty BlockIndex
tay
lda BlockTile0, y ;reads top left tile from metatile
sta TileBuffer0, x ;writes it to top tile buffer
lda BlockTile2, y ;reads bottom left tile from metatile
sta TileBuffer1, x ;writes it to bottom tile buffer
inx
lda BlockTile1, y ;reads top right tile from metatile
sta TileBuffer0, x ;writes it to top tile buffer
lda BlockTile3, y ;reads bottom right tile from metatile
sta TileBuffer1, x ;writes it to bottom tile buffer
inx
cpx #$20 ;tests if both buffers are full
bne DecodeBlock
TopRowAddress = NTBase + (NTCameraY / 8) * 32
BottomRowAddress = TopRowAddress + 32
Now you have everything you need to update the name tables in your vblank handler.
We still need to tackle the attribute tables, but this post is already pretty long as it is, so let's make sure that we're good on everything regarding tiles before we move on to the more complicated part.
Re: Metatiles and Dynamic Nametable Management
thanks for your reply tokumaru it can lead me to the right direction, i made a Startup code to try to fill the first nametable with only one metatile with your approach and it seems very useful, there's no scroll yet i'm trying to go on steps so i blocked it, above all cause i have to try to make the camera ( and try to understand what a camera should be on Nes
). by the way i wanna share my rough code just to have some feedback on the implementation, thank you to all 


Code: Select all
.include "constants.inc"
.export metatile_acqua, CopiaBuffersInVRAM
.importzp tempTileTopLeft, tempTileTopRight, tempTileBottomLeft, tempTileBottomRight, TileBuffer0, TileBuffer1
.importzp CameraY, OldCameraY, tempAttributes, tempNametableAddress, metatileDataIndex, pointerLo, pointerHi, BlockIndex, RowAddress
.import MapAddress1
; Queste sono le "etichette" ai metatiles
BlockTile0 = metatile_acqua
BlockTile1 = metatile_acqua + 1
BlockTile2 = metatile_acqua + 2
BlockTile3 = metatile_acqua + 3
; -------------------------
; PROCEDURA: CaricaMappaIniziale
; Descrizione: Carica la mappa iniziale nei buffer RAM.
; -------------------------
.proc CaricaMappaIniziale
PHP
PHA
TXA
PHA
TYA
PHA
; Imposta l'indirizzo iniziale della mappa
lda #<mappa ; Prende il byte basso dell'indirizzo di "mappa".
sta RowAddress
lda #>mappa ; Prende il byte alto dell'indirizzo di "mappa".
sta RowAddress+1
TAY
PLA
TAY
PLA
TAX
PLA
PLP
RTS
.endproc
; -------------------------
; PROCEDURA: DecodeMetatiles
; Descrizione: Decodifica i metatiles dalla mappa nei buffer RAM.
; -------------------------
.proc DecodeMetatiles
PHP
PHA
TXA
PHA
TYA
PHA
ldx #$00
stx BlockIndex
DecodeBlock:
ldy BlockIndex
lda (RowAddress), y ;reads block index from map
iny
sty BlockIndex
tay
lda BlockTile0, y ;reads top left tile from metatile
sta TileBuffer0, x ;writes it to top tile buffer
lda BlockTile2, y ;reads bottom left tile from metatile
sta TileBuffer1, x ;writes it to bottom tile buffer
inx
lda BlockTile1, y ;reads top right tile from metatile
sta TileBuffer0, x ;writes it to top tile buffer
lda BlockTile3, y ;reads bottom right tile from metatile
sta TileBuffer1, x ;writes it to bottom tile buffer
inx
cpx #$20 ;tests if both buffers are full
bne DecodeBlock
TAY
PLA
TAY
PLA
TAX
PLA
PLP
RTS
.endproc
.proc CopiaBuffersInVRAM
PHP
PHA
TXA
PHA
TYA
PHA
; Carica mappa nei buffer RAM
JSR CaricaMappaIniziale
; Decodifica i metatiles nei buffer
JSR DecodeMetatiles
; Reset PPU latch
LDA PPUSTATUS
; Settiamo l'indirizzo iniziale nella VRAM per la NameTable 0
LDA #$20
STA PPUADDR
LDA #$00
STA PPUADDR
LDY #$0F ; Numero di volte che i buffer verranno copiati
OuterLoop:
; Copia TileBuffer0 nella VRAM
LDX #$00
CopiaTileBuffer0:
LDA TileBuffer0, x
STA PPUDATA
INX
CPX #$20
BNE CopiaTileBuffer0
; Copia TileBuffer1 nella VRAM
LDX #$00
CopiaTileBuffer1:
LDA TileBuffer1, x
STA PPUDATA
INX
CPX #$20
BNE CopiaTileBuffer1
DEY
BNE OuterLoop
; Ripristina i registri e torna
TAY
PLA
TAY
PLA
TAX
PLA
PLP
RTS
.endproc
.segment "RODATA"
; Metatile Acqua
metatiles:
metatile_acqua: .byte $00, $01, $10, $11, %00000011
mappa:
.repeat 15
.byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
.endrepeat
Re: Metatiles and Dynamic Nametable Management
Could someone provide me with an example of how to implement a camera system? I'm considering using a 16-bit variable for the camera, as it needs to cover the entire level. Is there a more efficient way to achieve this? My plan is to update the camera position with each scroll event and then use another variable to determine when to draw the next set of tiles (horizontal pillars in my case) based on a certain number of pixels scrolled. Is this a correct approach?
Re: Metatiles and Dynamic Nametable Management
This experience is from OpenGL camera design so ymmv.
In the few OpenGL demos I've done, I've quickly implemented some sort of tracking logic in my camera handler, that way the camera is always by definition centered on and the follower of a distinct point in world space. This point could be a point inside a particular actor, like the center, resulting in a camera that moves and adjusts following a character. The point could also be an invisible tracking actor that has no model to speak of but simply exists as a camera anchor, which is useful for scripted camera tracks.
Since positioning of the camera wouldn't be an absolute matter, you can, in that scenario, quickly move it around by changing the reference point. An added feature could be making the "elasticity" of the camera in response to the movement of what it is following. In other words you define your camera in terms of how far its reference point can go in any direction before it starts to move in that direction, kinda like your camera has a leash connected to the thing it is tracking. You walk closer to that camera reference point, it doesn't move, but you go far enough in that direction and you'll start to tug it along. In 3D, this gets tricky though because you don't want to move an object you're tracking into the center of the camera tracking, then you clip through it into the middle and can't see it. Two approaches are to either put another tolerance in the other direction, if you get *closer* than xyz the camera moves *away*, or if you've got a collision engine in place, just give the invisible camera object an adequate collision sphere or cylinder, it'll move out of the way by the same rules as anything else.
Anywho, this is an alternative to simply tracking some sort of viewing area based on a specific actor in your engine. Likewise this is an exercise that even if you would only ever follow your character may help generalize your camera logic.
In the few OpenGL demos I've done, I've quickly implemented some sort of tracking logic in my camera handler, that way the camera is always by definition centered on and the follower of a distinct point in world space. This point could be a point inside a particular actor, like the center, resulting in a camera that moves and adjusts following a character. The point could also be an invisible tracking actor that has no model to speak of but simply exists as a camera anchor, which is useful for scripted camera tracks.
Since positioning of the camera wouldn't be an absolute matter, you can, in that scenario, quickly move it around by changing the reference point. An added feature could be making the "elasticity" of the camera in response to the movement of what it is following. In other words you define your camera in terms of how far its reference point can go in any direction before it starts to move in that direction, kinda like your camera has a leash connected to the thing it is tracking. You walk closer to that camera reference point, it doesn't move, but you go far enough in that direction and you'll start to tug it along. In 3D, this gets tricky though because you don't want to move an object you're tracking into the center of the camera tracking, then you clip through it into the middle and can't see it. Two approaches are to either put another tolerance in the other direction, if you get *closer* than xyz the camera moves *away*, or if you've got a collision engine in place, just give the invisible camera object an adequate collision sphere or cylinder, it'll move out of the way by the same rules as anything else.
Anywho, this is an alternative to simply tracking some sort of viewing area based on a specific actor in your engine. Likewise this is an exercise that even if you would only ever follow your character may help generalize your camera logic.
Re: Metatiles and Dynamic Nametable Management
I'm trying and trying but i can't make it work, any help would be appreciated.Thnaks.tokumaru wrote: ↑Mon Sep 11, 2023 4:41 am
Now that the buffers are full, you need to prepare to write them to VRAM. Where in VRAM? Well, this is where things start to complicate a bit. If the name tables were 32 tiles tall, we wouldn't have a problem, you could just use CameraY again. But as it turns out, Nintendo made the name tables 30 tiles tall, so the conversion between world coordinates and nametable coordinates is much more complex than it needed to be (basically you need a division by 15). Luckily we can opt to not do the conversion at all of we just maintain a second version of the camera's vertical coordinate, but this one is relative to the name tables rather then to the level map. As long as you update them in sync, all will be fine. The only difference with this one is that every time you update it, you have to test whether it landed on the "inexistent" nametable area (i.e. lower 8 bits > 239) and skip an extra 16 units if that's the case. Here's you can use this variable to calculate the target address for your rows of tiles in VRAM:
TopRowAddress = NTBase + (NTCameraY / 8) * 32
BottomRowAddress = TopRowAddress + 32
Now you have everything you need to update the name tables in your vblank handler.
We still need to tackle the attribute tables, but this post is already pretty long as it is, so let's make sure that we're good on everything regarding tiles before we move on to the more complicated part.
Re: Metatiles and Dynamic Nametable Management
After some efforts it seems that i reached the goal of tokumaru's advices, of course it only has one map and two metatiles but it seems a good starting point, now if anyone could give some hints about how to handle the 5fth byte with attribute and other stuff like collision it would be really appreciated.
https://www.youtube.com/watch?v=abnU5jwEgyI
this is the code that i made with his help if someone newbie like me needs some hints.
https://www.youtube.com/watch?v=abnU5jwEgyI
this is the code that i made with his help if someone newbie like me needs some hints.
Code: Select all
; -------------------------
; PROCEDURE: LoadInitialMap
; Description: Loads the initial map into RAM buffers.
; -------------------------
.proc LoadInitialMap
PHP
PHA
TXA
PHA
TYA
PHA
; Set the initial address of the map
lda #<map ; Get the low byte of the "map" address.
sta RowAddress
lda #>map ; Get the high byte of the "map" address.
sta RowAddress+1
; Start decoding
PLA
TAY
PLA
TAX
PLA
PLP
RTS
.endproc
; -------------------------
; PROCEDURE: DecodeMetatiles
; Description: Decodes metatiles from the map into RAM buffers.
; -------------------------
.proc DecodeMetatiles
PHP
PHA
TXA
PHA
TYA
PHA
ldx #$00 ; Initialize the buffer index
stx BlockIndex
DecodeBlock:
ldy BlockIndex ; Y = Map index
lda (RowAddress), y ; Load metatile index from the map
sta temp ; Save the metatile index in temp
iny
sty BlockIndex
; Calculate the offset for the metatile index
asl ; A = index * 2
asl ; A = index * 4
adc temp ; A = index * 5
tay ; Y = Offset for the metatile index
; Decode the metatile using the offset
lda metatiles, y ; Read the top-left tile
sta TileBuffer0, x ; Write it to the upper buffer
lda metatiles + 2, y ; Read the bottom-left tile
sta TileBuffer1, x ; Write it to the lower buffer
inx
lda metatiles + 1, y ; Read the top-right tile
sta TileBuffer0, x ; Write it to the upper buffer
lda metatiles + 3, y ; Read the bottom-right tile
sta TileBuffer1, x ; Write it to the lower buffer
inx
cpx #$20 ; Check if both buffers are full
bne DecodeBlock
PLA
TAY
PLA
TAX
PLA
PLP
RTS
.endproc
.proc CopyBuffersToVRAM
PHP
PHA
TXA
PHA
TYA
PHA
;LDA levelStarted ; Load the flag value into A
;BNE AlreadyInitialized ; If the flag is set (non-zero), exit the procedure immediately
LDA PPUSTATUS
JMP AlreadyInitialized ; If the flag is set (non-zero), exit the procedure immediately
; Set levelStarted to 1 because we're about to initialize
LDA #1
STA levelStarted
; Reset PPU latch
LDA PPUSTATUS
; Set the initial VRAM address for NameTable 0
LDA #$20
STA PPUADDR
LDA #$00
STA PPUADDR
LDY #$0F ; Number of times the buffers will be copied
OuterLoop:
; Copy TileBuffer0 to VRAM
LDX #$00
CopyTileBuffer0:
LDA TileBuffer0, x
STA PPUDATA
INX
CPX #$20
BNE CopyTileBuffer0
; Copy TileBuffer1 to VRAM
LDX #$00
CopyTileBuffer1:
LDA TileBuffer1, x
STA PPUDATA
INX
CPX #$20
BNE CopyTileBuffer1
DEY
BNE OuterLoop
LDA #$28
STA PPUADDR
LDA #$00
STA PPUADDR
LDY #$0F ; Number of times the buffers will be copied
OuterLoop2:
; Copy TileBuffer0 to VRAM
LDX #$00
CopyTileBuffer02:
LDA TileBuffer0, x
STA PPUDATA
INX
CPX #$20
BNE CopyTileBuffer02
; Copy TileBuffer1 to VRAM
LDX #$00
CopyTileBuffer12:
LDA TileBuffer1, x
STA PPUDATA
INX
CPX #$20
BNE CopyTileBuffer12
DEY
BNE OuterLoop2
; Restore the registers and return
AlreadyInitialized:
PLA
TAY
PLA
TAX
PLA
PLP
RTS
.endproc
.proc CopyPillarToVRAM
LDA startCopyVram
BEQ notDraw
;LDA PPUSTATUS
; Write the tiles to the PPU for TopRow
LDA TopRowAddress+1
STA PPUADDR
LDA TopRowAddress
STA PPUADDR
LDX #$00
LoopTop:
LDA TileBuffer0, x
STA PPUDATA
INX
CPX #$20 ; There are 16 tiles in a row of metatiles
BNE LoopTop
; Write the tiles to the PPU for BottomRow
LDA BottomRowAddress+1
STA PPUADDR
LDA BottomRowAddress
STA PPUADDR
LDX #$00
LoopBottom:
LDA TileBuffer1, x
STA PPUDATA
INX
CPX #$20 ; There are 16 tiles in a row of metatiles
BNE LoopBottom
notDraw:
LDA #255
CMP NTCamera
BEQ UpdateBase
BNE continua
UpdateBase:
LDA NTBase
EOR #$01
STA NTBase
LDA #239
STA NTCamera
continua:
.endproc
.proc VRAMCopyPrep
PHP
PHA
TXA
PHA
TYA
PHA
LDA NTBase
BEQ NTBaseZero
; NTBase is 1, so tempNAddress is $2800
LDA #$00 ; Load the low byte of $2800.
STA tempNAddress
LDA #$28 ; Load the high byte of $2800.
STA tempNAddress + 1
JMP CalculateTopRowAddress
NTBaseZero:
; NTBase is 0, so tempNAddress is $2000
LDA #$00 ; Load the low byte of $2000.
STA tempNAddress
LDA #$20 ; Load the high byte of $2000.
STA tempNAddress + 1
CalculateTopRowAddress:
LDY #0 ; Use Y as a temporary high byte
; Shift NTCamera right three times and track the overflow in NTCameraHi
CLC
LDA NTCamera
LSR A
ROR NTCameraHi
LSR A
ROR NTCameraHi
LSR A
ROR NTCameraHi
; Now perform the ASL
ASL A
ROL NTCameraHi
ASL A
ROL NTCameraHi
ASL A
ROL NTCameraHi
ASL A
ROL NTCameraHi
ASL A
ROL NTCameraHi
; Now A has the low byte, and NTCameraHi has the high byte
; Add with tempNAddress
ADC tempNAddress
STA TopRowAddress
LDA NTCameraHi
ADC tempNAddress+1
STA TopRowAddress+1
LDA TopRowAddress
CLC
ADC #$20
STA BottomRowAddress
LDA TopRowAddress + 1
ADC #$00
STA BottomRowAddress + 1
LDA #$01
STA startCopyVram
PLA
TAY
PLA
TAX
PLA
PLP
RTS
.endproc
; -------------------------
; PROCEDURE: UpdateCamera
; Description: Updates the Y position of the camera.
; -------------------------
.proc UpdateCamera
PHP
PHA
TXA
PHA
TYA
PHA
; Update NTCamera
;LDA NTCamera
;CLC
;ADC #1 ; Increment the camera
;CMP #240
;BCC EndUpdate
;ADC #15 ; Skip the extra 16 bytes if we go over 239
LDA NTCamera
SEC
SBC #1 ; Decrement the camera
BNE EndUpdate ; If NTCamera is not 0, continue.
EndUpdate:
STA NTCamera
JMP Continue
Continue:
CLC
LDA CameraY ; Load the low byte of CameraY
ADC #1 ; Increment by 1, taking into account the carry flag
STA CameraY ; Save the new value
LDA CameraY + 1 ; Load the high byte of CameraY
ADC #0 ; Add the carry flag from the previous increment, if present
STA CameraY + 1 ; Save the new value
; Compare the difference between the low byte of CameraY and OldCameraY
LDA CameraY
SEC
SBC OldCameraY
CMP #8
BCC continue
JSR DecodeMetatiles
continue:
CMP #16
BCC DoNotDraw
LDA CameraY
STA OldCameraY
JSR VRAMCopyPrep
DoNotDraw:
PLA
TAY
PLA
TAX
PLA
PLP
RTS
.endproc
.segment "RODATA"
; Water Metatile
metatiles:
metatile_water: .byte $00, $01, $10, $11, %00000011
metatile_Ground: .byte $FF, $FF, $FF, $FF, %00000011
map:
.byte 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Re: Metatiles and Dynamic Nametable Management
One trick you can do is to split your metatile data into separate arrays for each byte, rather than having all of the bytes of a metatile consecutive in a single array. That means you'd have MetatileTl, MetatileTr, MetatileBl, and MetatileBr arrays for the 4 tiles of the metatile, all using the same index to refer to the same metatile; you just load that index into X or Y and use it for each array. This makes it easy to add other properties, too; you could have other arrays with attributes or collision properties, indexed in the same way. It also allows you to easily have 256 metatiles, though if you want to have different sets of metatiles in different levels, you'll need to use indirect pointers to the arrays in zero page.
Re: Metatiles and Dynamic Nametable Management
Thanks fiskbit i'm trying your approach and it's great, by the way i can't found yet a proper way to write the attribute buffer after isolated the first two bit that rapresent palette, can you suggest me some approach? Everything i'm trying is a lot complicated and, at the end, doesn't work properly 

Re: Metatiles and Dynamic Nametable Management
Finally i think i made the attributes work, i'll share my code tomorrow to help somebody newbie like me and to have advice about it, i'm sure that will be a better way but at least it works 

Re: Metatiles and Dynamic Nametable Management
Ok i think i've done a good work about write metatile and attributes dinamically, now i need a hint to set the collision with background with this new approach and with scrolling, in the past (a months ago i think) i did for a single screen created using data stored in rom and without scrolling taking some Tokumaru helpful hints from another thread. Believe me i have no idea hot to manage collision with scrolling, or maybe i have some ideas but all of them seems overall complicated and data and cycle consuming, please i need help to go on.
Meantime i share some of the code i made to have feedback about it, sorry but the comments are in italian and i don't have time to translate them right now
This is what i do to prepare writing the horizontal pillar
And here the camera and setup before writing to vram with rodata at the end
And this is the result:
https://www.youtube.com/watch?v=swq010zJXHI
Meantime i share some of the code i made to have feedback about it, sorry but the comments are in italian and i don't have time to translate them right now
This is what i do to prepare writing the horizontal pillar
Code: Select all
.proc UpdateMap
; Save registers
PHP
PHA
TXA
PHA
TYA
PHA
; Check levelCounter and update if it's 0 or 15
LDA levelCounter
BEQ setFromMapSequence ; If levelCounter is 0, set currentRowIndex from MapSequence
CMP #15
BNE skipUpdate
; Reset levelCounter
LDA #$00
STA levelCounter
; If levelCounter is reset, fetch the next value from MapSequence
setFromMapSequence:
LDY nametableCounter
LDA MapSequence, Y
STA temp
; Multiply this value by 15 to get the offset for mapIndex
ASL A ; x2
ASL A ; x4
ASL A ; x8
ASL A ; x16
SEC
SBC temp
STA currentRowIndex
; Increment nametableCounter for the next nametable
INC nametableCounter
skipUpdate:
; Increment levelCounter
INC levelCounter
; Load value from mapIndex using currentRowIndex
LDY currentRowIndex
LDA mapIndex, Y
; Multiply by 16 to get the offset into the metatile data
ASL A
ASL A
ASL A
ASL A
ROL tempOffset+1
STA tempOffset ; Store the offset low byte
; Calculate the low byte of the address
LDA #<mappa
CLC
ADC tempOffset
STA RowAddress
; Calculate the high byte of the address
LDA #>mappa
ADC tempOffset+1
STA RowAddress+1
; Increment currentRowIndex for next time
INC currentRowIndex
; Restore registers and return
PLA
TAY
PLA
TAX
PLA
PLP
RTS
.endproc
; -------------------------
; PROCEDURA: DecodeMetatiles
; Descrizione: Decodifica i metatiles dalla mappa nei buffer RAM.
; -------------------------
.proc DecodeMetatiles
PHP
PHA
TXA
PHA
TYA
PHA
LDX #$00 ; Inizializza l'indice del buffer
STX BlockIndex
DecodeBlock:
LDY BlockIndex ; Y = Indice della mappa
LDA (RowAddress), y ; Carica indice metatile dalla mappa
INY
STY BlockIndex
TAY ; Y = Offset per l'indice del metatile
LDA MetatileTl, y ; Legge il tile in alto a sinistra
STA TileBuffer0, x ; Scrive nel buffer superiore
LDA MetatileBl, y ; Legge il tile in basso a sinistra
STA TileBuffer1, x ; Scrive nel buffer inferiore
INX
LDA MetatileTr, y ; Legge il tile in alto a destra
STA TileBuffer0, x ; Scrive nel buffer superiore
LDA MetatileBr, y ; Legge il tile in basso a destra
STA TileBuffer1, x ; Scrive nel buffer inferiore
INX
CPX #$20 ; Verifica se entrambi i buffer sono pieni
BNE DecodeBlock
PLA
TAY
PLA
TAX
PLA
PLP
RTS
.endproc
.proc ProcessAttribute
PHP
PHA
TXA
PHA
TYA
PHA
LDX #$00 ; Inizializza l'indice del buffer
STX BlockIndex
STX counterAttrAlt
STX SaveYAttribute
STX SaveYIndex
DecodeBlock:
LDY BlockIndex ; Y = Indice della mappa
LDA (RowAddress), y ; Carica indice metatile dalla mappa
INY
STY BlockIndex
TAY ; Y = Offset per l'indice del metatile
LDA MetatileSetting, Y ;legge l'attributo all'indice Y
AND #%00000011 ; estrae i byte della palette 0-1
STA paletteTemp
LDA timingAttributeBool
BNE lowRowBits
LDY SaveYAttribute
LDA counterAttrAlt
CMP #$00
BNE firstBitLow
LDA paletteTemp
ORA AttributeBuffer, Y
STA AttributeBuffer, y
STY SaveYAttribute
firstBitLow:
LDA counterAttrAlt
CMP #$01
BNE outSecondBitLow
LDA paletteTemp
ASL
ASL
ORA AttributeBuffer, y
STA AttributeBuffer, y
INY
STY SaveYAttribute
outSecondBitLow:
LDA counterAttrAlt
EOR #$01
STA counterAttrAlt
INX
CPX #$20 ; Verifica se entrambi i buffer sono pieni
BNE DecodeBlock
lowRowBits:
LDA timingAttributeBool
BEQ exit
LDY SaveYAttribute
LDA counterAttrAlt
CMP #$00
BNE firstBitHi
LDA paletteTemp
ASL
ASL
ASL
ASL
ORA AttributeBuffer, Y
STA AttributeBuffer, y
STY SaveYAttribute
firstBitHi:
LDA counterAttrAlt
CMP #$01
BNE outSecondBitHi
LDA paletteTemp
ASL
ASL
ASL
ASL
ASL
ASL
ORA AttributeBuffer, y
STA AttributeBuffer, y
INY
STY SaveYAttribute
outSecondBitHi:
LDA counterAttrAlt
EOR #$01
STA counterAttrAlt
INX
CPX #$20 ; Verifica se entrambi i buffer sono pieni
BNE DecodeBlock
exit:
PLA
TAY
PLA
TAX
PLA
PLP
RTS
.endproc
.proc clearAttributeBuffer
PHP
PHA
TXA
PHA
LDA timingAttributeBool
CMP #$00
BEQ fine
clear:
LDX #$00
LDA #$00
clearLoop:
STA AttributeBuffer, X
INX
CPX #$8 ; o qualunque sia la dimensione del buffer
BNE clearLoop
fine:
LDA MetatileCounter
CMP #$08
BNE continua
LDA #$00
STA MetatileCounter
JMP clear
continua:
PLA
TAX
PLA
PLP
RTS
.endproc
Code: Select all
.proc VRAMCopyPrep
PHP
PHA
TXA
PHA
TYA
PHA
LDA NTBase
BEQ NTBaseZero
; NTBase è 1, quindi tempNAddress è $2800
LDA #$00 ; Carica il byte meno significativo di $2800.
STA tempNAddress
LDA #$20 ; Carica il byte più significativo di $2800.
STA tempNAddress + 1
JMP CalculateTopRowAddress
NTBaseZero:
; NTBase è 0, quindi tempNAddress è $2000
LDA #$00 ; Carica il byte meno significativo di $2000.
STA tempNAddress
LDA #$28 ; Carica il byte più significativo di $2000.
STA tempNAddress + 1
CalculateTopRowAddress:
LDY #0 ; Usiamo Y come byte alto temporaneo
; LSR tre volte su NTCamera e traccia l'overflow in NTCameraHi
CLC
LDA NTCamera
LSR A
ROR NTCameraHi
LSR A
ROR NTCameraHi
LSR A
ROR NTCameraHi
; Ora eseguiamo gli ASL
ASL A
ROL NTCameraHi
ASL A
ROL NTCameraHi
ASL A
ROL NTCameraHi
ASL A
ROL NTCameraHi
ASL A
ROL NTCameraHi
; Ora A ha il byte basso e NTCameraHi ha il byte alto
; Somma con tempNAddress
ADC tempNAddress
STA TopRowAddress
LDA NTCameraHi
ADC tempNAddress+1
STA TopRowAddress+1
LDA TopRowAddress
CLC
ADC #$20
STA BottomRowAddress
LDA TopRowAddress + 1
ADC #$00
STA BottomRowAddress + 1
LDA #$00
STA calculatedOffset
LDX rowIndex
LDA OffsetTable, X
STA calculatedOffset
LDA OffsetTableHi, X
STA calculatedOffset + 1
LDA TopRowAddress
CLC
ADC calculatedOffset
STA AttributeAddressLo
LDA TopRowAddress + 1
CLC
ADC calculatedOffset + 1
STA AttributeAddressHi
LDA #$01
STA startCopyVram
PLA
TAY
PLA
TAX
PLA
PLP
RTS
.endproc
; -------------------------
; PROCEDURA: AggiornaCamera
; Descrizione: Aggiorna la posizione Y della camera.
; -------------------------
.proc AggiornaCamera
PHP
PHA
TXA
PHA
TYA
PHA
; Aggiornare NTCamera
LDA NTCamera
SEC
SBC #1 ; Decrementiamo la camera
BNE FineAggiornamento ; Se NTCamera non è 0, prosegui.
FineAggiornamento:
STA NTCamera
JMP Continue
Continue:
CLC
LDA CameraY ; Carica il byte meno significativo di CameraY
ADC #1 ; Incrementa di 1, tenendo conto del flag di carry
STA CameraY ; Salva il nuovo valore
LDA CameraY + 1 ; Carica il byte più significativo di CameraY
ADC #0 ; Aggiunge il flag di carry del precedente incremento, se presente
STA CameraY + 1 ; Salva il nuovo valore
;Confronta la differenza tra il byte meno significativo di CameraY e OldCameraY
LDA CameraY
SEC
SBC OldCameraY
CMP #4
BNE notClear
JSR clearAttributeBuffer
notClear:
CMP #10
BNE decode
JSR UpdateMap
decode:
CMP #14
BNE continua
JSR DecodeMetatiles
JSR ProcessAttribute
continua:
CMP #16
BCC NonDisegnare
LDA CameraY
STA OldCameraY
JSR VRAMCopyPrep
NonDisegnare:
PLA
TAY
PLA
TAX
PLA
PLP
RTS
.endproc
.segment "RODATA"
; Dividiamo i dati del metatile in quattro array separati
;Water Ice Wall L-R
MetatileTl: ; Angolo in alto a sinistra
.byte $00, $02, $EE, $CC
MetatileTr: ; Angolo in alto a destra
.byte $01, $03, $EF, $CD
MetatileBl: ; Angolo in basso a sinistra
.byte $10, $12, $FE, $DC
MetatileBr: ; Angolo in basso a destra
.byte $11, $13, $FF, $DD
; Aggiungiamo anche un array per le proprietà di collisione
MetatileSetting: ;byte 0-1: palette, byte 2: solid
.byte %00000000, %00000000, %00000011, %00000011
mappa:
MetAllWater: .byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
MetRWall: .byte 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0
MetLWall: .byte 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
MetLRWall: .byte 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2
mapIndex:
.byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
.byte 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2
.byte 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
.byte 1, 1, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3
.byte 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1
MapSequence:
levelOne: .byte 0,1,2,0,1,2,0,1,2,0,1,2
OffsetTable:
.byte $78, $F0, $68, $E0, $58, $D0, $48, $C0
OffsetTableHi:
.byte $00, $00, $01, $01, $02, $02, $03, $03
https://www.youtube.com/watch?v=swq010zJXHI
- donato-zits-
- Posts: 46
- Joined: Fri Jun 03, 2022 11:14 am
- Contact:
Re: Metatiles and Dynamic Nametable Management
I think that I will try to implement that for test solid destructible blocks in a single-screen...could work?rmonic79 wrote: ↑Tue Sep 19, 2023 12:25 amCode: Select all
.include "constants.inc" .export metatile_acqua, CopiaBuffersInVRAM .importzp tempTileTopLeft, tempTileTopRight, tempTileBottomLeft, tempTileBottomRight, TileBuffer0, TileBuffer1 .importzp CameraY, OldCameraY, tempAttributes, tempNametableAddress, metatileDataIndex, pointerLo, pointerHi, BlockIndex, RowAddress .import MapAddress1 ; Queste sono le "etichette" ai metatiles BlockTile0 = metatile_acqua BlockTile1 = metatile_acqua + 1 BlockTile2 = metatile_acqua + 2 BlockTile3 = metatile_acqua + 3 ; ------------------------- ; PROCEDURA: CaricaMappaIniziale ; Descrizione: Carica la mappa iniziale nei buffer RAM. ; ------------------------- .proc CaricaMappaIniziale PHP PHA TXA PHA TYA PHA ; Imposta l'indirizzo iniziale della mappa lda #<mappa ; Prende il byte basso dell'indirizzo di "mappa". sta RowAddress lda #>mappa ; Prende il byte alto dell'indirizzo di "mappa". sta RowAddress+1 TAY PLA TAY PLA TAX PLA PLP RTS .endproc ; ------------------------- ; PROCEDURA: DecodeMetatiles ; Descrizione: Decodifica i metatiles dalla mappa nei buffer RAM. ; ------------------------- .proc DecodeMetatiles PHP PHA TXA PHA TYA PHA ldx #$00 stx BlockIndex DecodeBlock: ldy BlockIndex lda (RowAddress), y ;reads block index from map iny sty BlockIndex tay lda BlockTile0, y ;reads top left tile from metatile sta TileBuffer0, x ;writes it to top tile buffer lda BlockTile2, y ;reads bottom left tile from metatile sta TileBuffer1, x ;writes it to bottom tile buffer inx lda BlockTile1, y ;reads top right tile from metatile sta TileBuffer0, x ;writes it to top tile buffer lda BlockTile3, y ;reads bottom right tile from metatile sta TileBuffer1, x ;writes it to bottom tile buffer inx cpx #$20 ;tests if both buffers are full bne DecodeBlock TAY PLA TAY PLA TAX PLA PLP RTS .endproc .proc CopiaBuffersInVRAM PHP PHA TXA PHA TYA PHA ; Carica mappa nei buffer RAM JSR CaricaMappaIniziale ; Decodifica i metatiles nei buffer JSR DecodeMetatiles ; Reset PPU latch LDA PPUSTATUS ; Settiamo l'indirizzo iniziale nella VRAM per la NameTable 0 LDA #$20 STA PPUADDR LDA #$00 STA PPUADDR LDY #$0F ; Numero di volte che i buffer verranno copiati OuterLoop: ; Copia TileBuffer0 nella VRAM LDX #$00 CopiaTileBuffer0: LDA TileBuffer0, x STA PPUDATA INX CPX #$20 BNE CopiaTileBuffer0 ; Copia TileBuffer1 nella VRAM LDX #$00 CopiaTileBuffer1: LDA TileBuffer1, x STA PPUDATA INX CPX #$20 BNE CopiaTileBuffer1 DEY BNE OuterLoop ; Ripristina i registri e torna TAY PLA TAY PLA TAX PLA PLP RTS .endproc .segment "RODATA" ; Metatile Acqua metatiles: metatile_acqua: .byte $00, $01, $10, $11, %00000011 mappa: .repeat 15 .byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 .endrepeat
edit:this 5th byte is for sure a trouble, where and how to store it, and affter how to make the relation with the player sprite
viewtopic.php?t=24252 <<<<my game
https://mega.nz/file/XapTCCiS#jBcf5oqDG ... M2sgnWv9OA <<BIOHAZARD 8BITS-my"compilingtryingrest" in nesmaker
https://mega.nz/file/XapTCCiS#jBcf5oqDG ... M2sgnWv9OA <<BIOHAZARD 8BITS-my"compilingtryingrest" in nesmaker
Re: Metatiles and Dynamic Nametable Management
If you need only s single screen without scroll i would suggest this thread viewtopic.php?t=24797donato-zits- wrote: ↑Thu Oct 26, 2023 8:31 amI think that I will try to implement that for test solid destructible blocks in a single-screen...could work?rmonic79 wrote: ↑Tue Sep 19, 2023 12:25 amCode: Select all
.include "constants.inc" .export metatile_acqua, CopiaBuffersInVRAM .importzp tempTileTopLeft, tempTileTopRight, tempTileBottomLeft, tempTileBottomRight, TileBuffer0, TileBuffer1 .importzp CameraY, OldCameraY, tempAttributes, tempNametableAddress, metatileDataIndex, pointerLo, pointerHi, BlockIndex, RowAddress .import MapAddress1 ; Queste sono le "etichette" ai metatiles BlockTile0 = metatile_acqua BlockTile1 = metatile_acqua + 1 BlockTile2 = metatile_acqua + 2 BlockTile3 = metatile_acqua + 3 ; ------------------------- ; PROCEDURA: CaricaMappaIniziale ; Descrizione: Carica la mappa iniziale nei buffer RAM. ; ------------------------- .proc CaricaMappaIniziale PHP PHA TXA PHA TYA PHA ; Imposta l'indirizzo iniziale della mappa lda #<mappa ; Prende il byte basso dell'indirizzo di "mappa". sta RowAddress lda #>mappa ; Prende il byte alto dell'indirizzo di "mappa". sta RowAddress+1 TAY PLA TAY PLA TAX PLA PLP RTS .endproc ; ------------------------- ; PROCEDURA: DecodeMetatiles ; Descrizione: Decodifica i metatiles dalla mappa nei buffer RAM. ; ------------------------- .proc DecodeMetatiles PHP PHA TXA PHA TYA PHA ldx #$00 stx BlockIndex DecodeBlock: ldy BlockIndex lda (RowAddress), y ;reads block index from map iny sty BlockIndex tay lda BlockTile0, y ;reads top left tile from metatile sta TileBuffer0, x ;writes it to top tile buffer lda BlockTile2, y ;reads bottom left tile from metatile sta TileBuffer1, x ;writes it to bottom tile buffer inx lda BlockTile1, y ;reads top right tile from metatile sta TileBuffer0, x ;writes it to top tile buffer lda BlockTile3, y ;reads bottom right tile from metatile sta TileBuffer1, x ;writes it to bottom tile buffer inx cpx #$20 ;tests if both buffers are full bne DecodeBlock TAY PLA TAY PLA TAX PLA PLP RTS .endproc .proc CopiaBuffersInVRAM PHP PHA TXA PHA TYA PHA ; Carica mappa nei buffer RAM JSR CaricaMappaIniziale ; Decodifica i metatiles nei buffer JSR DecodeMetatiles ; Reset PPU latch LDA PPUSTATUS ; Settiamo l'indirizzo iniziale nella VRAM per la NameTable 0 LDA #$20 STA PPUADDR LDA #$00 STA PPUADDR LDY #$0F ; Numero di volte che i buffer verranno copiati OuterLoop: ; Copia TileBuffer0 nella VRAM LDX #$00 CopiaTileBuffer0: LDA TileBuffer0, x STA PPUDATA INX CPX #$20 BNE CopiaTileBuffer0 ; Copia TileBuffer1 nella VRAM LDX #$00 CopiaTileBuffer1: LDA TileBuffer1, x STA PPUDATA INX CPX #$20 BNE CopiaTileBuffer1 DEY BNE OuterLoop ; Ripristina i registri e torna TAY PLA TAY PLA TAX PLA PLP RTS .endproc .segment "RODATA" ; Metatile Acqua metatiles: metatile_acqua: .byte $00, $01, $10, $11, %00000011 mappa: .repeat 15 .byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 .endrepeat
edit:this 5th byte is for sure a trouble, where and how to store it, and affter how to make the relation with the player sprite
Re: Metatiles and Dynamic Nametable Management
Guys i'm really going crazy, i made a 30 byte bitmap for collisions based on metatile pillar but how can i deal with scroll? Please i really need help
- donato-zits-
- Posts: 46
- Joined: Fri Jun 03, 2022 11:14 am
- Contact:
Re: Metatiles and Dynamic Nametable Management
no, I already got to implement colisions with bitmask, for learn well that I recomend that video ,was where I learn all the stuuf about it... but my intention now is to make a lot os blocks that the player could destroy in a digger action, something like a cave ambient; to a great amount of destructible terrain blocks or for a scroll game the bitmask colision map do not work
viewtopic.php?t=24252 <<<<my game
https://mega.nz/file/XapTCCiS#jBcf5oqDG ... M2sgnWv9OA <<BIOHAZARD 8BITS-my"compilingtryingrest" in nesmaker
https://mega.nz/file/XapTCCiS#jBcf5oqDG ... M2sgnWv9OA <<BIOHAZARD 8BITS-my"compilingtryingrest" in nesmaker