在6502汇编中,游戏动画的实现并没有专门的"动画指令",而是通过组合定时器控制 、索引寻址 、数据读取 和OAM(对象属性内存)更新等指令来完成。下面,我将穷举并分类在编写游戏动画系统时最核心的6502指令和编程模式,涵盖从帧序列管理到精灵渲染的完整流程。
1. 动画数据结构与存储
动画通常需要存储帧序列的图块ID、属性、以及每帧的持续时间。
| 指令/操作 | 作用 | 典型应用场景 | 寻址模式/技巧 |
|---|---|---|---|
.byte / .db |
汇编器伪指令,定义字节数据。 | 存储动画帧表:.db FRAME1_TILE, FRAME2_TILE, ... |
数据定义 |
.word / .dw |
定义字数据(16位)。 | 存储指向不同动画序列的指针表。 | 数据定义 |
LDA |
加载累加器。 | 从动画数据表中读取当前帧的图块ID或属性。 | 绝对X变址、绝对Y变址、间接Y变址 |
STA |
存储累加器。 | 将图块ID写入OAM(精灵内存)的相应位置。 | 绝对X变址(如 $0200,X) |
2. 帧定时器控制
动画的帧速率需要通过定时器来控制,通常每个游戏循环(帧)递减计数器。
| 指令/操作 | 作用 | 典型应用场景 | 寻址模式/技巧 |
|---|---|---|---|
DEC |
内存值减1。 | 递减动画帧定时器。 | 零页寻址(快速访问) |
INC |
内存值加1。 | 递增帧索引。 | 零页寻址 |
BNE |
非零跳转。 | 如果定时器未归零,则保持当前帧。 | 相对寻址 |
BEQ |
零跳转。 | 如果定时器归零,则切换到下一帧。 | 相对寻址 |
LDA / STA |
加载/存储累加器。 | 重置定时器为初始值。 | 立即数寻址(如 LDA #$06) |
💡 核心技巧:帧定时器管理
assembly
; 假设 animTimer 和 animFrame 是两个零页变量
; ANIM_SPEED 是每帧持续的帧数(例如6个游戏帧)
UpdateAnimation:
DEC animTimer ; 定时器减1
BNE NoFrameChange ; 如果还没到0,不切换帧
; 重置定时器
LDA #ANIM_SPEED
STA animTimer
; 切换到下一帧
INC animFrame
LDA animFrame
CMP #MAX_FRAMES ; 是否超过最大帧数?
BNE NoWrap
LDA #0 ; 循环回第一帧
STA animFrame
NoWrap:
; 这里可以调用更新精灵数据的函数
JSR UpdateSpriteFromFrame
NoFrameChange:
RTS
3. 帧数据读取与精灵更新
根据当前的帧索引,从动画数据表中读取对应的图块ID和属性,并更新OAM中的精灵。
| 指令/操作 | 作用 | 典型应用场景 | 寻址模式/技巧 |
|---|---|---|---|
LDY |
加载Y寄存器。 | 将帧索引作为Y偏移量,读取帧数据。 | 立即数、内存 |
LDA AnimationData, Y |
绝对Y变址读取。 | 从一维帧数据表中读取当前帧的图块ID。 | 绝对Y变址 |
LDA (AnimPtr), Y |
间接Y变址读取。 | 当每个动画序列有独立的帧数据表时,AnimPtr指向该表,Y为帧索引。 | 间接Y变址 |
STA $0200, X |
绝对X变址存储。 | 将图块ID写入OAM的图块ID区域(例如每个精灵占4字节,X索引到正确位置)。 | 绝对X变址 |
INX / INY |
索引寄存器加1。 | 移动到下一个精灵数据槽。 | 隐含寻址 |
TXA / TYA |
索引寄存器传送到累加器。 | 用于保存/恢复索引值。 | 隐含寻址 |
💡 核心技巧:多精灵角色动画
一个角色可能由多个精灵拼成(如8x16或16x16)。每帧需要更新多个OAM条目。以下示例展示了如何从帧数据表中读取四个精灵的信息并写入OAM。
assembly
; 帧数据表结构:每个帧包含4个字节(4个精灵的图块ID)
; 例如:PlayerAnim:
; .db $01, $02, $11, $12 ; 帧0的4个图块ID
; .db $03, $04, $13, $14 ; 帧1的4个图块ID
; ...
UpdateSpriteFromFrame:
; 假设 animFrame 存储当前帧号(0,1,2...)
; 每个帧占用 4 字节
LDA animFrame
ASL A ; 乘以2
ASL A ; 再乘以2,相当于乘以4
TAY ; Y = 帧起始偏移
LDX #0 ; X 用于OAM索引(每个精灵4字节,从$0200开始)
LDX #0 ; 实际上我们想写4个精灵,所以需要循环4次
; 但为了简化,直接展开4次写入
; 精灵0
LDA PlayerAnim, Y ; 读取图块ID
STA $0201, X ; 写入OAM的图块ID位置(假设OAM格式:Y, Tile, Attr, X)
INY
INX ; 移动到下一个精灵(每个精灵4字节)
INX
INX
INX ; 实际上这样写效率低,更好的做法是用循环
; 更好的做法:循环4次
LDX #0 ; OAM索引
LDY animFrame
TYA
ASL A
ASL A
TAY ; Y = 帧起始偏移
LDX #0 ; 循环计数器,也作OAM索引
CopyLoop:
LDA PlayerAnim, Y ; 取图块ID
STA $0201, X ; 存到OAM的Tile位置(假设OAM基址$0200,每个精灵4字节,Tile是第二个字节)
INY
TXA
CLC
ADC #4 ; X += 4 移动到下一个精灵
TAX
CPX #16 ; 已经处理了4个精灵? (4*4=16)
BCC CopyLoop
RTS
更常见的是,每个帧的精灵数据可能还包含X/Y偏移、属性等,这时可以用结构体数组。
4. 使用指针表管理多个动画
角色可能有多个动画状态(如 idle, walk, attack),每个状态对应不同的帧数据表。
| 指令/操作 | 作用 | 典型应用场景 | 寻址模式/技巧 |
|---|---|---|---|
LDA AnimTableLo, X |
读取指针表低字节。 | X为动画类型索引,从指针表中取得该动画帧数据表的16位地址。 | 绝对X变址 |
LDA AnimTableHi, X |
读取指针表高字节。 | 同上。 | 绝对X变址 |
STA AnimPtr / STA AnimPtr+1 |
存储到零页指针。 | 将取出的地址存入零页指针,供间接寻址使用。 | 零页寻址 |
JSR |
跳转子程序。 | 调用通用的更新精灵函数,该函数使用AnimPtr和animFrame。 | 绝对寻址 |
💡 核心技巧:动画指针表
assembly
; 指针表(每个动画的帧数据表地址)
AnimTableLo:
.byte <IdleAnim, <WalkAnim, <AttackAnim
AnimTableHi:
.byte >IdleAnim, >WalkAnim, >AttackAnim
; 动画状态枚举
ANIM_IDLE = 0
ANIM_WALK = 1
ANIM_ATTACK = 2
; 切换动画
SetAnimation:
; 假设A中存有新的动画类型 (0,1,2)
ASL A ; 每个指针占2字节,所以乘以2
TAX
LDA AnimTableLo, X ; 取低字节
STA AnimPtr
LDA AnimTableHi, X ; 取高字节
STA AnimPtr+1
; 可选:重置帧索引和定时器
LDA #0
STA animFrame
LDA #INIT_TIMER
STA animTimer
RTS
; 更新当前动画的精灵(假设每个动画帧包含多个精灵数据,但这里简化)
UpdateCurrentAnim:
; 使用 AnimPtr 和 animFrame 读取帧数据
LDY animFrame
LDA (AnimPtr), Y ; 读取帧数据(可能是图块ID或更复杂的数据)
STA tempTile
; ... 更新OAM
RTS
5. 其他辅助指令
| 指令/操作 | 作用 | 典型应用场景 |
|---|---|---|
PHA / PLA |
累加器压栈/弹栈。 | 在更新动画时保护现场,避免破坏已有寄存器值。 |
PHP / PLP |
处理器状态压栈/弹栈。 | 保护中断标志等。 |
TXA / TYA / TAX / TAY |
寄存器间传输。 | 将帧索引从内存加载到Y,或将X中的OAM偏移保存到内存。 |
CMP / BCS / BCC |
比较与条件跳转。 | 检查帧索引是否到达最后一帧,决定是否循环。 |
BIT |
位测试。 | 可用于测试动画状态机的标志位。 |
6. 完整的动画更新框架
将以上模块组合,形成一个典型的动画系统流程(通常在垂直消隐期间执行):
assembly
GameLoop:
JSR ReadControllers
JSR UpdatePlayerLogic ; 可能改变动画状态
JSR UpdateAnimation ; 更新定时器和帧索引
JSR BuildSprites ; 根据当前动画帧和位置构建OAM
JSR WaitForVBlank
JSR DMAOAM ; 将OAM缓冲区数据复制到PPU的OAM(通常通过DMA)
JMP GameLoop
UpdateAnimation:
DEC animTimer
BNE NoFrameChange
LDA #ANIM_SPEED
STA animTimer
INC animFrame
LDA animFrame
CMP animFrameCount ; 当前动画的总帧数
BCC NoWrap
LDA #0 ; 循环
STA animFrame
; 可选:触发动画结束事件
NoWrap:
NoFrameChange:
RTS
BuildSprites:
; 假设 AnimPtr 指向当前动画的帧数据表(每个帧包含4个精灵的Tile和Attr等)
; animFrame 为当前帧索引
LDA animFrame
ASL A
ASL A ; 乘以4,因为每帧4个精灵
TAY ; Y = 帧内精灵数据起始偏移
LDX #0 ; OAM索引(每精灵4字节)
SpriteLoop:
LDA (AnimPtr), Y ; 读取 Tile ID
STA OAM_BUF+1, X ; 存入OAM缓冲区 Tile 位置
INY
LDA (AnimPtr), Y ; 读取 Attribute
STA OAM_BUF+2, X
INY
LDA (AnimPtr), Y ; 读取 X Offset(相对角色位置的偏移)
STA temp
; 这里需要加上角色的世界坐标,略
INY
LDA (AnimPtr), Y ; 读取 Y Offset
STA temp
INY
TXA
CLC
ADC #4
TAX
CPX #16 ; 是否处理完4个精灵?
BCC SpriteLoop
RTS
总结:6502动画编程核心指令表
| 功能类别 | 常用指令 |
|---|---|
| 数据定义 | .byte, .db, .word, .dw(汇编器伪指令) |
| 定时器控制 | DEC, INC, BNE, BEQ, LDA #imm, STA |
| 帧索引操作 | INC, CMP, BCC, BCS, ASL(乘以帧大小) |
| 数据读取 | LDA abs,Y, LDA (ptr),Y, LDA abs,X |
| OAM更新 | STA abs,X(如 STA $0200,X) |
| 指针管理 | LDA abs,X(读指针表),STA ptr |
| 现场保护 | PHA, PLA, TXA, TYA, TAX, TAY |
以上指令组合起来,足以实现从简单的单帧精灵动画到复杂的多状态、多帧角色动画系统。如果你有特定的动画需求(如帧压缩、插值、硬件精灵限制处理)需要代码示例,可以进一步说明。