320. Encontrar la posición de sprite 0 hit
Detecta el primer solapamiento de píxel opaco entre sprite 0 y el fondo.
Lección 320 de 356 · tests/chapter_11_sprite_zero_hit/test_320_find_sprite_zero_hit_position.py
Archivo a crear
emulator/rendering/sprite_zero_hit.pyPor qué existe este paso
El paso 319 estableció cuándo se limpia sprite 0 hit. Antes de establecer el bit 6 de PPUSTATUS, necesitamos un ayudante puro que responda:
Does a non-transparent sprite 0 pixel overlap a non-transparent background pixel?
If so, where is the first overlap?Devolver una posición en lugar de solo True/False le da al siguiente paso de temporización suficiente información para decidir cuándo la PPU debería establecer sprite 0 hit.
Definiciones
Sprite pixel is opaque:
its decoded CHR color index is 1, 2, or 3
Background pixel is opaque:
background_opaque_mask[y * screen_width + x] is True
Sprite 0 overlap:
both conditions are true at the same visible screen coordinateEjemplo de implementación sugerida
from emulator.ppu.chr_decoder import decode_chr_tile
from emulator.rendering.framebuffer import NES_SCREEN_HEIGHT, NES_SCREEN_WIDTH
from emulator.rendering.nametable_renderer import BackgroundOpaqueMask
from emulator.rendering.sprite_renderer import (
SpriteEntry,
decode_sprite_attributes,
)
SpriteZeroHitPosition = tuple[int, int]
def find_sprite_zero_hit_position(
sprite_zero: SpriteEntry,
pattern_table: bytes,
background_opaque_mask: BackgroundOpaqueMask,
screen_width: int = NES_SCREEN_WIDTH,
screen_height: int = NES_SCREEN_HEIGHT,
) -> SpriteZeroHitPosition | None:
if len(background_opaque_mask) != screen_width * screen_height:
raise ValueError(
"Background opaque mask size must be equal to screen width * height"
)
tile_start = sprite_zero.tile_index * 16
tile_end = tile_start + 16
if tile_end > len(pattern_table):
raise ValueError("Pattern table does not contain sprite 0 tile bytes")
attributes = decode_sprite_attributes(sprite_zero.attributes)
color_indexes = decode_chr_tile(pattern_table[tile_start:tile_end])
for tile_y in range(8):
for tile_x in range(8):
source_x = 7 - tile_x if attributes.flip_horizontal else tile_x
source_y = 7 - tile_y if attributes.flip_vertical else tile_y
sprite_color_index = color_indexes[source_y][source_x]
if sprite_color_index == 0:
continue
screen_x = sprite_zero.x + tile_x
screen_y = sprite_zero.y + tile_y
if not (0 <= screen_x < screen_width):
continue
if not (0 <= screen_y < screen_height):
continue
mask_index = screen_y * screen_width + screen_x
if background_opaque_mask[mask_index]:
return screen_x, screen_y
return NoneSimplificación de coordenadas importante
La OAM real de NES almacena la coordenada Y superior del sprite menos uno, así que la primera fila del sprite normalmente aparece en OAM Y + 1. El renderizador de sprites del tutorial existente actualmente usa OAM Y directamente. Este ayudante coincide intencionadamente con ese renderizador existente para que los píxeles de sprite visibles y las coordenadas de solapamiento sigan siendo consistentes. Un paso de precisión centrado posterior debería actualizar ambos a la vez.
Reglas importantes
- el índice de color 0 del sprite nunca contribuye a un hit
- una máscara de fondo False nunca contribuye a un hit
- el bit 5 de prioridad de sprites no impide la detección de sprite 0 hit
- el recorte debe ocurrir antes de indexar la máscara de fondo
- este ayudante no debe mutar PPUSTATUS
Fuera de alcance
- establecer o limpiar PPUSTATUS
- programar el hit por scanline/ciclo
- el comportamiento exacto de OAM Y + 1
- la excepción de hardware en x=255
- las reglas de habilitación de renderizado de PPUMASK
- sprites de 8x16
- validación con Super Mario Bros.
Ejecutar esta lección
uv run pytest tests/chapter_11_sprite_zero_hit/test_320_find_sprite_zero_hit_position.py -v