Unity回合制战斗框架TBST:从网格寻路到AI决策的完整解决方案

1. 项目概述:为什么我们需要一个现成的回合制战斗框架?

做战棋或者策略类游戏,听起来很酷,但真正动手开发过的人都知道,这里面的水有多深。从最基础的网格地图生成、单位移动范围计算,到复杂的AI寻路、技能释放逻辑、状态机管理,再到战斗UI的血条、行动顺序条,每一个环节都能让开发者掉一层皮。很多时候,项目还没到核心玩法验证阶段,就已经被这些底层框架的“脏活累活”拖垮了。

这就是为什么当我看到Turn-Based Strategy Toolkit (TBST)这个Unity插件时,会感到眼前一亮。它不是一个简单的脚本集合,而是一个完整的、开箱即用的回合制战斗解决方案。它把战棋游戏(比如《火焰纹章》、《XCOM》)和策略游戏(比如《文明》系列)中最核心、最通用的那部分系统,提前帮你搭建好了。你不再需要从零开始写一个A*寻路算法,也不用头疼如何优雅地管理几十个单位的状态。TBST提供了一个经过验证的架构,让你能把宝贵的开发时间,集中在打磨你游戏独一无二的玩法、剧情和美术表现上。

简单来说,TBST就是一个“战棋游戏引擎中的引擎”。它封装了底层复杂性,暴露了高度可配置的接口。无论你是想做一个致敬经典的日式SRPG,还是一个充满随机性的Roguelike策略游戏,甚至是带有战棋元素的RPG,这个工具箱都能提供一个坚实的起点。接下来,我就结合自己实际使用的经验,把这个插件的里里外外拆解清楚,告诉你它到底能做什么,以及怎么用它来高效地搭建你的策略世界。

2. 核心模块深度解析:TBST的四大支柱

TBST的架构非常清晰,主要围绕四个核心模块展开:网格地图系统、单位管理系统、AI逻辑系统和战斗UI系统。这四个模块相互独立又紧密协作,共同构成了一个完整的回合制战斗循环。

2.1 网格地图系统:战场的基石

所有战棋游戏的舞台都是一个被划分好的网格。TBST的网格系统是其最核心的组件之一,它不仅仅是视觉上的格子,更承载了地形、移动成本、视野阻挡等丰富的游戏逻辑数据。

网格数据与生成TBST通常提供两种网格生成方式:基于Tilemap(适合2D或2.5D游戏)和基于Mesh(适合3D游戏)。我个人更常用基于Tilemap的方式,因为它与Unity的2D工作流集成得更好,美术资源制作也方便。

// 示例:通过代码动态创建一个10x10的网格 public class BattleGridManager : MonoBehaviour { public Grid grid; // Unity的Grid组件 public Tilemap walkableTilemap; // 可行走层 public Tile walkableTile; // 可行走格子的Tile void GenerateGrid(int width, int height) { for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { Vector3Int tilePosition = new Vector3Int(x, y, 0); walkableTilemap.SetTile(tilePosition, walkableTile); // 同时,需要向TBST的网格管理器注册这个格子,并设置其属性 // 例如:移动成本为1,地形类型为“平原” } } } }

每个网格节点(Node)在TBST内部都是一个数据结构,至少包含以下信息:

  • 坐标:在网格中的位置。
  • 移动成本:单位进入这个格子需要消耗的移动力。平原是1,森林可能是2,山脉可能是3甚至不可通行。
  • 地形效果:可能提供的防御加成、回避加成或特殊效果(如治疗泉)。
  • 占据状态:当前是否有单位站在上面。

寻路与移动范围计算这是网格系统的灵魂。TBST内置了高效的寻路算法(通常是A*算法的变种),你只需要关心规则。例如,计算一个移动力为5的单位可以到达的范围:

注意:寻路计算是性能敏感点,尤其是单位众多或地图很大时。TBST通常会做优化,比如缓存移动范围、使用跳跃点搜索(JPS)优化大范围空旷地图。在自定义地形成本非常复杂的场景中,你需要关注算法的性能表现。

