CC65 开发FC/NES 游戏(四)方向键控制一个简单的精灵
Contents
起因
系列第四篇:上一篇把按键编码摸清了,这一篇让它"动起来"——用方向键控制一个精灵(先用星号占位)在屏幕上移动,顺便第一次接触 PPU(FC 的图像处理器)的寄存器。以后再把星号换成像游戏人物的图片,加上背景图。
需求
- 上下左右操作一个星号精灵移动
- 实时显示坐标,不出屏幕边界
技术实现原理
FC 图形系统:背景 + 64 个硬件精灵
PPU 分两层画图:背景层(nametable 里摆的图案)和精灵层。精灵有 64 个,每个在 OAM(Object Attribute Memory)里占 4 字节,顺序是 y 坐标、tile 编号、属性、x 坐标。
代码里的 putSP 宏就是直接写 OAM:先向 0x2003 写 OAM 地址(n*4 即第 n 个精灵的起始字节),再往 0x2004 连续写 y、t、a、x 四个字节。改了 OAM 里的 x/y,PPU 下一帧就把精灵画到新位置——“星号会动而背景不动"的原理就在这:坐标在 OAM,背景在 nametable,两回事。
镜像(mirroring):为什么看到的是"田字格"左上
NES 的显存只放得下两块物理 nametable,逻辑上却要排成四个(田字格),多出来的靠"镜像"复用:水平镜像=左右共享、垂直镜像=上下共享。镜像方式本该写在卡带 NES 头信息里;因为缺少nes头信息的配置,cc65会有默认生成nes的配置——默认情况下使用水平镜像,田字格的左上和左下是镜像,我们看到的就是田字格的左上角显示部分。

精灵为什么不出现在卷轴查看器里
调试器的卷轴查看器画的是 nametable(背景);精灵是 PPU 扫描每一行时实时叠上去的,不存放在 nametable 里。另外精灵不属于卷轴里固定显示的内容,右边的卷轴查看器不会看到精灵(也就是那个星号)。
PPU 控制寄存器:0x2000 与 0x2001
- 0x2000(PPUCTRL)写 0x80 = 二进制 10000000:bit7 置 1,VBlank 中断使能。CC65 的 waitvsync() 就靠它提供每帧 1/60 秒的节拍;
- 0x2001(PPUMASK)写 0x1E = 二进制 00011110:bit1~bit4 依次打开"背景左侧 8 像素显示、背景显示、精灵左侧 8 像素显示、精灵显示”——不开这些位,屏幕是黑的。
和(三)的关键差异:按住就连续动
(三)打印按键值用"值变了才处理"防闪烁;移动精灵要相反——按住就每帧走一格,所以每次循环都做位与检测,再 waitvsync() 等一次垂直消隐,一秒 60 帧就是一秒走 60 格。边沿触发还是电平触发,按业务选。
|
|
代码说明
- 0x10 / 0x20 / 0x40 / 0x80 的编码看上一篇里面有介绍,对应 上下左右 按键;
(p1 & 0x10) && 0x10后半个&& 0x10其实是多余的:位与结果非零即为真,写if (p1 & 0x10)完全等价(保留原写法当对比没问题);- 2000 和 2001 地址的作用见上面"技术实现原理":0x80 是二进制的 10000000,VBlank 使能;0x1E 是二进制的 00011110,打开背景和精灵显示;
- y 是 u8(0
255)、屏幕高 240,所以对 239 做了封顶/回绕;x 方向 0254 暂时没处理 255(u8 溢出会回绕到 0)。
编译

使用cc65的命令生成 nes文件的命令
|
|
源文件是 简单精灵.c
-t nes 就是告诉cc65 生成文件对应的平台是nes
踩坑记录与注意事项
- 屏幕全黑:先查 0x2001 的 0x1E 没写(显示没开),再查 0x2000 的 0x80——waitvsync 靠 VBlank 中断,没开启程序会卡死在等待里;
- 渲染期间写 0x2004 可能花屏:PPU 正在画时改 OAM 有竞争,正规做法是等 VBlank(waitvsync 之后)再写,或用 OAMDMA(0x4014)整块搬运;本 demo 数据量小直接写没出问题;
- 精灵实际显示位置比 OAM 的 y 值大 1 行:NES 的历史怪癖,做精确对位时记得补偿;
- 一条扫描线最多 8 个精灵:超出会被 PPU 丢弃造成闪烁,以后精灵多了(人物+子弹)要错开 y;
- conio 混用:gotoxy/cprintf 走背景层文本,改动镜像配置后定位可能乱。
Author 软件开发大郭
LastMod 2022-01-13