Unity多技能Boss实现:数据驱动技能系统与AI调度实战

📅 发布时间:2026/10/14 22:42:38
Unity多技能Boss实现:数据驱动技能系统与AI调度实战
平时刷游戏视频的时候我经常被一类“整活”视频吸引一个本来只会平砍的 BOSS 单位突然拥有了关卡里所有 BOSS 的技能出场就是一套连招满屏幕都是特效。标题类似【TABS】当大哥有了全部Boss的技能看着确实爽但作为游戏开发方向的爱好者我更关心的是背后的实现问题一个单位身上挂着十几种技能AI 凭什么决定什么时候放哪个不同技能的冷却时间怎么独立管理动画、伤害、范围这些数据从哪里配置这篇文章就围绕“Boss 技能系统”这个主题展开用 Unity C# 实现一个简化但完整可运行的多技能 BossBoss 身上可以挂任意多个技能AI 会按规则自动挑选可用技能技能各自拥有独立的冷却、伤害、范围和动画表现。无论你是刚接触 Unity 的新手还是想给自己的游戏加一套技能框架的开发者本文的代码都可以直接参考。为了阅读方便先说明一个背景TABS 全称 Totally Accurate Battle Simulator是 Landfall 开发的一款物理模拟对战游戏。它的乐趣在于兵种搭配和碰撞效果。我们在文章里不会去分析 TABS 源码也没有必要而是把它作为需求灵感做一个属于自己的可扩展技能系统。1. 背景与核心概念1.1 什么是“Boss 拥有全部技能”在 TABS 这类游戏中普通单位一般只有简单的攻击行为例如挥动武器、射击或冲锋。而 Boss 单位往往拥有技能近战挥击、范围践踏、远程弹幕、召唤小兵等等。玩家如果通过自定义单位功能把一个 Boss 的所有技能都安装到同一个单位身上就会出现“走一步放一个技能”的效果。从这个需求出发游戏代码层需要支持技能的数量不固定可以动态增加或删除。每个技能有自己的冷却时间技能之间互不干扰。每个技能需要不同的触发距离近战技能贴近目标才放远程技能远距离就可以放。技能释放时要播放对应动画并产生实际的伤害效果。Boss 的 AI 要能“看懂”当前局面选择最合适的技能。不要把技能系统想得很玄乎。本质上它就是一个“数据配置 行为调度 效果执行”的三层结构。数据层描述技能长什么样调度层决定什么时候放执行层负责打出伤害、播放动画。1.2 技能系统的常见设计思路很多新手会这样写在 MonoBehaviour 里定义一堆公有方法然后写一个巨大的 switchswitch (skillName) { case FireBall: FireBall(); break; case GroundPound: GroundPound(); break; }这在技能只有两三个的时候没有问题但技能一旦达到十几个函数会越来越长新增技能还要修改旧脚本。更合理的做法是把“技能”本身当作一种可配置的数据资源再用一个通用控制器来统一调度。本文使用的核心思路是用 ScriptableObject 描述技能数据。用 List 保存当前 Boss 的所有技能。用独立的冷却数组保存每个技能的剩余冷却时间。用 AI 决策脚本按照规则选择技能。用一个技能释放器统一播放动画、执行伤害逻辑。这个结构下新增一个 Boss 技能只需要创建一个新的 SkillData 资产再把它拖进 Boss 的技能列表完全不需要修改脚本。1.3 技术选型文章示例使用 Unity 游戏引擎和 C# 语言原因有三个TABS 本身就是 Unity 开发的同引擎更容易理解它的机制。Unity 的 ScriptableObject 非常适合做技能配置。C# 的协程、委托和 List 等机制能优雅地处理多技能调度。如果你用的是 Godot、Unreal 或者其他引擎本文的“数据驱动 组件化调度”思想同样可以迁移过去。代码只是思想的载体。2. 环境准备与版本说明2.1 开发环境本文示例以 Unity 2021.3 LTS 或 2022.3 LTS 为参考版本这两个版本都长期维护适合学习与项目落地。如果你使用的是其他版本例如 Unity 6、Unity 2023代码依然可以运行只要支持 ScriptableObject、Physics.OverlapSphere 和协程即可。操作系统使用 Windows 10/11 或 macOS 都可以Unity 是跨平台编辑器本文的代码不依赖系统特性。C# 脚本用 Visual Studio 2022 或 JetBrains Rider 编写都可以。如果你刚安装 Unity内置的 Visual Studio Community 也完全够用。2.2 新建项目打开 Unity Hub新建一个 3D 项目项目名称可以叫BossSkillDemo模板选择 3D。场景里会自动生成一个 Main Camera 和一个 Directional Light。为了演示技能伤害我们需要一个简单的场地和一个目标单位。可以直接在场景里创建创建一个 Cube 作为地面名称改成GroundScale 调整成(10, 1, 10)。创建一个 Capsule 作为目标名称改成PlayerY 轴放在地面之上。创建一个 Capsule 作为 Boss名称改成BossY 轴同样抬到地面上方。Project 窗口里的目录结构建议先建好Assets/ Scripts/ Boss/ SkillData.cs BossSkillController.cs SimpleBossAI.cs Health.cs Samples/ Scenario/Samples不是必须的但把场景文件单独放会更整洁。3. 核心设计拆解3.1 技能数据模型ScriptableObjectScriptableObject 是 Unity 中一种用来存放数据的资源类型。它的好处是数据与逻辑分离同一个技能数据可以被多个单位引用修改一份配置就能影响所有使用该技能的单位。我们先定义技能数据。新建脚本SkillData.cs写入以下代码using UnityEngine; [CreateAssetMenu(menuName Boss/SkillData, fileName SkillData)] public class SkillData : ScriptableObject { [Header(基础信息)] public string skillName 普通技能; public string skillId skill_001; [Header(战斗数值)] public float damage 10f; public float cooldown 3f; public float range 2f; public float triggerDistance 5f; [Header(释放表现)] public SkillAnimationType animationType SkillAnimationType.MeleeSwing; public float castTime 0.2f; public float effectRadius 2f; } public enum SkillAnimationType { MeleeSwing, GroundPound, CastSpell }关键字段解释skillName、skillId用于日志和辨识技能Id 更稳定Name 可以随便改。damage技能造成的伤害伤害目标在效果执行阶段查找。cooldown技能释放后需要等待的秒数示例中独立统计。range技能实际作用半径影响特效和伤害范围。triggerDistanceAI 决定是否释放该技能的判定距离。远程技能可以设置较大值近战技能设置较小值。animationType对应 Animator 中的 Trigger 名称枚举转字符串后传给动画控制器。castTime从播放动画到真正产生伤害之间的延迟给玩家一点点反应时间表现。effectRadiusAOE 技能的伤害检测半径。代码里的[CreateAssetMenu]特性让 Unity 编辑器菜单里出现 “Assets - Create - Boss - SkillData” 选项这样就能在 Project 窗口直接创建技能资产。为什么把技能定义为“数据”而不是“脚本”因为改动数值、调整冷却、换动画名称都属于策划配置工作不应该让程序每次修改代码。数据驱动是游戏开发中非常基础也非常重要的思想。3.2 技能持有与冷却管理Boss 身上需要一个脚本负责保存技能列表、追踪冷却时间和执行技能。我们定义一个BossSkillController.cs。using System.Collections.Generic; using UnityEngine; public class BossSkillController : MonoBehaviour { [SerializeField] private ListSkillData skills new ListSkillData(); private Listfloat cooldownRemainings new Listfloat(); private Animator animator; private void Awake() { animator GetComponentAnimator(); cooldownRemainings new Listfloat(new float[skills.Count]); } private void Update() { for (int i 0; i cooldownRemainings.Count; i) { if (cooldownRemainings[i] 0f) { cooldownRemainings[i] - Time.deltaTime; } } } public bool CanUseSkill(int index) { if (index 0 || index skills.Count) { return false; } return cooldownRemainings[index] 0f; } public int SkillCount skills.Count; public SkillData GetSkill(int index) { return skills[index]; } public Listint GetAvailableSkillIndexes() { Listint result new Listint(); for (int i 0; i skills.Count; i) { if (cooldownRemainings[i] 0f) { result.Add(i); } } return result; } public void TryUseSkill(int index, Transform target) { if (!CanUseSkill(index) || target null) { return; } SkillData skill skills[index]; float distance Vector3.Distance(transform.position, target.position); if (distance skill.triggerDistance) { return; } cooldownRemainings[index] skill.cooldown; StartCoroutine(CastSkillRoutine(skill, target)); } private System.Collections.IEnumerator CastSkillRoutine(SkillData skill, Transform target) { if (animator ! null) { animator.SetTrigger(skill.animationType.ToString()); } Vector3 faceDirection target.position - transform.position; faceDirection.y 0f; if (faceDirection.sqrMagnitude 0.001f) { transform.rotation Quaternion.LookRotation(faceDirection.normalized); } Debug.Log($[Boss] 开始释放技能{skill.skillName}); yield return new WaitForSeconds(skill.castTime); ExecuteSkillEffect(skill, target); } private void ExecuteSkillEffect(SkillData skill, Transform target) { switch (skill.animationType) { case SkillAnimationType.MeleeSwing: MeleeSwing(skill, target); break; case SkillAnimationType.GroundPound: GroundPound(skill); break; case SkillAnimationType.CastSpell: CastSpell(skill, target); break; } } private void MeleeSwing(SkillData skill, Transform target) { if (Vector3.Distance(transform.position, target.position) skill.range) { ApplyDamage(target.gameObject, skill.damage); } Debug.Log($[Boss] 近战挥击命中伤害 {skill.damage}); } private void GroundPound(SkillData skill) { Collider[] colliders Physics.OverlapSphere(transform.position, skill.effectRadius); foreach (Collider col in colliders) { if (col.gameObject gameObject) { continue; } IDamageable damageable col.GetComponentIDamageable(); if (damageable ! null) { damageable.TakeDamage(skill.damage); Debug.Log($[Boss] 践踏命中 {col.gameObject.name}伤害 {skill.damage}); } } } private void CastSpell(SkillData skill, Transform target) { if (target ! null) { ApplyDamage(target.gameObject, skill.damage); Debug.Log($[Boss] 远程法术命中 {target.gameObject.name}伤害 {skill.damage}); } } private void ApplyDamage(GameObject target, float damage) { IDamageable damageable target.GetComponentIDamageable(); if (damageable ! null) { damageable.TakeDamage(damage); } } private void OnDrawGizmosSelected() { Gizmos.color Color.red; foreach (SkillData skill in skills) { if (skill ! null) { Gizmos.DrawWireSphere(transform.position, skill.range); } } } }这个脚本的核心逻辑有三个第一冷却管理。cooldownRemainings与skills数组一一对应。每次 Update 把所有冷却时间统一减去Time.deltaTime这样技能之间互不干扰。第二技能释放前校验。TryUseSkill会依次检查冷却是否完成、目标是否为空、目标距离是否在技能的triggerDistance范围内。只要满足条件就先设置冷却、播放动画、旋转朝向再等待castTime秒产生实际效果。第三技能效果。ExecuteSkillEffect根据SkillAnimationType走不同的分支。这里的 switch 结构虽然不算最优雅但对于教学来说非常直观——读者能一眼看明白技能效果在哪里写。这里要特别说明GroundPound用Physics.OverlapSphere做范围检测它会找到以 Boss 为中心、半径effectRadius内所有带 Collider 的对象。为了防止攻击自己代码里跳过自身。实战项目中建议用 Layer Mask 或者标签过滤避免误伤友军单位。这个点留到后面“常见问题”部分再详细讲。另外代码里使用了IDamageable接口。我们在实战案例里会给目标单位挂一个Health组件并实现这个接口。这样可以避免直接把伤害逻辑写死在 Boss 技能脚本里后续任何单位想接收伤害只需要实现接口即可。3.3 AI 决策什么时候放哪个技能Boss 有了技能列表也需要“大脑”来指挥。最简单的 AI 策略可以拆成两部分先确定目标是否在技能范围内。再从可用技能里选一个释放。新手的常见错误是每个技能独立判断、写成一长串 if-else。例如if (cooldown1 0 distance 5) UseSkill1(); if (cooldown2 0 distance 10) UseSkill2();这样会出现多个技能同时触发看起来不自然。更合理的做法是“每轮决策只释放一个技能”。我们在SimpleBossAI.cs中实现这个策略using UnityEngine; public class SimpleBossAI : MonoBehaviour { [SerializeField] private Transform target; [SerializeField] private float detectRange 8f; [SerializeField] private float decisionInterval 0.8f; private BossSkillController skillController; private float nextDecisionTime; private void Awake() { skillController GetComponentBossSkillController(); } private void Update() { if (target null) { target FindTarget(); return; } float distance Vector3.Distance(transform.position, target.position); if (distance detectRange) { return; } if (Time.time nextDecisionTime) { return; } nextDecisionTime Time.time decisionInterval; TryCastSkill(); } private void TryCastSkill() { if (skillController null) { return; } int candidateIndex SelectSkillIndex(); if (candidateIndex 0) { return; } skillController.TryUseSkill(candidateIndex, target); } private int SelectSkillIndex() { int[] availableIndexes skillController.GetAvailableSkillIndexes().ToArray(); if (availableIndexes.Length 0) { return -1; } int chosen Random.Range(0, availableIndexes.Length); return availableIndexes[chosen]; } private Transform FindTarget() { GameObject[] allObjects GameObject.FindGameObjectsWithTag(Player); if (allObjects.Length 0) { return allObjects[0].transform; } return null; } }AI 决策每隔decisionInterval秒执行一次避免每帧随机导致技能释放太密集。SelectSkillIndex从BossSkillController拿到所有未冷却的技能随机选一个。这个策略很简单但已经足够体现“技能调度”的核心流程。如果你想做更“聪明”的 Boss可以在SelectSkillIndex里增加权重、距离判断和连招逻辑。例如目标在 3 米内优先使用近战目标在 8 米外优先使用远程法术某个技能释放后接着强制释放另一个技能形成连招甚至根据 Boss 血量切换技能池。这些都属于 AI 决策层的内容与技能数据层解耦后扩展起来非常容易。记得给目标单位添加Player标签否则FindTarget找不到目标。选中场景里的 Capsule在 Inspector 右上角 Tag 下拉框里选择 Player如果没有该标签点击 Add Tag 创建。3.4 动画与技能衔接技能释放的观赏性很大程度来自动画。Animator 的 Trigger 类型可以做到“一次性触发动画”适合攻击技能。我们在SkillData里定义了animationType枚举枚举值转换成字符串后直接作为 Trigger 名传给 Animator。假设你给 Boss 挂了一个 Animator Controller里面至少需要三个 Trigger 参数MeleeSwingGroundPoundCastSpell每个 Trigger 连接到对应的动画动画结束后回到 Idle。在CastSkillRoutine中执行的是animator.SetTrigger(skill.animationType.ToString());这样做的意义是技能脚本永远不需要关心“这个技能对应哪个动画”只需要根据配置告诉 Animator 参数名。动画资源后续怎么换都不会影响代码逻辑。如果你的项目暂时没有动画资源可以让 AnimationType 为空字符串代码里判断if (animator ! null !string.IsNullOrEmpty(skill.animationType.ToString())) { animator.SetTrigger(skill.animationType.ToString()); }本文示例为了保持代码简洁默认 Boss 会挂 Animator。实际运行时如果没有 Animator 组件代码会跳过动画播放逻辑但技能效果依然执行。4. 完整实战案例接下来我们把整套系统串起来运行一个真实场景。建议按下面步骤操作最终你可以看到 Boss 自动走近目标然后周期性释放随机技能目标单位血量不断下降。4.1 创建技能数据资产在 Project 窗口右键选择Create - Boss - SkillData创建三个技能资产MeleeSkill近战挥击伤害 20冷却 2 秒triggerDistance 设 3range 设 2.5animationType 选 MeleeSwing。PoundSkill地面践踏伤害 35冷却 5 秒triggerDistance 设 4effectRadius 设 4animationType 选 GroundPound。SpellSkill远程法术伤害 15冷却 1.5 秒triggerDistance 设 8range 设 1animationType 选 CastSpell。每个资产生成后在 Inspector 中修改对应的字段即可。技能数量不需要一次做很多三个足够演示“全部技能集合”的效果。读者可以随时右键创建更多资产。4.2 添加伤害接口与健康组件为了能让目标接收伤害我们新建IDamageable.cspublic interface IDamageable { void TakeDamage(float amount); }接口一般不继承 MonoBehaviour放在Scripts/Boss目录下即可。然后新建Health.csusing UnityEngine; public class Health : MonoBehaviour, IDamageable { [SerializeField] private float maxHealth 100f; private float currentHealth; private void Awake() { currentHealth maxHealth; } public void TakeDamage(float amount) { currentHealth - amount; Debug.Log(${gameObject.name} 受到 {amount} 点伤害剩余血量 {currentHealth}); if (currentHealth 0f) { Die(); } } private void Die() { Debug.Log(${gameObject.name} 被击败了); Destroy(gameObject); } }把Health组件挂到场景里的 CapsulePlayer上。Boss 也可以挂但本文为了观察方便没有让 Boss 参与受伤测试。4.3 配置 Boss 对象选中场景中的Boss对象按下面配置添加Animator组件。如果你暂时没有动画控制器可以留空但建议创建一个简单的 Animator Controller 并在其上添加三个 Trigger 参数。不会建也没关系技能依然会正常造成伤害。添加BossSkillController组件。在BossSkillController的 Skills 列表里把刚才创建的三个 SkillData 资产拖进去顺序随意。添加SimpleBossAI组件。给Boss对象添加 Rigidbody并勾选 Is Kinematic避免物理系统把单位推来推去。如果你需要对碰撞做更精细控制可以不用 Rigidbody但Physics.OverlapSphere一般仍需要 Collider 才能检测到目标。最终 Boss 的 Inspector 结构大致是Boss (GameObject) Capsule Collider Animator BossSkillController (Script) SimpleBossAI (Script)目标 Player 的结构Player (GameObject) Capsule Collider Health (Script) Tag: Player不需要写任何代码来把 Player 赋值给SimpleBossAI.target因为 AI 脚本会在运行时按Player标签查找目标。如果你想手动指定也可以把 target 字段拖到 Inspector 里。4.4 运行与验证点击 Unity 编辑器的 Play 按钮Console 窗口应该能看到类似下面的输出[Boss] 开始释放技能远程法术 Player 受到 15 点伤害剩余血量 85 [Boss] 远程法术命中 Player伤害 15 [Boss] 开始释放技能地面践踏 Player 受到 35 点伤害剩余血量 50 [Boss] 践踏命中 Player伤害 35由于 AI 每 0.8 秒决策一次冷却时间各不相同释放顺序是随机的。你可以通过修改决策间隔和技能冷却来观察 AI 行为变化把decisionInterval调成 0.2 会让 Boss 释放节奏更快。把所有技能冷却改成 0 会让 Boss 几乎不停释放技能更容易压力测试。把detectRange调小Boss 会“发呆”更久直到目标走近才动手。4.5 结果说明这个案例还原了视频标题里最吸引人的部分一个单位挂载多个 Boss 技能且全部可以自动释放。代码层面的核心成果有三个技能数据与逻辑分离新增技能不需要改脚本。每个技能的冷却独立管理解决“多个技能共享冷却导致卡手”的问题。AI 决策与技能执行分层后续可以轻松替换更复杂的 AI 策略。如果说视频里我们看到的是“表现层效果”那么这套代码就是“逻辑层实现”。有了这样的基础框架后续接对象池、特效、行为树都只是时间问题。5. 常见问题与排查思路多技能系统的报错和异常往往不在语法层面而是设计层面。下面整理几个最容易踩的坑。问题现象常见原因解决思路Boss 走近目标但一直不放技能目标没有Player标签或者detectRange小于目标距离检查 Tag 设置调大检测范围多个技能同时释放AI 决策没限制“每轮只放一个”使用decisionInterval限制决策频率并在TryCastSkill中只提交一个技能技能冷却好像无效冷却更新逻辑放在 Update 中但使用了错误的 deltaTime 累加方式用Time.deltaTime逐个减去冷却数组不要用整型计数器范围技能误伤 Boss 自己Physics.OverlapSphere检测到了自身 Collider在逻辑里判断col.gameObject gameObject更推荐用 Layer Mask动画不播放但伤害有效Animator 为空或 Trigger 名与动画控制器参数不一致检查动画控制器里是否有同名 Trigger注意大小写远程技能近距离释放时看起来奇怪triggerDistance设置过大AI 逻辑没考虑最小距离给 SkillData 增加minRange字段AI 选择技能时做区间判断技能列表在运行后添加了新技能但冷却数组长度不对Awake 阶段初始化了旧长度的冷却数组不建议运行时改列表如果必须支持在列表变化后重新初始化冷却数组5.1 关于误伤和 Layer MaskPhysics.OverlapSphere是范围检测凡是挂在碰撞器上的对象都可能被命中。项目里如果存在友军单位Boss 可能会一脚踩死队友。更稳妥的做法是给目标单位单独分配一个 Layer例如Enemy然后代码里使用int layerMask LayerMask.GetMask(Enemy); Collider[] colliders Physics.OverlapSphere(transform.position, skill.effectRadius, layerMask);按 Layer 过滤比每次都GetComponentIDamageable()高效也更安全。为了控制代码长度上面实战示例没有加 Layer 判断但正式项目建议从第一天就做好 Layer 规划。5.2 关于标签查找的性能GameObject.FindGameObjectsWithTag(Player)每帧查找目标并不是好方案因为每次查找都会遍历场景对象。示例代码里只在target null时调用所以性能压力不大。正式项目推荐两种方案在目标生成时通过事件或单例把自己注册给 Boss。在 Boss 周围持续做一次范围内的碰撞检测把最近的目标作为攻击对象。如果你做的是类塔防游戏单位数量非常多那么尽量避免全局标签查找改用物理检测或者单位注册表。6. 最佳实践与工程建议6.1 把技能冷却与 Animator 参数配置放在数据层本文的 SkillData 已经实现了“数据驱动”但这个方向还可以继续深入。几个字段值得补充minRange技能最短释放距离。animationName如果不喜欢枚举转字符串可以直接配置一个 string更加灵活。aiWeightAI 随机选择时按权重分配。iconUI 技能栏图标。soundEffect技能音效。每次新增字段只需要在 SkillData 资产里配置不需要改动 AI 和播放逻辑。这就是数据驱动带来的维护价值。6.2 在 BossSkillController 之上抽象“技能效果”示例里的 switch 结构能运行但不利于大型项目。如果技能数量达到几十个推荐使用策略模式定义抽象基类SkillBase每个技能独立继承并实现释放逻辑SkillData 只保存公共数据技能资产通过引用一个SkillBehaviour配置来绑定效果。这种方式的好处是新增技能无需修改控制器代码只要新建一个技能行为类再拖给资产即可。缺点是项目结构变复杂新手容易绕晕。本文选择 switch 版本是为了让核心流程更容易理解读者可以按自己的项目规模决定是否升级。6.3 使用对象池管理技能特效与伤害判定技能释放频繁后实例化和销毁对象会产生性能波动。比如火球术每 1 秒放一次每次都Instantiate一个火球预制体后期单位一多就会卡顿。建议技能特效、子弹、伤害数字统一走对象池。Unity 中可以直接使用内置的UnityEngine.Pool.ObjectPoolT也可以自己实现一个简单的队列对象池。核心点只有一个对象创建后不销毁而是“回收”下次使用前重置状态。本文代码没有实例化任何预制体所以暂时不存在该问题。但当你把技能系统接入正式战斗时对象池通常是绕不开的一环。6.4 动画事件与伤害帧的配合技能释放的时机会影响手感。示例使用的是WaitForSeconds(skill.castTime)来延迟伤害判定用起来简单。另一种方案是在动画某一帧发送动画事件调用伤害逻辑。这种方法可以让伤害点和动画表现完美对齐例如“刀刚挥到一半时就产生判定”。动画事件的缺点是配置成本高且如果动画被中途打断事件可能不触发。比较灵活的做法是在控制器里写一个可配置的damageTriggerFrame用时间线判断角色动画播放进度再决定是否执行伤害。这对动作游戏非常重要但对回合制或塔防类游戏则意义不大应视玩法决定。6.5 异常处理与日志规范技能系统运行时最常见的异常是空引用目标被杀死后 AI 还持有它的 Transform。示例代码里TryUseSkill会检查 target 是否为空但SimpleBossAI在目标死亡后仍然会继续获取target.position需要自己处理目标失效问题。建议在 Boss 上维护一个目标生命周期回调例如void Update() { if (target null || target.gameObject null) { target FindTarget(); return; } }同时日志要分级技能释放用普通 Log数据配置错误用 Warning例如当triggerDistance小于range时提醒开发者检查配置。6.6 如果用网络同步需要注意什么如果这个 Boss 出现在多人联机游戏中技能系统必须考虑同步问题。技能选择、冷却开始时间、伤害广播都需要放在服务器端计算。客户端只负责表现播放动画、生成特效、显示伤害数字。示例代码的冷却计算基于本地Time.time在多端会比产生不一致。正式联机项目建议使用服务器时间戳或者用容错时钟系统。这个内容已经超出本文范围但提前了解有助于后续扩展。7. 总结与学习路线本文从一个娱乐化视频标题切入实现了一个可运行的 Boss 多技能系统。重点内容包括用 ScriptableObject 定义技能数据使技能配置和逻辑代码解耦。用冷却数组独立管理多个技能的冷却解决技能之间互相影响的问题。用 BossSkillController 统一下发技能动画、旋转、伤害判定都在一个流程里完成。用 SimpleBossAI 每隔一段时间决策一次从可用技能池中随机选择一个技能释放。用 IDamageable Health 组件实现通用伤害接口后续单位只要实现接口即可被攻击。如果你接下来想继续深入可以按以下路线扩展先给 AI 加上权重让远程技能的触发概率高于近战技能观察不同手感。再实现一个简单的动画状态机把三个技能动画和Idle状态串起来。然后尝试给技能加特效预制体并用对象池管理生命周期。进一步学习行为树Behavior Tree让 Boss 根据血量切换技能池或进入狂暴状态。最后可以考虑技能编辑器在 Inspector 里拖拽配置技能甚至用节点图把多个技能串成连招。每一层都是独立课题但底层框架就是我们今天搭建的这套“数据 AI 执行器”。建议你打开 Unity 亲手把项目跑一遍改几个数值试一下技能节奏。只有真正动手才会发现“全部 Boss 技能”这个玩法背后到底有哪些设计取舍。