尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

Unity回合制战斗系统:状态机、网格寻路与技能架构实战

Unity回合制战斗系统:状态机、网格寻路与技能架构实战
📅 发布时间:2026/7/9 21:28:30

1. 项目概述与核心价值

最近在整理自己的项目库,翻到了一个几年前做的战旗回合制格斗游戏Demo,当时是为了验证一套基于状态机的战斗逻辑框架。看到网上很多朋友在找类似的免费资源,索性就把这个项目的核心源码和设计思路整理出来,分享给大家。这不仅仅是一份“源码”,我更想把它看作一个可拆解、可学习、可二次开发的Unity3D回合制战斗系统模板。

这个项目麻雀虽小,五脏俱全。它实现了一个经典的战棋(SRPG)玩法:在一个网格地图上,敌我双方单位轮流行动,每个回合可以移动、攻击或释放技能。同时,它又融合了传统格斗游戏的元素,比如攻击命中时的受击反馈、连招Combo的潜力设计,以及技能的前摇、后摇帧管理。对于想入门回合制游戏开发,或者想深入研究Unity中状态管理、网格寻路、战斗数值计算的开发者来说,这个项目有不错的参考价值。

它适合谁呢?如果你是Unity初学者,想通过一个完整的项目理解游戏循环、预制体、动画状态机等基础概念,这个项目提供了清晰的场景结构。如果你是有一定经验的开发者,正在为你的独立游戏寻找一个稳定可靠的回合制战斗底层框架,这里的基于网格的移动系统、可扩展的技能系统和基于事件驱动的UI交互,能为你节省大量前期架构设计的时间。最关键的是,它完全免费,没有任何加密或依赖不明资产,所有代码都开放,你可以随意修改、学习甚至用于你的商业项目(当然,直接套用美术资源可能涉及版权,请注意)。

2. 核心系统设计与架构拆解

拿到一个游戏源码,直接一头扎进代码里是最低效的。我们先从顶层设计看起,理解作者是如何组织各个模块的,这比看懂某一行代码更重要。

2.1 整体架构:基于MVC的变体

这个项目没有严格遵循教科书式的MVC,而是采用了一种在Unity中更实用的数据-逻辑-表现分离架构。我们可以将其理解为:

  • Model(数据/逻辑层): 核心是GameManager单例和Unit(单位)类。GameManager掌管整个游戏流程,如回合切换、胜负判定、地图数据(一个二维数组表示的网格)。Unit类则封装了一个战斗单位的所有属性和核心逻辑,如生命值、攻击力、移动范围、所拥有的技能列表等。所有核心计算(伤害、移动路径)都发生在这里。
  • Controller(控制层): 主要是UnitController和InputHandler。UnitController挂载在每个单位GameObject上,它接收玩家的输入指令(通过InputHandler),然后调用Unit模型层的方法执行移动或攻击,并触发视图层的更新。InputHandler负责处理鼠标点击、悬停等交互,将屏幕坐标转换为游戏网格逻辑坐标。
  • View(表现层): 包括所有看得见的部分:UnitView(负责播放单位的移动、攻击、受击动画)、GridHighlighter(高亮显示可移动范围、攻击范围)、UIManager(显示血条、行动菜单、战斗日志)。这一层严格依赖模型层的数据,模型层状态变化后,通过C#事件或Unity的SendMessage/UnityAction通知视图层更新。

这种架构的好处是清晰。当你需要修改一个单位的攻击力时,你只需改动Unit模型类的数值;当你觉得血条样式不好看,你只需修改UIManager中血条预制体的样式,无需触碰核心逻辑代码。

2.2 状态机:游戏逻辑的脊柱

