在6502汇编中,并没有专门为"游戏存档"设计的独有指令。实现存档功能,本质上是将内存中的特定数据(如角色属性、道具、关卡进度)写入 到某种非易失性存储介质(如电池备份的SRAM、卡带上的EEPROM),或从其中读取出来。
下面,我将穷举并分类在编写游戏存档系统时最核心的6502指令和编程模式,涵盖了从数据定位到校验的完整流程。
1. 核心数据读写指令
这是与存档硬件"对话"的基础,通过读写特定的内存地址(即硬件寄存器或SRAM窗口)来完成。
| 指令/操作 | 作用 | 典型应用场景 | 寻址模式/技巧 |
|---|---|---|---|
LDA |
将某个内存地址的值加载到累加器A中。 | 从内存中的角色结构体读取HP、经验值等数据,准备写入存档。 | 常配合零页寻址快速访问变量。 |
STA |
将累加器A的值存储到某个内存地址。 | 将数据写入存档区域(如SRAM的起始地址 $6000)。 |
绝对寻址或索引寻址。 |
LDX / LDY |
将值加载到X或Y索引寄存器。 | 用作循环计数器,或作为索引访问数组形式的存档数据。 | 立即数或内存值。 |
STX / STY |
将X或Y寄存器的值存储到内存。 | 在存档中保存索引寄存器的值(如当前选中的道具栏位)。 | 绝对寻址。 |
TAX / TAY / TXA / TYA |
在累加器、X寄存器、Y寄存器之间传输数据。 | 将累加器中的数据转移到X,以便用X作为索引进行存储。 | 寄存器间传输。 |
2. 循环与数据块传输指令
存档通常是一块连续的数据(如一个结构体或数组),需要用循环来批量处理。
| 指令/操作 | 作用 | 典型应用场景 | 寻址模式/技巧 |
|---|---|---|---|
DEX / DEY |
X或Y寄存器减1。 | 作为循环计数器,从数据块末尾向前遍历,或从0递增到块大小。 | 常用于 BNE 前。 |
BNE |
如果不相等(Z标志=0)则跳转。 | 循环体的核心,判断计数器是否减到0,未到则继续循环传输下一个字节。 | 结合 DEX 使用。 |
INX / INY |
X或Y寄存器加1。 | 在循环中递增索引,移动到下一个源地址或目标地址。 | 常用于从0开始正向遍历。 |
CPX / CPY / CMP |
比较寄存器与内存或立即数。 | 判断是否已处理完指定数量的字节,常用于循环结束条件。 | 立即数比较。 |
LDA (ptr), Y |
间接Y变址寻址 。从零页指针ptr指向的基地址加上Y偏移处加载数据。 |
核心技巧:用零页指针指向内存中的数据结构,用Y作为索引遍历存档数据。 | 间接Y变址。 |
STA (ptr), Y |
间接Y变址寻址。将数据存储到零页指针指向的基地址加上Y偏移处。 | 核心技巧:用零页指针指向SRAM中的存档槽位,用Y作为索引依次写入数据。 | 间接Y变址。 |
💡 核心技巧:使用间接寻址遍历存档数据块
以下代码演示了如何使用间接Y变址,将内存中的玩家数据保存到SRAM中。
assembly
; 假设:
; SAVE_PTR = $F0, $F1 指向SRAM中的存档起始地址 (如 $6000)
; DATA_PTR = $F2, $F3 指向内存中的玩家数据结构起始地址
; SAVE_SIZE = 64 存档数据大小(字节数)
SaveGame:
LDA #<SRAM_START ; 设置SAVE_PTR指向SRAM存档区
STA SAVE_PTR
LDA #>SRAM_START
STA SAVE_PTR+1
LDA #<PlayerData ; 设置DATA_PTR指向内存中的玩家数据
STA DATA_PTR
LDA #>PlayerData
STA DATA_PTR+1
LDY #$00 ; 初始化索引Y为0
LDX #SAVE_SIZE ; 设置循环计数器X为存档大小
SaveLoop:
LDA (DATA_PTR), Y ; 从内存中加载一个字节(源)
STA (SAVE_PTR), Y ; 将其存储到SRAM中(目标)
INY ; 索引加1
DEX ; 计数器减1
BNE SaveLoop ; 如果没传完64字节,继续循环
RTS
3. 地址计算与校验和指令
为了在存档时定位到正确的槽位,或在读取时验证存档是否损坏,需要一些算术和位操作指令。
| 指令/操作 | 作用 | 典型应用场景 | 寻址模式/技巧 |
|---|---|---|---|
CLC / ADC |
清除进位标志 / 带进位加法。 | 计算第N个存档槽的起始地址:base_addr + (slot_index * slot_size)。由于没有乘法指令,需用循环加法实现。 |
多字节加法。 |
SEC / SBC |
设置进位标志 / 带进位减法。 | 在验证存档时,将计算出的校验和与存储的校验和相减,判断是否为0。 | 多字节减法。 |
EOR (异或) |
按位异或。 | 生成校验和的核心指令。将存档的每个字节依次与累加器进行异或,最终得到单字节校验和。 | 累加器异或内存。 |
ADC |
带进位加法。 | 另一种生成校验和的方法:将每个字节累加,得到简单的和校验。 | 累加器加内存。 |
AND |
按位与。 | 掩码操作,例如只保留校验和的低4位。 | 立即数掩码。 |
💡 核心技巧:计算存档校验和
使用异或指令生成一个简单的校验和,用于检测数据损坏。
assembly
; 假设 SAVE_PTR 指向SRAM中的存档数据,Y已清零,SAVE_SIZE在X中
CalculateChecksum:
LDY #$00
LDA #$00 ; 初始化累加器为0
ChecksumLoop:
EOR (SAVE_PTR), Y ; 将当前字节与累加器异或
INY
DEX
BNE ChecksumLoop
STA ChecksumByte ; 保存计算出的校验和
RTS
4. 条件判断与分支指令
在存档前检查存档区是否可用,或读取后验证存档是否有效。
| 指令/操作 | 作用 | 典型应用场景 |
|---|---|---|
CMP |
比较累加器与操作数。 | 比较读取的"魔术字"(Magic Number)或校验和是否正确。 |
BEQ / BNE |
相等/不相等时跳转。 | 如果校验和匹配(BEQ),则加载存档;否则(BNE)显示"存档损坏"并重新开始游戏。 |
BIT |
位测试。 | 测试SRAM硬件的一个状态寄存器,判断其是否就绪或是否处于写保护状态。 |
BCS / BCC |
进位标志为1/0时跳转。 | 在16位地址计算或校验和比较后,检查进位。 |
5. 存档操作的标准流程框架
将上述模块组合,形成一个典型的游戏存档与读档流程:
assembly
; 存档主流程
DoSave:
JSR SelectSaveSlot ; 玩家选择存档槽位 (1/2/3)
JSR CalculateSaveAddress ; 根据槽位计算SRAM中的起始地址 -> SAVE_PTR
JSR CheckSRAMReady ; 检查存档硬件是否就绪(如电池备份正常)
BNE SaveFailed ; 如果未就绪,跳转到错误处理
JSR CopyDataToSRAM ; 将内存数据块复制到SAVE_PTR指向的SRAM
JSR CalculateChecksum ; 对刚写入的数据计算校验和
STA ChecksumAddr ; 将校验和存储在存档数据的末尾
; 可选:回读验证
JSR VerifyChecksum
BNE SaveCorrupted
RTS ; 存档成功
; 读档主流程
DoLoad:
JSR SelectSaveSlot
JSR CalculateSaveAddress
JSR CalculateChecksum ; 对SRAM中的存档数据计算校验和
STA TempChecksum
LDA ChecksumAddr ; 读取之前存储的校验和
CMP TempChecksum
BNE LoadFailed ; 如果不匹配,说明存档损坏
JSR CopySRAMToData ; 将SRAM数据复制回内存数据结构
RTS ; 读档成功
总结:6502存档编程核心指令表
| 功能类别 | 常用指令 |
|---|---|
| 数据搬运 | LDA, STA, LDX, STX, LDY, STY, TAX, TAY, TXA, TYA |
| 循环控制 | DEX, DEY, INX, INY, BNE, BEQ, CPX, CPY |
| 指针与间接寻址 | LDA (ptr), Y, STA (ptr), Y |
| 算术与校验 | CLC, ADC, SEC, SBC, EOR, AND |
| 条件判断 | CMP, BIT, BCS, BCC, BEQ, BNE |
| 子程序调用 | JSR, RTS |
以上指令组合起来,足以实现从简单密码存档(如FC上的《塞尔达传说》)到复杂的多槽位电池备份存档系统。
如果你有特定的硬件平台(如Mapper芯片类型)或存档介质(如SRAM、EEPROM、磁带)需要处理,可以进一步说明,我可以提供更具体的硬件交互代码示例。