在6502汇编中编写游戏动作(如角色移动、跳跃、攻击、物理模拟等),没有专用的"动作指令",而是通过组合算术运算指令 、位操作指令 、分支指令 以及数据访问指令来构建复杂的游戏逻辑。下面我将按照动作开发中常见的功能模块,穷举并分类最核心的6502指令和编程模式,涵盖从物理模拟到状态机管理的完整流程。
1. 运动与物理模拟
处理角色的位置、速度、加速度和重力,通常需要多字节算术和比较。
| 指令/操作 | 作用 | 典型应用场景 | 寻址模式/技巧 |
|---|---|---|---|
CLC / ADC |
清除进位标志 / 带进位加法 | 16位坐标更新:x_lo + vx_lo,x_hi + vx_hi + carry |
多字节加法时需先 CLC |
SEC / SBC |
设置进位标志 / 带进位减法 | 16位坐标更新(反向移动)、碰撞后位置修正 | 多字节减法时需先 SEC |
CMP / CPX / CPY |
比较累加器/X/Y与操作数 | 检测是否到达边界、比较Y坐标与地面高度 | 常与 BCS / BCC / BEQ / BNE 配合 |
LDA / STA |
加载/存储累加器 | 读取/保存速度、位置、加速度变量 | 结合零页寻址快速访问 |
INX / DEX |
X寄存器加1/减1 | 对象计数、循环遍历 | 用于对象池管理 |
ASL |
算术左移 | 将速度乘以2(实现加速)、坐标转换为图块索引 | 相当于乘以2 |
LSR |
逻辑右移 | 将速度除以2(实现摩擦或阻力)、提取子像素位 | 相当于除以2 |
💡 核心技巧:子像素精度
许多游戏使用子像素(如1/256像素)来实现平滑移动。速度和位置用16位表示,高字节为整数部分,低字节为小数部分。
assembly
; 更新玩家X坐标(16位): x += vx
UpdateX:
CLC
LDA x_lo
ADC vx_lo
STA x_lo
LDA x_hi
ADC vx_hi
STA x_hi
RTS
; 应用重力:vy += gravity (gravity 通常是小数,如 #$10 代表 16/256 像素/帧²)
ApplyGravity:
CLC
LDA vy_lo
ADC #$10 ; 假设重力为 16/256
STA vy_lo
BCC NoCarry
INC vy_hi
NoCarry:
RTS
2. 状态机与控制逻辑
角色在不同动作(站立、跑动、跳跃、攻击)间切换,需要标志位判断和跳转表。
| 指令/操作 | 作用 | 典型应用场景 | 寻址模式/技巧 |
|---|---|---|---|
BNE / BEQ |
零标志为0/1时跳转 | 根据输入按键决定是否切换状态 | 常用 |
BCS / BCC |
进位标志为1/0时跳转 | 碰撞检测后决定是否回退位置 | |
BMI / BPL |
符号标志为1/0时跳转 | 判断速度方向(正/负) | 速度最高位为1表示负方向 |
BIT |
位测试 | 测试控制器的按键位是否按下 | 可以直接用 BIT $4016 测试某一位 |
JMP |
无条件跳转 | 进入状态机的主循环 | |
JMP (indirect) |
间接跳转 | 实现状态跳转表(查表跳转) | 根据状态值跳转到对应处理函数 |
CMP / CMP #value |
比较累加器与立即数 | 检查当前状态值,决定分支 | |
TAX / TXA |
累加器与X寄存器互换 | 将状态值存入X以用于索引 |
💡 核心技巧:状态跳转表
使用间接跳转实现高效的多路分支。
assembly
; 状态枚举
STATE_IDLE = 0
STATE_WALK = 1
STATE_JUMP = 2
STATE_ATTACK = 3
; 状态处理函数地址表
StateJumpTable:
.word IdleHandler
.word WalkHandler
.word JumpHandler
.word AttackHandler
; 状态机更新
UpdatePlayer:
LDX playerState ; 加载当前状态
LDA StateJumpTable, X ; 不能直接间接跳转,需要将地址加载到临时指针
STA jumpPtr
LDA StateJumpTable+1, X
STA jumpPtr+1
JMP (jumpPtr) ; 跳转到对应处理函数
3. 碰撞检测
基于轴对齐包围盒(AABB)的碰撞检测,需要比较坐标和尺寸。
| 指令/操作 | 作用 | 典型应用场景 |
|---|---|---|
CMP |
比较 | 检查玩家右边界是否超过物体左边界、上边界是否低于物体下边界等 |
BCS / BCC |
基于进位的条件跳转 | 用于判断是否发生重叠 |
SEC / SBC |
带借位减法 | 计算两个物体之间的距离差 |
CLC / ADC |
带进位加法 | 计算边界坐标:x + width |
AND |
按位与 | 屏蔽坐标的低位以对齐到网格 |
LSR / ASL |
移位 | 将像素坐标转换为图块索引 |
💡 核心技巧:四方向碰撞检测
通常先尝试移动X,如果碰撞则回退;再尝试移动Y。
assembly
; 检查玩家与某个障碍物的X轴重叠
CheckCollisionX:
; 计算玩家右边界 = x + player_width
CLC
LDA player_x
ADC player_width
STA player_right
; 计算障碍物右边界 = obj_x + obj_width
CLC
LDA obj_x
ADC obj_width
STA obj_right
; 检查玩家右边界是否大于障碍物左边界 (player_right > obj_x) 且 玩家左边界 < 障碍物右边界
LDA player_right
CMP obj_x
BCC NoCollisionX ; 如果 player_right < obj_x,无碰撞
LDA player_x
CMP obj_right
BCS NoCollisionX ; 如果 player_x >= obj_right,无碰撞
; 有碰撞
...
NoCollisionX:
4. 动画控制
更新角色精灵的帧索引,管理动画定时器。
| 指令/操作 | 作用 | 典型应用场景 |
|---|---|---|
DEC |
内存值减1 | 递减动画帧计数器、定时器 |
INC |
内存值加1 | 递增帧索引 |
CMP / BEQ |
比较与相等跳转 | 检查定时器是否归零,决定是否切换帧 |
LDA (ptr), Y |
间接Y变址 | 从动画数据表中读取当前帧的精灵图块ID、属性等 |
STA $2007 |
存储到PPU数据端口 | 更新OAM(精灵内存)中的图块ID |
💡 核心技巧:动画定时器
使用一个递减计数器控制帧速率。
assembly
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:
NoFrameChange:
; 根据当前帧更新OAM
LDY animFrame
LDA AnimationData, Y ; 取帧对应的精灵图块ID
STA $0201 ; 假设存储到OAM的第一个精灵的图块ID位置
RTS
5. 输入处理与动作触发
结合之前读取的控制器状态,决定是否执行某个动作。
| 指令/操作 | 作用 | 典型应用场景 |
|---|---|---|
AND |
按位与 | 从控制器状态字节中提取特定按键位 |
BNE |
非零跳转 | 如果按键按下(对应位为1)则跳转 |
BIT |
位测试 | 可直接测试某一位并影响标志,无需修改累加器 |
LDA buttons |
加载按键状态 | 读取上一帧读取的按键状态 |
EOR |
异或 | 检测按键上升沿(刚按下) |
💡 核心技巧:检测按键按下瞬间
通过比较当前帧和上一帧的按键状态,找出刚刚被按下的键。
assembly
; 假设 curButtons 和 oldButtons 分别存储当前帧和上一帧的按键状态
DetectPress:
LDA curButtons
EOR #$FF ; 取反,得到未按下的位为1
AND oldButtons ; 现在oldButtons中与当前帧不同的位(即刚松开的键)为1?我们想要刚按下的,需要另一种方法。
; 更常见的方法:
LDA curButtons
AND #BUTTON_A_MASK
BEQ NoAPress ; 当前帧A键没按下
LDA oldButtons
AND #BUTTON_A_MASK
BNE NoAPress ; 上一帧A键也按下了(长按),不是刚按下
; 此时A键刚被按下
...
NoAPress:
6. 对象管理与循环
游戏通常有多个动态对象(敌人、子弹),需要循环更新每个对象。
| 指令/操作 | 作用 | 典型应用场景 |
|---|---|---|
LDX #N / DEX / BNE |
初始化计数器、递减、非零跳转 | 构建固定次数的对象更新循环 |
LDA ObjectData, X |
绝对X变址 | 访问对象数组的成员(每个对象占固定字节数) |
INX / INY |
寄存器加1 | 移动到下一个对象的数据槽 |
CPX #MAX_OBJECTS |
比较X与最大值 | 用于不定数量对象的循环结束判断 |
💡 核心技巧:对象数组遍历
每个对象占用N个字节,使用索引X遍历。
assembly
; 假设每个对象占用 8 字节:x_lo, x_hi, y_lo, y_hi, vx, vy, state, timer
OBJ_SIZE = 8
UpdateAllObjects:
LDX #0
@Loop:
; 处理对象 X
LDA obj_state, X
CMP #STATE_INACTIVE
BEQ @Next ; 跳过非活动对象
; 更新位置
CLC
LDA obj_x_lo, X
ADC obj_vx, X
STA obj_x_lo, X
LDA obj_x_hi, X
ADC #0
STA obj_x_hi, X
; ... 其他更新
@Next:
TXA
CLC
ADC #OBJ_SIZE ; 移动到下一个对象
TAX
CPX #MAX_OBJECTS*OBJ_SIZE
BCC @Loop
RTS
7. 常用的完整动作更新框架
将以上模块组合,形成一个典型的游戏动作更新循环:
assembly
GameLoop:
JSR ReadControllers ; 读取手柄
JSR HandleInput ; 根据输入切换状态/设置速度
JSR UpdatePhysics ; 应用重力、速度更新位置
JSR CheckCollisions ; 碰撞检测与响应
JSR UpdateAnimation ; 更新动画帧
JSR UpdateObjects ; 更新所有动态对象
JSR DrawSprites ; 将最终位置写入OAM
JSR WaitForVBlank ; 等待垂直消隐
JMP GameLoop
总结:6502动作编程核心指令表
| 功能类别 | 常用指令 |
|---|---|
| 算术运算 | ADC, SBC, CLC, SEC, INC, DEC |
| 比较与分支 | CMP, CPX, CPY, BEQ, BNE, BCS, BCC, BMI, BPL |
| 位操作 | AND, ORA, EOR, BIT, ASL, LSR, ROL, ROR |
| 数据传输 | LDA, STA, LDX, STX, LDY, STY, TAX, TAY, TXA, TYA |
| 流程控制 | JMP, JSR, RTS, BRK, RTI(中断返回) |
| 栈操作 | PHA, PLA, PHP, PLP |
| 索引与间接 | LDA (ptr),Y, STA (ptr),Y, LDA abs,X, STA abs,Y 等 |
以上指令组合起来,足以实现从简单平台跳跃到复杂格斗游戏的所有动作逻辑。如果你有特定的动作类型(如抛物线投掷、二段跳、受击硬直)需要代码示例,可以进一步说明。