Updating background metatiles during game

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems.

Moderator: Moderators

Post Reply
rludlamjr
Posts: 3
Joined: Tue Mar 02, 2021 8:03 am

Updating background metatiles during game

Post by rludlamjr » Tue Mar 02, 2021 8:17 am

Hi all, I'm new to nes development and trying to learn assembly. I have (with the help of this forum) succeeded so far in getting metatiles to load and have a decent character controller going, with collisions, etc. However, I'm stumped so far on the math trying to update background metatiles when collided (for picking up coins, etc.) I don't have any issues if I use pixel coordinates and update a single tile, but I can't seem to figure out how to translate the metatile reference to the tiles for an update.

My metatile structure is 16x16, using an example from this forum to write it out to the PPU:

Code: Select all

setup_background:
  LDA $2002             ; read PPU status to reset the high/low latch
  LDA #$20
  STA $2006             ; write the high byte of $2000 address
  LDA #$00
  STA $2006             ; write the low byte of $2000 address
  LDX #$00              ; start out at 0
  LDY #$00
  STX bgcounter
  STX tilecounter

@LoadBackgroundLoop:
  LDX tilecounter
  LDA backgroundMeta, x     ; load data from address (background + the value in x)
  TAY
  INX
  STX tilecounter
  
  LDX bgcounter
  LDA bgTopLeft,y
  STA buffer,x

  LDA bgTopRight,y
  STA buffer+1,x
  
  LDA bgBottomLeft,y
  STA buffer+32,x
  LDA bgBottomRight,y
  STA buffer+33,x

  INX
  INX
  STX bgcounter
  CPX #$20
  BNE @LoadBackgroundLoop
  ;BEQ WriteBGPPU

  LDY #$00

@WriteBGPPU:
  LDA buffer, y
  STA $2007
  INY
  CPY #$40
  BNE @WriteBGPPU

  LDY #$00
  STY bgcounter
  LDX tilecounter
  CPX #$F0
  BNE @LoadBackgroundLoop 
This works well. During the game, I am able to get which block is collided with. In the following code, collisionX and collisionY are my character's hotspots. collisionTile the metatile reference (same rom data as loaded in the loop above). This works well and using that reference I can retrieve whether the block is solid, etc., from the tileCollisionData.

Code: Select all

checkCollision:

	LDA collisionY ;collision hit spot y coordinate
	and #$f0
	STA collisionBlockY ;block y pos
	STA temp
	LDA collisionX ;collision hit spot x coordinate
	lsr a
	lsr a
	lsr a
	lsr a
	ora temp
	STA collisionBlockX ; collision block num	
	tax
	lda backgroundMeta, x ; look up metatile reference on bg map

	STA collisionTile
	TAX
	LDA tileCollisionData, x ; check if this metatile is solid
	STA collisionResult

	@done:
rts

What I am trying to do now is update a specific metatile when the player collides with it. However, I can't seem to get the math right to convert the tile references in my checkCollision / collisionResult to the addresses needed for $2006 to update the nametable. Any help would be greatly appreciated! I am still a beginner with 6502 math, so I'm probably just missing something obvious. Thanks!

User avatar
Dwedit
Posts: 4427
Joined: Fri Nov 19, 2004 7:35 pm
Contact:

Re: Updating background metatiles during game

Post by Dwedit » Tue Mar 02, 2021 10:33 am

I'll be calling the game metatiles (16x16) "Squares" just to simplify the names.

Let's suppose you have a big map. It could be as big as 4096x4096 squares large.

Variables you may have:

Camera Coordinates in pixels (16 bits)
CameraX (unit: pixels)
CameraY

Tile Coordinates of upper-left corner in VRAM
TileX (unit: tiles, 8px) Range is either 0-31 (single screen or horizontal mirroring), or 0-63 (vertical mirroring or four screen)
TileY (unit: tiles, 8px) Range is either 0-29 (single screen or vertical mirroring), or 0-29/32-61 (horizontal mirroring or four screen)

You can get TileX just by dividing CameraX by 8. But TileY needs to be tracked separately, because of wrapping after 30 tiles.

Size of scrolling region that will contain valid tiles at any given point
I suggest a 272x240 scrolling region (17x15 squares) if you are using vertical mirroring, but you might be using a 256x240 region instead (16x15 squares)

