353. Sprite zero hit 使用视口掩码

在 sprite-zero-hit 重叠检测中使用支持视口的背景不透明度。

353 / 356 · tests/chapter_13_scrolling/test_353_sprite_zero_hit_uses_viewport_mask.py

待更新文件

emulator/rendering/sprite_zero_hit.py

参考资料

https://www.nesdev.org/wiki/PPU_rendering
https://www.nesdev.org/wiki/PPU_scrolling

为什么需要这一步

Sprite zero hit 是一种 PPU 状态信号,当 OAM 精灵条目 0 的不透明像素与不透明背景像素重叠时产生。游戏可以轮询这个信号,以便在帧中途某个时刻改变寄存器,例如将固定的状态区域与可滚动的游戏区域分隔开。

重叠辅助函数使用屏幕坐标对背景不透明度进行索引

mask_index = screen_y * 256 + screen_x

因此该掩码必须描述在这些相同坐标处实际可见的背景。经过定时滚动后,屏幕 X 在每一行上都可能映射到不同的粗 X 列或逻辑名称表。旧的固定 $2000 掩码可能检查的是不同的背景像素,即便精灵 0 仍处于同一屏幕位置。

示例

sprite 0 screen X:       100
scanline viewport X:      40
visible background X:    140

fixed mask inspects X:   100
viewport mask inspects X: 140

如果源 X 140 是不透明的而源 X 100 是透明的,固定掩码就会漏掉这次重叠。支持视口的适配器在完整帧时选择定时合成,并在定时数据不可用时保留此前已测试过的快照回退方案。

为什么要使用导入别名?历史上的 sprite-zero-hit 测试对模块内的局部名称 ppu_background_to_opaque_mask 进行了猴子补丁。将每一处调用都改为新的局部名称会破坏这个稳定的测试接口。以历史名称导入支持视口的适配器,可以在保持旧测试不变的同时改变生产行为:

new viewport-aware function as old module-local name

重要不变量

  • sprite-zero-hit 使用支持视口的不透明度适配器
  • 历史局部依赖名称仍然可以被猴子补丁
  • 所选掩码原样传递给纯重叠辅助函数
  • 精灵 CHR 图案表的选择与背景掩码保持独立
  • 渲染代码不直接设置 PPUSTATUS
  • Console 和 PPU 时序继续负责调度和设置该命中

常见误解

Sprite zero hit 不是游戏逻辑中的碰撞检测,并不意味着马里奥撞到了敌人或方块。它只是一种渲染层面的重叠,仅涉及精灵条目 0 和一个不透明的背景图案像素,通常被软件用作时序信号。

测试策略

一个测试检查生产环境中的依赖身份,以确保旧的固定生产者不会悄悄地重新出现。另一个测试让一个完整的定时帧经过真实的视口适配器,仅替换其开销较大的行合成器,然后验证得到的掩码原样到达既有的纯重叠边界。

不在本步骤范围内

  • 改变纯重叠搜索
  • 改变精灵图案表的选择
  • 改变 Console 的帧调度
  • 精确的左边缘、X=255 以及 OAM Y+1 硬件行为
  • 完整的垂直源行滚动

完整示例实现

# emulator/rendering/sprite_zero_hit.py

# --- UPDATED BLOCK: VIEWPORT MASK WITH HISTORICAL LOCAL NAME ---
from emulator.rendering.ppu_background_renderer import (
    PATTERN_TABLE_0_ADDR,
    PATTERN_TABLE_1_ADDR,
    ppu_background_viewport_to_opaque_mask as ppu_background_to_opaque_mask,
)

...

运行本课

uv run pytest tests/chapter_13_scrolling/test_353_sprite_zero_hit_uses_viewport_mask.py -v