实际操作中,你调用一个类似CalculateMovementRange(unit, startNode)的方法,它会返回一个所有可达节点的列表,并且通常会自动排除被敌方单位占据的格子(除非有“穿越”技能)。更高级的是,它还能计算“攻击范围”——即从每个可达格子出发,单位武器能攻击到的区域,这常用于显示单位的威胁范围。

2.2 单位管理系统:战场上的棋子

单位(Unit)是玩家的化身,也是所有策略的执行者。TBST的单位管理系统提供了一个可扩展的基类,用于定义单位的属性和行为。

单位属性与组件化设计一个标准的TBST单位通常会包含以下核心属性:

  • 基础属性:生命值(HP)、魔法值(MP)、攻击力、防御力、速度等。
  • 战斗属性:命中率、暴击率、格挡率等。
  • 状态信息:当前是否处于“待机”、“移动中”、“行动结束”或“异常状态”(如中毒、眩晕)。

TBST通常采用组件(Component)模式来组织这些功能。例如:

  • MovementComponent: 处理移动逻辑,包含移动力属性。
  • AttackComponent: 处理攻击逻辑,包含攻击范围、伤害计算公式。
  • SkillComponent: 管理单位所拥有的技能列表。
  • StatusEffectComponent: 管理单位身上的增益和减益效果。

这种设计的好处是灵活。如果你想给单位添加一个“潜行”技能,不需要修改核心的Unit类,只需要创建一个StealthComponent并挂载上去即可。

单位行动流程回合制战斗的核心是“行动点”或“行动阶段”。TBST规范了一个单位的典型行动流程:

  1. 回合开始:触发“回合开始”事件,用于回复少量HP/MP或处理持续伤害。
  2. 选择行动:玩家或AI为单位选择移动、攻击、使用技能、道具或待机。
  3. 执行移动:如果选择了移动,单位沿计算好的路径移动到目标格。这个过程通常有平滑的动画插值。
  4. 执行主要行动:移动后(或不移动),执行攻击或技能。此时会进入一个子状态,比如选择目标、播放攻击动画、计算伤害。
  5. 回合结束:行动执行完毕后,单位状态变为“已行动”,触发“回合结束”事件。

TBST通过一个状态机来管理这个流程,确保逻辑不会乱套。比如,一个单位在“移动中”状态时,是不能接收攻击指令的。

2.3 AI逻辑系统:与聪明的电脑对战

没有AI的战棋游戏是没有灵魂的。TBST的AI系统提供了一套框架,让你能够相对轻松地构建具有挑战性的电脑对手。

基于目标的AI决策树TBST的AI通常不是深度学习那种黑盒,而是基于规则和目标的、可预测的“脚本化”AI,这正适合策略游戏。其决策过程可以简化为:

  1. 评估目标:AI单位当前的首要目标是什么?是攻击最脆弱的后排?还是去占领某个据点?TBST允许你为AI设置不同的“目标权重”。
  2. 生成可行方案:AI会枚举所有可能的行动组合。例如:“移动到A点攻击玩家牧师”、“移动到B点使用范围治疗技能”、“原地不动进行防御”。
  3. 方案评分:对每个方案进行打分。评分规则可以自定义,例如:
    • 攻击方案得分 = 预期伤害 * 伤害权重 + (目标单位剩余HP百分比低) * 击杀权重 - 自身风险系数。
    • 治疗方案得分 = 可恢复生命值总量 * 治疗权重。
  4. 执行最高分方案:AI选择得分最高的方案并执行。

可配置的AI人格你可以通过调整评分规则的权重,来塑造不同的AI“人格”。比如:

  • 激进型:伤害权重极高,倾向于换血,无视自身风险。
  • 保守型:自身风险系数权重高,优先保证生存,喜欢待在治疗单位旁边。
  • 战术型:击杀权重高,优先攻击残血单位;或者对“占领据点”这个行为赋予高权重。

在TBST中,实现一个简单的攻击AI可能像下面这样:

public class AggressiveAI : UnitAIBase { public override Action DecideAction(Unit aiUnit, List<Unit> playerUnits) { Action bestAction = null; float bestScore = float.MinValue; foreach (Unit target in playerUnits) { // 1. 检查是否能攻击到目标 if (CanAttackTarget(aiUnit, target, out AttackData attackData)) { // 2. 为这次攻击评分 float score = CalculateAttackScore(aiUnit, target, attackData); // 3. 更新最佳行动 if (score > bestScore) { bestScore = score; bestAction = new AttackAction(aiUnit, target, attackData); } } } // 如果没有找到攻击目标,则移动到离最近敌人最近的位置 if (bestAction == null) { bestAction = CalculateMoveToNearestEnemyAction(aiUnit, playerUnits); } return bestAction; } private float CalculateAttackScore(Unit attacker, Unit defender, AttackData data) { float score = data.estimatedDamage; // 基础分:预期伤害 score += (1f - defender.HPPercentage) * 50f; // 奖励分:目标血量越低,分越高(鼓励补刀) score -= attacker.CalculateRiskAfterAttack(defender) * 20f; // 惩罚分:攻击后自身风险 return score; } }

实操心得:AI的调试是个耐心活。TBST通常提供AI行为可视化工具,比如在编辑器中显示AI单位的“视野”、“可攻击范围”和“当前最佳目标”。善用这些工具,能极大提高调试效率。另外,别忘了给AI加入一定的随机性,让玩家的体验不会每次都一样。

2.4 战斗UI系统:信息的窗口

清晰、及时的战斗UI是策略游戏体验的重要组成部分。TBST提供了一套可定制的UI预制件和绑定逻辑。

核心UI组件

  • 单位信息面板:当鼠标悬停或选中一个单位时显示,包含其头像、HP/MP条、属性数值和状态图标。
  • 行动菜单:单位选中后出现的环形或列表菜单,包含“移动”、“攻击”、“技能”、“道具”、“待机”等选项。
  • 战斗预测窗口:在玩家选择攻击或技能目标时弹出,显示命中率、预期伤害范围、暴击概率等关键信息。这是减少玩家挫败感的关键设计。
  • 回合指示器:显示当前是“玩家回合”还是“敌方回合”,以及行动顺序列表(如果游戏有速度属性决定出手顺序)。

数据驱动与事件绑定TBST的UI系统通常是数据驱动的。UI元素不直接操作游戏逻辑,而是监听游戏状态的变化。例如:

  • UnitOnHPChanged事件被触发 →HealthBarUI组件接收到事件 → 更新血条填充值和平滑动画。
  • BattleManagerOnTurnStarted事件被触发 →TurnIndicatorUI组件接收到事件 → 高亮当前行动的单位。

这种松耦合的设计让你可以随意替换UI美术资源,而无需修改核心游戏代码。你只需要确保新的UI预制件上有相同的脚本组件,并订阅了正确的事件。

3. 实战构建:从零搭建一个简易战棋关卡

理论说了这么多,我们来点实际的。假设我们要用TBST创建一个最简单的关卡:一张小地图,一个玩家英雄,一个敌方小兵。

3.1 环境准备与基础设置

首先,在Unity中导入TBST插件。通常它会创建几个重要的管理器预制件,比如BattleManagerGridManagerUnitManager。你需要把这些拖入场景,或者让它们在运行时动态生成。

场景搭建

  1. 创建一个Grid游戏对象,添加Unity的Grid组件。
  2. 创建两个Tilemap作为子对象,一个叫WalkableLayer(可行走层),一个叫ObstacleLayer(障碍层)。
  3. 在TBST的GridManager组件上,将这两个Tilemap分别赋值给“可行走图层”和“障碍图层”字段。这样TBST就能自动从Tilemap读取地图数据。

单位创建