An in-RAM copy of the attribute tables, so you don't need to read it back out of VRAM.
This consumes 64 bytes of RAM per nametable, and is much simpler than trying to read bytes out of video memory.

So the problem is, given an Absolute Map Square Coordinate (Target), and Camera Coordinates, update the video memory for a new square.

--------------------------

First, you need Map Square coordinates of Upper-Left corner.
Just divide camera X/Y by 16.

MapUpperLeftX = CameraX / 16 (units: map squares, 16px)
MapUpperLeftY = CameraY / 16

Then you can get a relative position from the Target coordinate

RelativeX = TargetX - MapUpperLeftX (units: map squares, 16px)
RelativeY = TargetY - MapUpperLeftY

Check if your RelativeX, RelativeY is in bounds (for example, the 17x15 square region, but you might be using something else), sometimes a game can try to update a tile after it has scrolled out of bounds, and you get a graphical glitch

--------------------------

Address in video RAM:

VramAddress = 0x2000
AttributeTableAddress = 0x23C0
AttributeIndexRam = 0 (range: 0-63 per nametable)

TargetTileX = RelativeX * 2 + TileX (unit: tiles 8px)
Mask it to 0-63 for vertical mirroring or 4 screen, 0-31 for single screen or horizontal mirroring
If TargetTileX >= 32, subtract 32
- If it exceeded 32, For Vertical Mirroring or 4 screen: add 0x400 to VramAddress and AttributeTableAddress, and add 0x40 to AttributeIndexInRam
VramAddress += TargetTileX
AttributeTableAddress += TargetTileX / 4
AttributeIndexInRam += TargetTileX / 4

TargetTileY = RelativeY * 2 + TileY (unit: tiles 8px)
(horizontal mirroring or 4 screen only): If TargetTileY >= 62, subtract 62
If TargetTileY >= 30, subtract 30
- If it exceeded 30, For Horizontal Mirroring or 4 screen: add 0x800 to VramAddress and AttributeTableAddress, and add 0x40 to AttributeIndexInRam (add 0x80 for 4 screen)
VramAddress += TargetTileY * 32
AttributeTableAddress += (TargetTileY / 4) * 8
AttributeIndexInRam += (TargetTileY / 4) * 8

Now we have an address in VRAM for the target tile, and target attribute byte address.

------------------------

Need to set a new value for the attribute byte.

Easiest way to do this is to replicate the new color value four times within a byte, then apply a mask to the old data, and new data.
NewValue = color + color * 4 + color * 16 + color + 64 (Use a tiny lookup table for this, 4 entries)

Determine a mask for which bits we want from the old data, and which bits we want from the new data.
Mask: 1 = bit comes from old data, 0 = bit comes from new data
if TargetTileX & 2, Mask |= 00110011, otherwise Mask |= 11001100
if TargetTileY & 2, Mask |= 00001111, otherwise Mask |= 11110000

NewByte = (OldByte & Mask) | (NewValue & (Mask xor FF))

-------------------------

I hope I did this correctly, and I hope it helps with the math side of things.
Here come the fortune cookies! Here come the fortune cookies! They're wearing paper hats!

rludlamjr
Posts: 3
Joined: Tue Mar 02, 2021 8:03 am

Re: Updating background metatiles during game

Post by rludlamjr » Tue Mar 02, 2021 12:07 pm

Hey @dwedit thank you for the quick reply! This is super helpful, but I'm still not 100% following. I tried to implement but I must have messed something up as it's still not giving me the correct results. The code following appears to get the right X values, but the Y values are not right (some are too low, some are too high). Would appreciate your help in debugging my (very shaky) assembly.

Also, should say for simplification to start I am not scrolling, so my map is just a fixed 16x15 metatiles with vertical mirroring. For starters I am just trying to update the tiles not the attributes, so this code reflects that. This is a subroutine that is populating a buffer that I then transfer to the PPU during NMI. I am previously setting collisionBlockX and collisionBlockY to their X/Y tile number (as you have in your post tileX and tileY). As I'm not scrolling, I don't have a relativeX and relativeY. Can you tell me where this code is mixed up? I am using tile_debugLO and tile_debugHI to store the low and high bits to write to $2006 during the NMI update. That just reads the values in the buffer sequentially and writes them out to $2006 and then puts the tile into $2007.

