Metatiles and Dynamic Nametable Management [SOLVED]

Are you new to 6502, NES, or even programming in general? Post any of your questions here. Remember - the only dumb question is the question that remains unasked.

Moderator: Moderators

User avatar
rmonic79
Posts: 26
Joined: Sat Aug 26, 2023 2:55 am

Metatiles and Dynamic Nametable Management [SOLVED]

Post by rmonic79 »

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.
Last edited by rmonic79 on Thu Nov 23, 2023 1:59 am, edited 1 time in total.
User avatar
tokumaru
Posts: 12416
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Metatiles and Dynamic Nametable Management

Post by tokumaru »

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:

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
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.
User avatar
rmonic79
Posts: 26
Joined: Sat Aug 26, 2023 2:55 am

Re: Metatiles and Dynamic Nametable Management

Post by rmonic79 »

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 :D

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


User avatar
rmonic79
Posts: 26
Joined: Sat Aug 26, 2023 2:55 am

Re: Metatiles and Dynamic Nametable Management

Post by rmonic79 »

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?
User avatar
segaloco
Posts: 106
Joined: Fri Aug 25, 2023 11:56 am
Contact:

Re: Metatiles and Dynamic Nametable Management

Post by segaloco »

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.
User avatar
rmonic79
Posts: 26
Joined: Sat Aug 26, 2023 2:55 am

Re: Metatiles and Dynamic Nametable Management

Post by rmonic79 »

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.
I'm trying and trying but i can't make it work, any help would be appreciated.Thnaks.
User avatar
rmonic79
Posts: 26
Joined: Sat Aug 26, 2023 2:55 am

Re: Metatiles and Dynamic Nametable Management

Post by rmonic79 »

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.

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

Fiskbit
Posts: 803
Joined: Sat Nov 18, 2017 9:15 pm

Re: Metatiles and Dynamic Nametable Management

Post by Fiskbit »

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.
User avatar
rmonic79
Posts: 26
Joined: Sat Aug 26, 2023 2:55 am

Re: Metatiles and Dynamic Nametable Management

Post by rmonic79 »

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 :mrgreen:
User avatar
rmonic79
Posts: 26
Joined: Sat Aug 26, 2023 2:55 am

Re: Metatiles and Dynamic Nametable Management

Post by rmonic79 »

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 :D
User avatar
rmonic79
Posts: 26
Joined: Sat Aug 26, 2023 2:55 am

Re: Metatiles and Dynamic Nametable Management

Post by rmonic79 »

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

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    
And here the camera and setup before writing to vram with rodata at the end

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

And this is the result:
https://www.youtube.com/watch?v=swq010zJXHI
User avatar
donato-zits-
Posts: 46
Joined: Fri Jun 03, 2022 11:14 am
Contact:

Re: Metatiles and Dynamic Nametable Management

Post by donato-zits- »

rmonic79 wrote: Tue Sep 19, 2023 12:25 am

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


I think that I will try to implement that for test solid destructible blocks in a single-screen...could work?
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
User avatar
rmonic79
Posts: 26
Joined: Sat Aug 26, 2023 2:55 am

Re: Metatiles and Dynamic Nametable Management

Post by rmonic79 »

donato-zits- wrote: Thu Oct 26, 2023 8:31 am
rmonic79 wrote: Tue Sep 19, 2023 12:25 am

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


I think that I will try to implement that for test solid destructible blocks in a single-screen...could work?
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
If you need only s single screen without scroll i would suggest this thread viewtopic.php?t=24797
User avatar
rmonic79
Posts: 26
Joined: Sat Aug 26, 2023 2:55 am

Re: Metatiles and Dynamic Nametable Management

Post by rmonic79 »

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
User avatar
donato-zits-
Posts: 46
Joined: Fri Jun 03, 2022 11:14 am
Contact:

Re: Metatiles and Dynamic Nametable Management

Post by donato-zits- »

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
Post Reply