回合制游戏的核心是“状态”。这个项目里至少有两套关键的状态机在协同工作:

  1. 游戏全局状态机(GameState): 由GameManager管理。通常包括PlayerTurn(玩家回合)、EnemyTurn(敌人回合)、UnitActing(单位行动中)、GameOver等状态。状态切换驱动着游戏流程。例如,当最后一个敌人单位被击败,状态从EnemyTurn切换到GameOver,并触发胜利UI。
  2. 单位动画与行为状态机(Animator & UnitState): 每个单位都有一个Unity Animator Controller,管理Idle(待机)、Walk(移动)、Attack(攻击)、Hit(受击)、Die(死亡)等动画状态。更重要的是,在代码层面,UnitController内部维护着一个简单的UnitActionState(如Selecting选择目标、Moving移动中、Attacking攻击中),这个逻辑状态与Animator的动画状态通过参数(Parameters)同步,确保角色的行为与动画表现一致。

实操心得:在Unity中,不要试图用一套状态机管理所有事情。将“游戏流程状态”和“单位行为/动画状态”分开,能让代码更清爽。游戏状态机可以用简单的enum加switch语句实现,而单位行为状态机则更适合与Animator深度结合。

2.3 网格与寻路系统

战棋游戏的舞台是网格。本项目使用一个简单的二维数组int[,] mapGrid来表示地图,其中不同的整数值代表地形(如0=平地可通行,1=障碍物,2=高地有防御加成)。

  • 移动范围计算: 核心算法是广度优先搜索(BFS)。当玩家选中一个单位时,系统会以该单位所在网格为起点,根据单位的移动力(MoveRange)向外层层扩散,标记出所有在移动力内且可通行的网格。这里会考虑地形消耗,例如沼泽地可能每格消耗2点移动力。
  • 路径查找与显示: 在BFS计算的过程中,系统会同时记录每个可达网格的“父节点”。当玩家点击一个目标网格时,系统通过从终点回溯父节点,快速生成一条最短路径。GridHighlighter脚本会实例化一些半透明的色块(如蓝色代表可移动,红色代表可攻击)来可视化这些范围。
  • 攻击范围: 通常基于“曼哈顿距离”或“切比雪夫距离”计算。例如,一个近战单位的攻击范围可能是自身周围1格(曼哈顿距离<=1)。计算方式与移动范围类似,但只检查距离,不考虑路径是否被阻挡(除非设定为“直线攻击”)。

3. 核心模块深度解析与实现

理解了架构,我们深入几个最关键的模块,看看代码是如何具体实现的。

3.1 单位(Unit)数据与技能系统

Unit类是战斗的基石。它的主要属性包括:

public class Unit : MonoBehaviour { public string unitName; public int maxHP; private int currentHP; public int attackPower; public int defense; public int moveRange; public List<Skill> skills; // 技能列表 public Faction faction; // 阵营:Player 或 Enemy // 关键方法 public bool TakeDamage(int damage) { int finalDamage = Mathf.Max(1, damage - defense); // 简易伤害公式 currentHP -= finalDamage; if (currentHP <= 0) { currentHP = 0; Die(); } OnHPChanged?.Invoke(currentHP, maxHP); // 触发事件更新血条UI return currentHP <= 0; } public void UseSkill(Skill skill, Unit target) { // 消耗MP、计算冷却等 skill.Execute(this, target); // 执行技能效果 } }

技能系统通过Skill基类实现多态。这是一种非常优雅的设计:

public abstract class Skill : ScriptableObject // 使用ScriptableObject便于在编辑器内配置 { public string skillName; public int mpCost; public int range; public Sprite icon; public abstract void Execute(Unit caster, Unit target); } // 具体技能类型 [CreateAssetMenu(fileName = "New AttackSkill", menuName = "Skills/Attack")] public class AttackSkill : Skill { public int powerMultiplier = 100; // 百分比 public override void Execute(Unit caster, Unit target) { int damage = caster.attackPower * powerMultiplier / 100; target.TakeDamage(damage); } } [CreateAssetMenu(fileName = "New HealSkill", menuName = "Skills/Heal")] public class HealSkill : Skill { public int healAmount; public override void Execute(Unit caster, Unit target) { target.Heal(healAmount); } }

使用ScriptableObject来定义技能,意味着你可以在Unity编辑器里像配置资源一样,拖拽设置技能的名称、图标、消耗和效果参数,无需硬编码。当需要新增一个“造成中毒效果”的技能时,你只需新建一个PoisonSkill类继承Skill,并实现其Execute逻辑即可,原有系统完全不受影响。

3.2 回合流程与AI实现

GameManager中的回合流程控制是游戏的大脑:

void Update() { switch (currentGameState) { case GameState.PlayerTurn: // 等待玩家操作,由InputHandler触发 break; case GameState.EnemyTurn: if (!isAIActing) { StartCoroutine(EnemyAITurn()); // 使用协程让AI行动有间隔,便于观察 } break; case GameState.UnitActing: // 等待单位行动(移动、攻击动画)完成 break; } } IEnumerator EnemyAITurn() { isAIActing = true; foreach (Unit enemy in enemyUnits) { if (enemy.IsDead) continue; // 1. 寻找最近玩家单位 Unit nearestPlayer = FindNearestPlayer(enemy); if (nearestPlayer == null) break; // 2. 计算移动路径,尝试靠近玩家 List<Vector2Int> path = CalculatePathTo(enemy.gridPos, nearestPlayer.gridPos); if (path.Count > 1 && path.Count <= enemy.moveRange + 1) { yield return StartCoroutine(enemy.MoveAlongPath(path)); // 移动 } // 3. 判断是否在攻击范围内 if (IsInAttackRange(enemy, nearestPlayer)) { yield return StartCoroutine(enemy.Attack(nearestPlayer)); // 攻击 } yield return new WaitForSeconds(0.5f); // 每个AI行动间隔,增加可读性 } EndEnemyTurn(); // 切换到玩家回合 isAIActing = false; }

这是一个非常基础的“寻敌-移动-攻击”AI。在实战中,你可以扩展它,比如让AI优先攻击血量最低的单位,或者根据技能冷却和MP值智能选择技能。

注意事项:AI计算(特别是寻路)可能比较耗时。一定要把密集计算放在协程中分帧进行,或者使用async/await,避免在一帧内卡住主线程。对于大型地图,可以考虑使用A*算法替代BFS,并缓存寻路结果。

3.3 动画与事件融合

如何让单位的攻击动作播放到最高点时才触发伤害计算?这里用到了动画事件(Animation Event)。

  1. 在Unity动画窗口中,编辑Attack动画片段。
  2. 在攻击动作的拳头接触到敌人的那一帧,添加一个动画事件。
  3. 在事件函数栏填写一个方法名,例如OnAttackHit。
  4. 在UnitView或UnitController脚本中,定义这个OnAttackHit方法:
public void OnAttackHit() { // 此时才真正执行伤害计算 if (currentTarget != null) { bool isDead = currentTarget.TakeDamage(CalculateDamage()); // 可以在这里触发屏幕震动、音效、受击动画等 if (isDead) { PlayDeathAnimation(); } } }

这种方法实现了表现与逻辑的精准同步,是格斗和动作类游戏常用的技巧。同样,你可以在移动动画结束时触发“移动完成”事件,来通知GameManager进入下一个状态。

4. 关键实现步骤与代码剖析

让我们跟随一个完整的“玩家单位攻击”流程,把上述模块串联起来。

4.1 步骤一:玩家输入与目标选择

InputHandler脚本在Update中检测鼠标点击:

if (Input.GetMouseButtonDown(0) && GameManager.Instance.CurrentState == GameState.PlayerTurn) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { GridCell cell = hit.collider.GetComponent<GridCell>(); if (cell != null && cell.IsHighlighted) { // 点击了高亮网格(可能是移动终点或攻击目标) if (selectedUnit != null) { selectedUnit.MoveTo(cell.GridPosition); } } else { Unit unit = hit.collider.GetComponent<Unit>(); if (unit != null && unit.faction == Faction.Player) { // 点击了己方单位,选中它 SelectUnit(unit); } else if (unit != null && unit.faction == Faction.Enemy && selectedUnit != null) { // 点击了敌方单位,且己方单位已选中,尝试攻击 if (selectedUnit.IsInAttackRange(unit)) { selectedUnit.Attack(unit); } } } } }

SelectUnit(Unit unit)方法会做几件事:清除之前的高亮,计算并高亮该单位的移动范围,将UI行动菜单(攻击、技能、待机)显示在屏幕下方。

4.2 步骤二:移动与路径渲染

当玩家点击一个高亮的移动网格,Unit.MoveTo(Vector2Int targetPos)被调用:

public IEnumerator MoveAlongPath(List<Vector2Int> path) { controller.SetState(UnitActionState.Moving); foreach (Vector2Int step in path) { Vector3 worldPos = GridManager.Instance.GridToWorld(step); // 使用Lerp或DoTween制作平滑移动 yield return StartCoroutine(MoveToPosition(worldPos, moveSpeed)); gridPos = step; // 更新逻辑坐标 } controller.SetState(UnitActionState.Idle); OnMoveCompleted?.Invoke(); // 通知游戏管理器移动结束 }

移动过程中,UnitActionState.Moving会触发Animator播放行走动画。GridHighlighter会在移动开始前绘制出路径线(通常用LineRenderer实现)。

4.3 步骤三:攻击执行与伤害结算

玩家从行动菜单选择“攻击”或直接点击敌人后,Unit.Attack(Unit target)启动:

public IEnumerator Attack(Unit target) { if (IsInAttackRange(target)) { controller.SetState(UnitActionState.Attacking); currentTarget = target; // 1. 播放攻击动画(动画内包含事件触发OnAttackHit) animator.SetTrigger("Attack"); // 2. 等待攻击动画完成(可以通过动画事件或等待时间) while (controller.CurrentState == UnitActionState.Attacking) { yield return null; } // 3. 清理 currentTarget = null; EndTurn(); // 单位行动结束,可能切换控制权 } }

真正的伤害计算发生在OnAttackHit()动画事件中。计算完成后,调用target.TakeDamage(),该方法会触发OnHPChanged事件。监听此事件的UIManager会立即更新目标头顶的血条UI,如果血量归零,则播放死亡动画,并将该单位从活动单位列表中移除。

4.4 步骤四:回合结束与切换

一个单位行动结束后(移动后攻击,或直接待机),会调用EndTurn()。GameManager会检查当前阵营是否所有单位都行动完毕。如果是,则切换游戏状态:

public void OnUnitTurnEnd(Unit unit) { if (unit.faction == Faction.Player) { playerUnitsActedThisTurn++; if (playerUnitsActedThisTurn >= totalPlayerUnits) { // 所有玩家单位行动完毕,切换到敌人回合 StartEnemyTurn(); } } // ... 类似逻辑处理敌人 }

切换到敌人回合后,就进入了前面提到的EnemyAITurn()协程流程。

5. 性能优化与扩展方向

一个基础框架跑起来后,要考虑如何让它更健壮、更高效。

5.1 性能优化要点

  1. 对象池(Object Pooling): 战斗中的伤害数字、技能特效、高亮网格块都是频繁生成和销毁的对象。使用对象池可以极大减少GC(垃圾回收)压力。项目中可以为DamageText、GridHighlight等创建简单的对象池。
  2. 寻路优化: 当前BFS对于小地图没问题,但地图变大后效率低。可以引入A*寻路算法,它通过启发式函数更智能地寻找最短路径。Unity的NavMesh虽然强大,但对于严格的网格化战棋可能过于重量级,A*是更轻量级的选择。
  3. 协程(Coroutine)管理: 大量使用yield return new WaitForSeconds的协程如果管理不善,会造成混乱。可以使用更现代的UniTask库(需安装)来获得更好的可读性和性能,或者自己封装一个简单的协程管理器来统一启动和停止。
  4. 事件系统解耦: 当前模块间通信可能直接调用方法或使用SendMessage。可以引入一个全局事件中心(Event Center),各模块通过监听和触发字符串或枚举类型的事件来通信,彻底解耦。例如,Unit死亡时触发"UnitDied"事件,AchievementManager和SoundManager分别监听这个事件来解锁成就和播放死亡音效,而Unit类完全不知道它们的存在。

5.2 功能扩展思路

这个Demo是一个完美的起点,你可以在此基础上添加无数功能:

  • 地形与高度差: 在mapGrid中存储地形类型和高度。移动力消耗、命中率、伤害加成都可以与地形挂钩。攻击时,检查攻击者与目标之间是否有高度阻挡。
  • 技能范围与效果多样化: 扩展Skill系统,支持扇形范围、直线范围、十字范围等。增加Buff/Debuff系统,技能可以附加“中毒”、“眩晕”、“攻击提升”等状态效果,这些效果持续若干回合。
  • 角色成长与装备系统: 为Unit添加Experience和Level属性,击败敌人获得经验升级。设计一个Inventory和Equipment系统,不同的武器、防具会影响单位属性。
  • 更复杂的AI: 实现行为树(Behavior Tree)或有限状态机(FSM)来管理AI决策。AI可以有不同的性格(激进型、保守型、治疗型),根据战场形势(己方血量、敌方阵型)做出更智能的选择。
  • 网络对战(PvP): 这是最具挑战性的扩展。需要将所有的游戏状态(单位位置、血量、行动指令)进行序列化,并通过网络同步。可以使用Mirror、Photon PUN或Fish-Networking等网络库。核心原则是:确定性逻辑,确保所有客户端在相同输入下计算出完全相同的结果,通常需要固定步长的逻辑帧更新。

6. 常见问题排查与调试技巧

在复现或修改这个项目的过程中,你肯定会遇到一些问题。这里记录几个我踩过的坑和解决方法。

6.1 单位移动或攻击后,游戏状态卡住

  • 问题现象: 单位行动完毕,但游戏没有切换到下一个单位或下一个回合。
  • 排查思路:
    1. 检查事件触发: 在Unit的EndTurn()方法里打日志或设断点,看是否被正确调用。
    2. 检查GameManager状态机: 确认OnUnitTurnEnd事件是否被GameManager监听到。确保事件没有被重复触发或遗漏。
    3. 检查协程: 如果移动或攻击是协程,确保协程正常执行完毕,没有因为异常而中途停止。在协程开始和结束处打日志。
  • 解决方案: 最常见的原因是动画事件没有正确触发,或者动画状态机参数设置错误,导致UnitActionState没有从Attacking切换回Idle。仔细检查Animator Controller中的状态过渡条件和动画事件帧。

6.2 点击没有反应或点击位置不准

  • 问题现象: 鼠标点击单位或网格没有响应,或者响应的是错误的对象。
  • 排查思路:
    1. 射线检测(Raycast): 确保被点击的GameObject有Collider组件(Box Collider或Mesh Collider)。检查Camera的Tag是否为MainCamera。
    2. 图层(Layer): 为可交互的物体(如Unit、GridCell)设置单独的Layer(如“Unit”,“Ground”)。在Physics.Raycast方法中指定图层掩码,避免检测到UI或其他无关物体。Raycast(ray, out hit, Mathf.Infinity, LayerMask.GetMask("Unit", "Ground"))。
    3. 屏幕坐标转换: 如果使用了UI和世界空间混合,注意区分Screen Point To Ray(用于3D物体)和Graphic Raycaster(用于UI)。
  • 解决方案: 使用Unity的Debug模式,在Scene视图中显示射线,可以直观看到射线是否击中了目标Collider。

6.3 伤害计算或技能效果不符合预期

  • 问题现象: 打出的伤害数值不对,或者技能没有产生效果。
  • 排查思路:
    1. 公式检查: 在TakeDamage或Skill.Execute方法中打印出每一步的计算中间值,如基础攻击力、防御减免、最终伤害。
    2. 数据引用: 检查Inspector面板中,单位的攻击力、防御力数值是否被正确赋值。检查Skill ScriptableObject的资源引用是否正确。
    3. 目标确认: 确保技能执行时,target参数不是null,并且是正确的目标单位。
  • 解决方案: 在关键计算函数开头加入Debug.Log($"计算伤害:攻击力{attacker.attackPower}, 防御力{target.defense}, 最终伤害{finalDamage}");,这是最直接的调试手段。

6.4 在Git导入或版本升级后出现编译错误

  • 问题现象: 从GitCode等平台下载源码后,Unity报错,主要是命名空间找不到或脚本依赖丢失。
  • 排查思路:
    1. 检查Unity版本: 项目可能是在较新或较旧的Unity版本中创建的。用文本编辑器打开项目根目录的ProjectSettings/ProjectVersion.txt,查看原项目版本。尽量使用相同或相近的版本打开。
    2. 重新导入Asset: 在Unity Editor中,尝试Assets -> Reimport All。
    3. 检查插件依赖: 项目可能使用了第三方插件,如DOTween(用于动画)、TextMeshPro(用于UI文字)。如果缺失,需要从Unity Asset Store或Package Manager中自行安装。
    4. 脚本编译顺序: 如果错误是关于某个类未定义,可能是脚本编译顺序问题。确保基础类(如Unit,Skill)放在标准Assets文件夹下,它们会比Editor文件夹或其他特殊文件夹下的脚本先编译。
  • 解决方案: 优先根据错误信息去安装对应的Unity Package或Asset。如果问题依旧,可以尝试新建一个空的Unity项目,再将源码中的Assets、Packages、ProjectSettings文件夹覆盖过去。

这个项目源码的价值在于它展示了一个完整、可运行的游戏循环。我建议你不要只满足于让它跑起来,而是尝试去修改它:增加一个新的“冲锋”技能,让单位可以移动后攻击更远的敌人;或者修改AI,让敌人会优先集火血量最低的我方单位。在这个过程中,你会对Unity游戏开发有更深刻、更实际的理解。游戏开发是门实践的手艺,动手改,比光看十遍代码都管用。

相关新闻

  • Matlab图像纹理分析工具集:GLCM/LBP特征提取+分类接口代码
  • 西宁城东区大众街街道亨得利官方钟表服务中心电话公示(2026年7月最新) - 亨得利钟表维修中心
  • TB6593FNG与TM4C129ENCPDT直流电机驱动方案详解

最新新闻

  • 批处理脚本进阶:for循环与变量扩展的10个高级用法解析
  • 2026 广州废铁不锈钢回收权威 TOP5 排行榜 - 星际AI
  • 珠海废旧中央空调回收上门收费吗?多久能到?统一回复 - 再生资源回收资讯
  • 车载大模型文本扰动防御:三层语义防护网设计
  • MTGuard 2.0 反爬系统逆向:从 15 项环境检测到 AES 加密密钥的完整链路
  • JDK环境配置全解析:从下载安装到JAVA_HOME精准设置

日新闻

  • SQL 查询语句的标准逻辑执行顺序(即语义处理顺序),它与实际书写顺序不同,但决定了数据库如何解析和执行查询
  • ORB-SLAM2 重定位模块深度解析:从 BoW 候选帧到 PnP 优化的 6 步流程
  • 罗技鼠标宏压枪脚本终极指南:从原理到实战的完整解析

周新闻

  • 基于YOLOv12的番茄成熟度智能检测系统开发
  • 终极RimWorld模组管理指南:用RimSort告别模组冲突烦恼
  • AI Agent框架开发:从理论到实践的完整指南

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号