Code: Select all

        LDA #$00
	STA tile_debugLO ; initialize lo and hi bytes
	STA tile_debugHI

	ldx nmt_update_len	; buffer

	lda collisionBlockX ; collisionBlockX = tileX
	ASL ;multiply by 2
	BCC @addTileX
	INC tile_debugHI

	@addTileX: ;commented out as this doesn't seem to work, x-value is right if this is commented
	;CLC
	;ADC collisionBlockX ;add tileX

	CMP #$40 ; check if greater than 32, and subtract 32 if so
	BCS @subtract32
	JMP @storeX

	@subtract32:
	SEC
	SBC #$40

	@storeX:
	CLC
	ADC tile_debugLO
	STA tile_debugLO
	LDA tile_debugHI
	ADC #$00
	STA tile_debugHI



	LDA collisionBlockY
	ASL ; multiply by 2
	BCC @addTileY
	INC tile_debugHI


	@addTileY:
	CLC
	ADC collisionBlockY ;add tileY

	CMP #$1E ; check if greater than 30, if so, subtract 30
	BCS @subtract30
	JMP @storeY

	@subtract30:
	SEC
	SBC #$F0


	@storeY:
	ASL ;multiply by 32
	BCC :+
		INC tile_debugHI
	:
	ASL
	BCC :+
		INC tile_debugHI
	:	
	ASL
	BCC :+
		INC tile_debugHI
	:	
	ASL
	BCC :+
		INC tile_debugHI
	:	
	ASL
	BCC :+
		INC tile_debugHI
	:	

	CLC
	ADC tile_debugLO ;add to vram address
	STA tile_debugLO

	LDA tile_debugHI
	ADC #$00
	STA tile_debugHI


	;save to buffer for later write
	ORA #$20 ;set to start at 0x200
	sta nmt_update, X ;write hi bit
	INX
	

	LDA tile_debugLO ; write lo bit
	sta nmt_update, X
	INX


	LDA #$C2 ; write tile data
	STA nmt_update, X

	STX nmt_update_len

rludlamjr
Posts: 3
Joined: Tue Mar 02, 2021 8:03 am

Re: Updating background metatiles during game

Post by rludlamjr » Tue Mar 02, 2021 7:32 pm

Ah, good news! I got it working using your instructions as a base. Here's the updated code in case anyone is following along. One more question - for my collision data I'm using a ROM table... but if I modify the background, that won't be relevant anymore. Any suggestions on how to keep track of what has changed? Should I just reserve some space in memory for that and index it somehow so I can check it as part of my collision routine?

Thanks again for your help!!!

Code: Select all

	LDA #$00
	STA tile_debugLO
	STA tile_debugHI
	STA temp

	ldx nmt_update_len	

	lda collisionBlockX
	ASL ;multiply by 2
	BCC @addTileX
	INC tile_debugHI

	@addTileX: 

	CMP #$40 ; check if greater than 32, and subtract 32 if so
	BCS @subtract32
	JMP @storeX

	@subtract32:
	SEC
	SBC #$20

	@storeX:
	CLC
	ADC tile_debugLO
	STA tile_debugLO
	LDA tile_debugHI
	ADC #$00
	STA tile_debugHI



	LDA collisionBlockY


	@addTileY:

	CMP #$1E ; check if greater than 30, if so, subtract 30
	BCS @subtract30
	JMP @storeY

	@subtract30:
	SEC
	SBC #$F0
	BCC :+

	:


	@storeY:
	ASL ;multiply by 32
	BCC :+
		INC tile_debugHI
	:	

	CLC
	ADC tile_debugLO ;add to vram address
	STA tile_debugLO

	LDA tile_debugHI
	ADC #$00
	STA tile_debugHI


	;save to buffer for later write
	ORA #$20 ;set to start at 0x200
	STA tile_debugHI
	sta nmt_update, X ;write hi bit
	INX
	

	LDA tile_debugLO ; write lo bit
	sta nmt_update, X
	INX


	LDA #$C2 ; write tile data
	STA nmt_update, X

	STX nmt_update_len

Post Reply