  1. 创建一个3D模型或2D精灵作为英雄的视觉表现。
  2. 给这个游戏对象添加TBST提供的Unit组件(或继承自它的HeroUnit脚本)。
  3. Unit组件上配置基础属性:HP=30,攻击力=8,防御力=3,移动力=5。
  4. 添加MovementComponentAttackComponent,并在攻击组件上设置攻击范围为“相邻一格”,伤害公式为简单的“攻击力-防御力”。
  5. 重复上述步骤,创建一个属性稍弱的EnemyUnit(HP=20,攻击力=6,防御力=2,移动力=4)。

3.2 配置战斗流程与规则

接下来,需要配置战斗如何开始、进行和结束。

战斗初始化: 在BattleManager中,你需要指定玩家单位列表和敌方单位列表。TBST会在战斗开始时,根据单位的“队伍”属性,将它们分配到对应的阵营。

// 在一个初始化脚本中 void StartBattle() { List<Unit> playerTeam = new List<Unit> { playerHeroUnit }; List<Unit> enemyTeam = new List<Unit> { enemySoldierUnit }; BattleManager.Instance.InitBattle(playerTeam, enemyTeam); BattleManager.Instance.StartPlayerTurn(); // 玩家先手 }

胜利/失败条件: TBST允许你自定义胜利条件。最常见的是“全歼敌人”。你需要在BattleManager中订阅单位死亡事件,并检查条件。

void OnUnitDied(Unit deadUnit) { if (deadUnit.team == Team.Enemy) { bool allEnemiesDead = UnitManager.Instance.GetAllUnits(Team.Enemy).Count == 0; if (allEnemiesDead) { Debug.Log("战斗胜利!"); // 触发胜利UI、奖励发放等 } } else if (deadUnit.team == Team.Player) { bool allPlayersDead = UnitManager.Instance.GetAllUnits(Team.Player).Count == 0; if (allPlayersDead) { Debug.Log("战斗失败..."); } } }

3.3 连接UI与实现玩家输入

现在,我们需要让玩家能控制他的英雄。

鼠标交互: TBST通常提供一套鼠标输入处理器。你需要做的是:

  1. 确保场景中有CameraController(控制镜头)和MouseInteractionManager(处理鼠标悬停、点击)。
  2. 当鼠标悬停在一个可移动格子上时,MouseInteractionManager会高亮该格子。
  3. 当玩家点击一个已选中的单位,然后点击一个高亮的可移动格子时,触发单位的移动命令。

UI绑定: 将TBST提供的UnitInfoPanel预制件放入Canvas。在UnitInfoPanel组件上,它会自动查找场景中的MouseInteractionManagerSelectionManager,当有单位被悬停或选中时,自动更新面板上的信息。

行动菜单: 当玩家选中自己的单位后,实例化ActionMenu。菜单上的每个按钮都绑定了一个具体的Action命令(如MoveAction,AttackAction)。点击“攻击”按钮后,游戏状态进入“选择攻击目标”模式,此时所有敌方单位会被高亮,点击其中一个即执行攻击。

4. 进阶技巧与性能优化

当你的游戏规模变大,单位增多,技能效果复杂后,一些潜在的问题就会浮现。这里分享一些TBST框架下的进阶处理技巧。

4.1 复杂技能与状态效果系统

TBST的基础攻击组件可能只支持简单的单体攻击。要实现一个“对3x3范围内所有敌人造成伤害,并附加中毒效果”的技能,你需要扩展技能系统。

技能数据脚本化: 建议使用ScriptableObject来定义技能数据。这是一个资源文件,可以在编辑器里配置,无需写死代码。

[CreateAssetMenu(fileName = "NewAreaPoisonSkill", menuName = "TBST/Skills/AreaPoison")] public class AreaPoisonSkillData : SkillData { public int range; // 施法范围 public DamageData damage; public StatusEffectData poisonEffect; // 中毒效果的数据 public GameObject castVFX; // 施法特效 public override void Execute(Unit caster, Vector3Int targetCell) { // 1. 播放施法动画和特效 Instantiate(castVFX, GridManager.GetWorldPosition(targetCell), Quaternion.identity); // 2. 获取目标区域所有单位 List<Unit> targets = GetUnitsInArea(targetCell, 3, 3, Team.Enemy); // 3. 对每个目标应用伤害和效果 foreach (Unit target in targets) { CombatCalculator.CalculateDamage(caster, target, damage); target.StatusEffectComponent.ApplyEffect(poisonEffect); } // 4. 消耗MP,结束单位行动 caster.ConsumeMP(this.mpCost); caster.EndTurn(); } }

状态效果管理器: 中毒、眩晕、防御提升等状态效果需要持续多个回合。TBST可能有一个基础的StatusEffect类,你需要实现一个StatusEffectManager来负责效果的添加、移除、持续时间和回合触发

关键点是效果的叠加规则刷新机制。例如,同一个单位被施加两次“攻击力提升50%”的效果,是叠加为100%,还是取最大值,还是持续时间刷新?这需要在StatusEffect基类中设计好。

4.2 寻路与范围计算性能调优

当地图达到100x100,单位有几十个时,每回合为每个单位计算移动和攻击范围会成为性能瓶颈。

缓存是王道

  • 移动范围缓存:如果一个单位本回合没有移动,且地图上的障碍物没有变化,那么它的移动范围是不会变的。可以在单位移动或地图改变时才重新计算。
  • 攻击范围缓存:攻击范围通常只取决于单位的武器和自身位置。如果单位没移动,攻击范围也可以缓存。

使用更高效的算法

  • TBST内置的A*算法对于标准网格通常足够快。但对于超大地图,可以研究插件是否支持切换为Dijkstra(用于计算所有格子的移动成本)或BFS(用于计算固定移动力的范围),它们在特定场景下可能更快。
  • 分层寻路:对于大地图,可以先在由“房间”或“区域”组成的粗粒度网格上寻路,再在目标区域内进行精细寻路。

减少不必要的计算

  • 只在需要显示(如鼠标悬停)或AI决策时,才计算单位的移动/攻击范围。
  • 使用Job SystemBurst Compiler进行并行计算。一些高级的TBST插件或自己实现的扩展,可以考虑将寻路这种纯计算任务放到Job中,能极大提升性能。

4.3 网络同步与存档读档考虑

虽然TBST主要面向单机游戏,但你的项目可能有多人模式或需要稳定的存档功能。

确定性逻辑: 回合制游戏是实现网络同步和确定性回放的绝佳类型,因为所有输入都是离散的。关键在于确保整个游戏逻辑是确定性的——在相同的随机种子下,相同的操作序列必须产生完全相同的结果。

  • 避免使用UnityEngine.Random:使用自定义的、可序列化的伪随机数生成器(PRNG),并将随机种子存入存档或通过网络同步。
  • 浮点数精度:不同平台(CPU架构)的浮点数运算可能有细微差异。对于关键计算(如伤害公式),考虑使用定点数或确保运算顺序一致。

存档数据设计: 存档需要保存整个战斗的状态。TBST的各个管理器(GridManager,UnitManager,BattleManager)应该提供序列化接口。

  • 单位数据:保存每个单位的唯一ID、位置、当前HP/MP、状态效果列表等。
  • 战场数据:保存当前回合数、当前行动方、随机数种子等。
  • 序列化方式:可以使用JsonUtilityNewtonsoft.Json或二进制格式。记住,只保存必要的数据,不保存对MonoBehaviourGameObject的引用。

5. 常见问题排查与避坑指南

在实际使用TBST的过程中,你肯定会遇到一些“坑”。下面是我和社区里常见的一些问题及解决方案。

5.1 单位移动或寻路异常

问题现象可能原因解决方案
单位无法移动到看似可达的格子1. 该格子的移动成本被设置为无限大(不可通行)。
2. 该格子被ObstacleLayer上的Tile占据。
3. 单位的MovementComponent的移动力不足。
1. 检查TBSTGridNode数据中该格子的移动成本。
2. 检查ObstacleLayerTilemap。
3. 在编辑器中选中单位,查看其当前移动力。
单位寻路绕远路,不走直线1. A*算法的启发函数权重设置不当。
2. 对角线移动成本设置不正确(应为1.414,但被近似为1或2)。
3. 地图中存在移动成本差异巨大的区域,算法在“绕开高成本区”和“走直线”间权衡。
1. 调整A启发函数的权重(如从1.0调到1.1可能更倾向于直线)。
2. 确认TBST网格设置中是否允许对角线移动及其成本计算方式。
3. 这是正常现象,A
会找“成本最低”路径而非“格数最少”路径。
移动路径显示正常,但单位“卡住”不动1. 移动动画系统出现问题(如动画控制器状态未切换)。
2. 单位移动的终点坐标与世界坐标有微小偏差,导致“到达”判断失败。
3. 有其他逻辑(如碰撞体、触发器)阻止了移动完成事件。
1. 检查单位的Animator在收到Move命令时是否正确触发。
2. 在移动逻辑的终点判断中,增加一个很小的容差(epsilon),例如if (distanceToTarget < 0.1f) StopMoving()
3. 使用Debug.Log或断点,检查移动协程或状态机是否正常进入完成状态。

5.2 AI行为不符合预期

问题:AI单位像个“傻子”,原地发呆或来回踱步。

  • 排查1:目标评分函数有缺陷。检查你的CalculateAttackScore或类似函数。是不是所有行动方案的得分都是0或负数?可能是计算风险的惩罚分太高,导致AI认为任何行动都“太危险”。尝试调整权重参数,或者为“待机”行动设置一个基础分(比如5分),让AI在找不到好目标时至少会选择待机。
  • 排查2:行动可行性判断过于严格。CanAttackTarget函数中,你是否检查了“攻击路径上不能有友军阻挡”?在一些游戏中,远程攻击是可以越过友军的。确认你的规则是否符合设计。同样,移动可行性也要检查,是否因为忽略了“单位占据”而导致AI认为无处可去。
  • 排查3:没有为AI设置“默认行动”。在决策循环的最后,如果bestAction仍然是null,必须有一个保底行动,比如“向最近敌人移动”或“使用防御技能”。

实操心得:调试AI时,一定要将其思考过程“可视化”。我通常会在AI决策时,用Debug.DrawLine画出它评估的攻击路径,或者用临时UI文字显示它给每个行动打的分数。这样一眼就能看出AI为什么做出了“愚蠢”的选择。

5.3 UI显示不同步或更新延迟

问题:单位血条不更新,或者行动菜单在单位死亡后仍然显示。

  • 根源:事件订阅与取消订阅。这是Unity事件系统开发中最常见的Bug之一。确保在UI脚本(如HealthBar)的OnEnable方法中订阅单位的事件(如OnHPChanged),在OnDisableOnDestroy方法中取消订阅。否则,当单位被销毁(如死亡)或UI被禁用时,事件引用会残留,导致错误或内存泄漏。
  • 检查数据绑定时机:UI刷新的时机应该在数据变化之后。确保是Unit.HP属性先改变,然后触发事件,最后UI响应事件更新。不要在改变HP的同一帧直接调用UI更新方法,这可能导致执行顺序问题。
  • 使用Unity的EventSystem有些TBST版本可能深度集成Unity的UI事件系统。检查是否有IPointerEnterHandler这样的接口需要正确实现,以及EventSystem.current.IsPointerOverGameObject()是否会阻断你对3D游戏对象的点击。

5.4 打包后出现的诡异问题

问题:在编辑器里运行完美,打包(Build)后单位无法移动或AI不工作。

  • 首要怀疑:资源引用丢失。检查所有在代码中通过public GameObject prefab;[SerializeField]拖拽赋值的字段。在打包时,确保这些Prefab或ScriptableObject资源被正确包含在构建中。对于动态加载的资源(如Addressables),确保打包设置和运行时加载代码正确。
  • 其次怀疑:路径或坐标计算差异。编辑器和打包后,某些底层API(如Application.dataPath)的返回值不同。如果你的代码里硬编码了资源路径,打包后肯定会失效。TBST框架本身应该处理好了这些问题,但你的扩展代码需要注意。
  • 使用Development Build:打包时勾选Development BuildAutoconnect Profiler。这样当游戏在玩家电脑上运行时,你可以在编辑器的Profiler中远程连接,查看错误日志和性能状况,这是定位打包后Bug的利器。

最后,关于TBST插件的学习,最好的资料除了官方文档,就是其自带的示例项目(Example/Scenes)。务必仔细研究每一个示例场景,从最简单的移动攻击,到复杂的技能连锁和AI行为树。尝试修改示例中的参数,观察变化,这是最快上手的方法。记住,框架是为你服务的工具,当你熟悉了它的运作方式后,就可以大胆地修改和扩展它,让它完美适配你那独一无二的策略游戏创意。