记录一下回合制战斗系统的实现思路,想到哪里写到哪里。

核心问题
设计难点在于服务器应当返回怎样的数据结构——既简单又便于实现,数据量小,同时具备常规工程化所需的特点。

回合制游戏的一般流程:

等待玩家输入
限定时间内
服务器计算
该回合战斗数据
客户端表现
播放战斗动画
循环
直到战斗结束

梦幻西游为例:

  • 对于概率性的技能,输出一定是一个确定的结果(触发 / 未触发)
  • 对于伤害数值,输出的是一个具体数字,并包含暴击等信息

方案 1:纯技能数据驱动

如果数据格式基于招式(技能,平A也可以看做一个技能),那么每个可攻击单位每次攻击行为的数据可能是这样的:

单次攻击行为的数据结构

对于一次完整的战斗,可能涉及多达 20 个单位,每个单位每次攻击的数据约 220 字节,一个回合约 4KB,10 回合仅战斗动作数据就达 40KB。

官网录像 — 帮战 5 回合,每回合一个 SkillAction 数组依次播放
方案 1 的致命问题
如果有技能会与当前攻击行为产生交互(比如反震),处理起来就非常棘手。方案 1 pass。

方案 2:客户端参与战斗逻辑

针对方案 1 的缺点,方案 2 做了如下改进:客户端参与战斗逻辑处理,数值和概率等由服务器发送,其余结果可以在本地产生。即使出现外挂,也只影响该玩家自身的体验,关键数据仍由服务器掌控。

同一个技能在客户端和服务端的数据差异
❌ 方案 1
服务器输出完整战斗数据,客户端纯播放。技能交互(反震等)难以处理,数据量大。
✅ 方案 2
客户端参与逻辑演算,服务器只发概率和关键数值。数据量小,交互灵活,但客户端和服务端都需实现技能逻辑。

异步战斗是方案 2 的极端形式:战斗开始前,服务器已经把结果全部计算好,客户端只需模拟展现。服务器只传递概率性的已确定事件,客户端按数据快照(装备信息等)模拟出整个过程。

之前做的小游戏三国志OL就采用了这种方式。但对于更复杂的游戏(如迷你西游),单位攻击时可能存在反馈机制(反击等),数据结构需要用树形结构来表达因果关系:

树形结构 — 从上往下、从左往右执行的顺序结构
每个动作节点的类型定义

无论是半自动还是全自动的回合战斗,都可以用这种数据结构来表示。关于具体实现还可以参考行为树框架——XYGame-AI设计4-行为树-第2版本

客户端不参与技能逻辑时的处理方案

如果客户端不参与技能的具体逻辑感知,就需要包含具体的事件信息。来看几个典型场景:

1
A 对 B 发起技能1,走到 B 面前砍了一刀,此时触发了反震效果。反震是在技能1逻辑执行过程中发生的。
2
技能1发起完毕、A 回到站位后,B 对 A 发起了反击。这是在 A 行动完毕后执行的,处理较简单。
3
A 准备在 B 面前砍一刀时,队友触发保护技能,站到 B 面前。A 砍下去后队友受到了伤害。

这类在技能执行期间嵌入复杂逻辑的情形,可以通过配置表来配置技能的关键执行点或逻辑组合——添加或修改技能逻辑与数值,只需修改配置表。

处理手法:这类情况都具有很强的顺序性(触发反震一定是因为攻击),需要预读下一个技能,判断关联性:

预读下一个技能,判断因果关联

通过配置表定义 3 个截断点:技能释放前、释放中、释放后。按服务器的验算逻辑序列化来看:

服务器验算的逻辑序列

对于技能1触发了2个技能的平行关系,可以通过加权判定来决定执行顺序。这种方式本质上回到了行为树的框架思路——因果关系用子树表示:

1V1 战斗的动作树 — 因果关系用子树表示,与行为树几乎一致
Action 节点的数据结构

录像系统的设计考量

录像只需保存这棵树。针对不同需求,有以下考量:

任意回合跳转
每个回合之间不能有耦合性,每个回合初始都需保存当前战局快照,保证各回合相互独立。
仅快进/后跳
只保留初始回合的快照即可,节省数据。体验不如方案1,但实现简单。
版本兼容问题
如果客户端知晓了具体逻辑,版本变更时技能或逻辑冲突会导致严重的录像兼容问题。

两种战斗模式分析

模式 1:回合制 Buff
  • Buff/Debuff 基于回合计算
  • 梦幻西游:半自动,每回合可下达指令
  • 迷你西游:全自动,异步战斗
  • 服务器交互仅限于每回合等待指令
  • 客户端可自由控制播放速度
模式 2:实时制 CD
  • 技能和 Buff 按时间计算
  • 偏向动作 MMO 的操作方式
  • 技能释放完成即可下达下一个指令
  • 客户端和服务器各维护一套 CD
  • 对录像系统不友好

模式 1 引入 CD 时,可在每回合开始节点的快照中包含 CD 信息,客户端根据时间做插值。模式 2 若像一般 MMO 那样用消息处理 CD,录像系统则需额外做消息快照——或者客户端自己通过配置表模拟 CD,但一旦存在「减少全局 CD 50%」「立刻冷却所有技能」这类逻辑,就行不通了。

动作序列的解析与执行

把树形结构解析为一个个独立动作。之所以采用树形结构,是因为它能够表达因果关系。树的层次通常不高,宽度与执行动作的单位数量正相关。

树形结构遍历后生成一维动作序列

逻辑处理器只需按生成的序列逐个执行。由于是一维信息,需要约定固定操作节点:

动作序列节点类型
顺序节点 依次往下逐个执行,遇到结束节点才停止
并行节点 一次处理结束节点之前的所有节点(如同时回血回蓝)
结束节点 标记一段因果关系的结束
动作节点 具体的技能/攻击/效果

每遇到一个结束节点,表示一段因果关系结束。每段因果关系序列的父节点,总是导火索——比如"B 对 A 使用了反震"的导火索就是"A 对 B 使用了技能1"。

每个节点的执行状态:

Idle 空闲
Running1 执行中(砍下前)
Running2 执行中(砍下后)
Finish 完成
节点状态与因果关系切入时机
根本矛盾
在各粒度嵌套很深的关系中,信息要么全部由服务器发送,要么只发送一部分,未发送的部分由客户端通过配置补充——一旦存在迭代更新,就会出问题。

解决思路:录像信息由客户端生成时,将未发送的信息也一并快照记录下来,录像播放机就变成了纯播放器。但如果客户端知晓了具体逻辑的代码(而非配置数据),这种做法就行不通——代码片段无法保存以供录像回放,虽然可以考虑变成脚本,但前提是脚本涉及的非脚本代码能保持兼容,几乎是一个恶性循环。