320. 查找精灵零命中的位置

检测精灵 0 与背景不透明像素首次发生重叠的位置。

320 / 356 · tests/chapter_11_sprite_zero_hit/test_320_find_sprite_zero_hit_position.py

需要创建的文件

emulator/rendering/sprite_zero_hit.py

为什么需要这一步

步骤 319 确立了精灵 0 命中在何时被清除。在设置 PPUSTATUS 第 6 位之前,我们需要一个纯函数辅助工具来回答这个问题:

Does a non-transparent sprite 0 pixel overlap a non-transparent background pixel?
If so, where is the first overlap?

返回一个位置而不仅仅是 True/False,可以为下一步的时序处理提供足够的信息,用来判断 PPU 应当何时设置精灵 0 命中。

定义

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 coordinate

建议的实现示例

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 None

重要的坐标简化处理

真实的 NES OAM 存储的是精灵顶部 Y 坐标减一后的值,因此精灵的第一行通常出现在 OAM Y + 1 处。现有的教程精灵渲染器目前是直接使用 OAM Y 的。这个辅助函数刻意与现有渲染器保持一致,以便可见的精灵像素与重叠坐标保持一致。之后会有一个专门的精度改进步骤,将两者一并更新。

重要规则

  • 精灵颜色索引 0 永远不会构成命中
  • 背景掩码为 False 永远不会构成命中
  • 精灵优先级第 5 位不会阻止精灵 0 命中的检测
  • 裁剪必须在对背景掩码进行索引之前完成
  • 这个辅助函数不得修改 PPUSTATUS

本步骤范围之外

  • 设置或清除 PPUSTATUS
  • 按扫描线/周期安排命中时机
  • 精确的 OAM Y + 1 行为
  • x=255 的硬件异常
  • PPUMASK 渲染启用规则
  • 8x16 精灵
  • 《超级马里奥兄弟》验证

运行本课

uv run pytest tests/chapter_11_sprite_zero_hit/test_320_find_sprite_zero_hit_position.py -v