ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

游戏技能系统设计:基于状态机与时间轴实现多段技能框架

游戏技能系统设计:基于状态机与时间轴实现多段技能框架

在实际游戏开发或技能设计项目中,我们经常需要为一个复杂的技能或动作设计一套连贯的、有节奏感的执行逻辑。一个典型的例子就是“后羿射日”这类多段式技能,它要求从起手、蓄力、释放到收尾,每个阶段都有明确的视觉表现、时间控制和伤害判定。如果代码写得松散,就容易出现动作卡顿、伤害计算时机错乱、特效与逻辑不同步等问题,导致玩家体验极差。

本文将以“后羿射日”这个四段式技能为案例,拆解如何实现一套“秒级”响应的技能系统。这里的“秒级”并非指技能持续一秒,而是强调代码的执行效率和时序控制的精确性,确保从“眼神锁定”(瞄准)到“箭矢爆炸”(伤害生效)的整个过程,逻辑清晰、性能可控、且易于调试和扩展。我们将从技能状态机设计、时间轴管理、事件驱动和伤害结算四个核心层面,构建一个可用于实战的轻量级技能框架。无论你是在开发MOBA、ARPG还是其他需要复杂技能逻辑的游戏,这套思路都能提供直接的参考。

1. 理解多段技能的核心:状态与时序

在动手写代码之前,必须厘清多段技能的本质。它不是一个简单的函数调用,而是一个随时间推进、拥有多个明确阶段的状态机。每个阶段(或称为“段”)都有其专属的生命周期:开始、持续、结束,并可能触发不同的事件。

1.1 技能阶段拆解(以后羿射日为例)

以“后羿射日”为例,我们可以将其抽象为四个阶段:

  1. 起手/瞄准阶段 (Windup):角色做出瞄准动作,播放动画,可能有一个准星特效。此阶段通常可被移动或受击打断。
  2. 蓄力/拉弓阶段 (Channeling):角色持续拉弓,能量或特效不断聚集。此阶段可能持续数秒,期间玩家可以调整方向,但移动会中断。
  3. 释放/出箭阶段 (Casting):箭矢射出,飞行物被创建并朝目标飞行。这是伤害判定的核心准备阶段。
  4. 收尾/爆炸阶段 (Follow-through):箭矢命中目标或到达终点,触发爆炸特效、范围伤害和击退等效果。技能效果真正生效。

这四段必须严格按顺序执行,且每段都有其特定的持续时间、可中断性和触发事件。

1.2 为什么需要“状态机”和“时间轴”

如果只用一堆布尔标志和if-else来控制,代码会迅速变得难以维护:

// 反面示例:难以维护的硬编码控制 bool isAiming = false; bool isCharging = false; bool isFlying = false; float timer = 0f; void Update() { if (isAiming) { timer += Time.deltaTime; if (timer > 0.5f) { isAiming = false; isCharging = true; timer = 0; } } else if (isCharging) { // ... 更多混乱的 if-else } }

问题:状态切换逻辑散落各处,难以增加新阶段或修改时序,且Update函数会越来越臃肿。

解决方案是引入一个轻量级状态机来管理阶段,并配合一个时间轴(Timeline)或计时器系统来驱动状态转换和事件触发。这样,技能的逻辑就变成了对“状态”和“时间点”的定义与响应。

2. 环境准备与项目结构

我们将使用 C# 和 Unity 引擎作为演示环境,因为其组件化和帧更新机制非常适合展示游戏逻辑。但核心设计模式(状态机、事件、时间轴)是语言和引擎无关的,你可以轻松移植到 Cocos、Unreal 或自研引擎中。

2.1 基础环境要求

  • Unity 版本: 2020.3 LTS 或更高版本(确保稳定的 MonoBehaviour 生命周期)。
  • 脚本运行时版本: .NET 4.x 或更高,以使用更丰富的 API。
  • 核心概念理解: 了解 Unity 的GameObjectComponentUpdate循环、协程 (Coroutine) 和事件 (Action/delegate) 的基本用法。

2.2 创建技能系统的核心目录结构

在 Unity 项目中的Assets/Scripts下创建如下结构:

Assets/ └── Scripts/ ├── Skills/ │ ├── Interfaces/ # 接口定义 │ ├── Core/ # 核心状态机、计时器 │ ├── Components/ # 挂载在GameObject上的组件 │ └── HouYi/ # 后羿射日技能具体实现 └── Demo/ # 测试场景和控制器

这个结构确保了核心框架与具体技能实现的分离。

2.3 定义核心接口:ISkillPhase

首先,为技能的每一个“阶段”定义一个契约。在Interfaces/ISkillPhase.cs中:

// Assets/Scripts/Skills/Interfaces/ISkillPhase.cs using UnityEngine; namespace Skills.Interfaces { /// <summary> /// 技能阶段接口。每一个具体的阶段(如瞄准、蓄力)都应实现此接口。 /// </summary> public interface ISkillPhase { /// <summary> /// 阶段名称,用于调试和日志。 /// </summary> string PhaseName { get; } /// <summary> /// 进入该阶段时调用。 /// </summary> /// <param name="context">技能上下文,包含施法者、目标等信息。</param> void OnEnter(SkillContext context); /// <summary> /// 每帧更新时调用(如果该阶段需要持续帧更新逻辑)。 /// </summary> /// <param name="deltaTime">帧间隔时间。</param> void OnUpdate(float deltaTime); /// <summary> /// 离开该阶段时调用。 /// </summary> void OnExit(); /// <summary> /// 检查该阶段是否已经完成(例如计时结束、条件满足)。 /// </summary> /// <returns>true 表示阶段完成,可以切换到下一阶段。</returns> bool IsFinished(); } }

这个接口强制每个阶段都拥有标准的生命周期方法,这是实现状态机切换的基础。

3. 构建技能状态机与时间轴驱动器

有了阶段定义,我们需要一个管理器来按顺序执行这些阶段,并控制切换时机。

3.1 实现技能状态机 (SkillStateMachine)

Core/SkillStateMachine.cs中:

// Assets/Scripts/Skills/Core/SkillStateMachine.cs using System.Collections.Generic; using Skills.Interfaces; using UnityEngine; namespace Skills.Core { /// <summary> /// 技能状态机。负责按顺序管理和执行技能阶段。 /// </summary> public class SkillStateMachine { private List<ISkillPhase> _phases; private int _currentPhaseIndex = -1; private ISkillPhase _currentPhase => (_currentPhaseIndex >= 0 && _currentPhaseIndex < _phases.Count) ? _phases[_currentPhaseIndex] : null; private SkillContext _context; public SkillStateMachine(SkillContext context) { _phases = new List<ISkillPhase>(); _context = context; } /// <summary> /// 添加一个技能阶段到执行序列。 /// </summary> public void AddPhase(ISkillPhase phase) { _phases.Add(phase); } /// <summary> /// 启动状态机,从第一个阶段开始。 /// </summary> public void Start() { if (_phases.Count == 0) { Debug.LogWarning("SkillStateMachine has no phases to execute."); return; } _currentPhaseIndex = 0; _currentPhase?.OnEnter(_context); } /// <summary> /// 每帧更新,驱动当前阶段逻辑并检查是否切换。 /// </summary> public void Update(float deltaTime) { if (_currentPhase == null) return; // 更新当前阶段 _currentPhase.OnUpdate(deltaTime); // 检查当前阶段是否完成 if (_currentPhase.IsFinished()) { // 退出当前阶段 _currentPhase.OnExit(); // 切换到下一个阶段 _currentPhaseIndex++; if (_currentPhaseIndex < _phases.Count) { _currentPhase.OnEnter(_context); } else { // 所有阶段执行完毕 Debug.Log("Skill execution completed."); // 这里可以触发技能完成事件 } } } /// <summary> /// 强制中断技能。 /// </summary> public void Interrupt() { _currentPhase?.OnExit(); _currentPhaseIndex = _phases.Count; // 跳到末尾,结束状态机 Debug.Log("Skill interrupted."); } } }

这个状态机维护了一个阶段列表,并在Update中驱动当前阶段。当IsFinished()返回true时,自动切换到下一阶段。

3.2 定义技能上下文 (SkillContext)

上下文对象用于在各个阶段间传递共享数据。在Core/SkillContext.cs中:

// Assets/Scripts/Skills/Core/SkillContext.cs using UnityEngine; namespace Skills.Core { /// <summary> /// 技能执行上下文,封装了技能释放所需的所有环境数据。 /// </summary> public class SkillContext { public GameObject Caster { get; set; } // 施法者 public Vector3 TargetPosition { get; set; } // 目标位置(地面点) public GameObject TargetUnit { get; set; } // 目标单位(可选) public Transform CastOrigin { get; set; } // 技能释放原点(如手部) // 可以扩展其他数据,如技能等级、威力加成等 public float PowerMultiplier { get; set; } = 1.0f; } }

3.3 创建基于时间的阶段基类 (TimedPhase)

很多技能阶段都是基于时间的(例如蓄力2秒)。我们创建一个通用的可计时阶段基类。在Core/Phases/TimedPhase.cs中:

// Assets/Scripts/Skills/Core/Phases/TimedPhase.cs using Skills.Interfaces; using UnityEngine; namespace Skills.Core.Phases { /// <summary> /// 一个具有固定持续时间的技能阶段基类。 /// </summary> public abstract class TimedPhase : ISkillPhase { public abstract string PhaseName { get; } protected SkillContext Context { get; private set; } protected float Duration { get; set; } protected float ElapsedTime { get; private set; } protected TimedPhase(float duration) { Duration = duration; } public virtual void OnEnter(SkillContext context) { Context = context; ElapsedTime = 0f; Debug.Log($"Entering phase: {PhaseName}, Duration: {Duration}s"); // 子类可以在这里播放动画、生成特效等 } public virtual void OnUpdate(float deltaTime) { ElapsedTime += deltaTime; // 子类可以在这里更新特效、检查输入等 } public virtual void OnExit() { Debug.Log($"Exiting phase: {PhaseName}"); // 子类可以在这里清理资源 } public virtual bool IsFinished() { // 默认根据时间判断是否完成 return ElapsedTime >= Duration; } } }

这个基类处理了通用的计时逻辑,具体阶段只需继承它并重写OnEnter,OnUpdate来添加特定行为。

4. 实现“后羿射日”四段技能

现在,我们用上面构建的框架来实现具体的四个阶段。

4.1 第一阶段:瞄准 (AimPhase)

HouYi/Phases/AimPhase.cs中:

// Assets/Scripts/Skills/HouYi/Phases/AimPhase.cs using Skills.Core.Phases; using UnityEngine; namespace Skills.HouYi.Phases { public class AimPhase : TimedPhase { public override string PhaseName => "Aim"; private GameObject _aimingReticle; // 瞄准准星特效 public AimPhase(float duration) : base(duration) { } public override void OnEnter(SkillContext context) { base.OnEnter(context); // 1. 播放瞄准动画 var animator = context.Caster.GetComponent<Animator>(); animator?.SetTrigger("Aim"); // 2. 在目标位置生成一个准星特效(假设有预制体) // _aimingReticle = GameObject.Instantiate(aimReticlePrefab, context.TargetPosition, Quaternion.identity); Debug.Log($"Aiming at position: {context.TargetPosition}"); } public override void OnUpdate(float deltaTime) { base.OnUpdate(deltaTime); // 可以在这里让准星跟随鼠标或摇杆微调目标位置 // if (Input.GetMouseButton(0)) { ... } } public override void OnExit() { base.OnExit(); // 销毁准星特效 // if (_aimingReticle != null) GameObject.Destroy(_aimingReticle); } public override bool IsFinished() { // 除了时间到,也可以被外部事件中断(如移动) return base.IsFinished() || IsInterruptedByMovement(); } private bool IsInterruptedByMovement() { // 简单示例:如果施法者移动了,则中断瞄准 // 实际项目中需要更精细的判断,如速度阈值 return false; // 此处简化 } } }

4.2 第二阶段:蓄力 (ChargePhase)

HouYi/Phases/ChargePhase.cs中:

// Assets/Scripts/Skills/HouYi/Phases/ChargePhase.cs using Skills.Core.Phases; using UnityEngine; namespace Skills.HouYi.Phases { public class ChargePhase : TimedPhase { public override string PhaseName => "Charge"; private ParticleSystem _chargeEffect; // 蓄力特效 private float _chargePower; // 蓄力强度,可能随时间增长 public ChargePhase(float duration) : base(duration) { } public override void OnEnter(SkillContext context) { base.OnEnter(context); _chargePower = 0f; // 播放蓄力动画 var animator = context.Caster.GetComponent<Animator>(); animator?.SetBool("IsCharging", true); // 开始播放蓄力粒子特效 // _chargeEffect = GameObject.Instantiate(chargeEffectPrefab, context.CastOrigin).GetComponent<ParticleSystem>(); // _chargeEffect.Play(); Debug.Log("Start charging..."); } public override void OnUpdate(float deltaTime) { base.OnUpdate(deltaTime); // 蓄力强度随时间的增长(线性或曲线) _chargePower = Mathf.Clamp01(ElapsedTime / Duration); // 可以根据_chargePower调整特效大小、颜色等 // if (_chargeEffect != null) _chargeEffect.main.startSize = _chargePower * 2f; // 检查是否提前释放(例如玩家松开按键) if (Input.GetMouseButtonUp(0)) // 假设鼠标左键控制蓄力 { // 可以设置一个标志,让IsFinished提前返回true // 这里简化处理,直接修改计时器来“完成”蓄力 ElapsedTime = Duration; } } public override void OnExit() { base.OnExit(); // 停止蓄力动画和特效 var animator = Context.Caster.GetComponent<Animator>(); animator?.SetBool("IsCharging", false); // if (_chargeEffect != null) _chargeEffect.Stop(); Debug.Log($"Charge completed with power: {_chargePower}"); // 将蓄力强度存入上下文,供后续阶段使用 Context.PowerMultiplier = 1.0f + _chargePower; // 例如,基础1倍,蓄满2倍 } } }

4.3 第三阶段:释放/箭矢飞行 (CastPhase)

这个阶段通常不是纯计时,而是创建一个飞行物(箭矢),并监听其命中事件。我们创建一个非TimedPhase的普通阶段。在HouYi/Phases/CastPhase.cs中:

// Assets/Scripts/Skills/HouYi/Phases/CastPhase.cs using Skills.Interfaces; using UnityEngine; namespace Skills.HouYi.Phases { public class CastPhase : ISkillPhase { public string PhaseName => "Cast"; private SkillContext _context; private GameObject _arrowInstance; private bool _hasHit = false; public void OnEnter(SkillContext context) { _context = context; _hasHit = false; // 1. 播放射箭动画 var animator = context.Caster.GetComponent<Animator>(); animator?.SetTrigger("Shoot"); // 2. 实例化箭矢飞行物 // _arrowInstance = GameObject.Instantiate(arrowPrefab, context.CastOrigin.position, Quaternion.identity); // var arrowScript = _arrowInstance.GetComponent<ArrowProjectile>(); // arrowScript.Initialize(context.TargetPosition, OnArrowHit); Debug.Log("Arrow released!"); } public void OnUpdate(float deltaTime) { // 此阶段的主要逻辑由飞行物脚本驱动,这里可以检查超时 // 例如,如果箭矢飞行超过5秒仍未命中,强制结束 } public void OnExit() { // 如果箭矢还未销毁(例如未命中任何目标而飞远),在此清理 // if (_arrowInstance != null) GameObject.Destroy(_arrowInstance); Debug.Log("Cast phase exit."); } public bool IsFinished() { // 当箭矢命中目标(触发OnArrowHit)或超时时,此阶段结束 return _hasHit; } // 由箭矢飞行物脚本回调 private void OnArrowHit(GameObject hitObject, Vector3 hitPoint) { _hasHit = true; // 将命中的目标和点信息存储,供下一阶段(爆炸)使用 _context.TargetUnit = hitObject; _context.TargetPosition = hitPoint; Debug.Log($"Arrow hit: {hitObject.name} at {hitPoint}"); } } }

这里的关键是,CastPhase的完成不依赖于固定时间,而是依赖于一个异步事件(箭矢命中)。这体现了事件驱动设计的灵活性。

4.4 第四阶段:爆炸/伤害结算 (ExplosionPhase)

HouYi/Phases/ExplosionPhase.cs中:

// Assets/Scripts/Skills/HouYi/Phases/ExplosionPhase.cs using Skills.Core.Phases; using UnityEngine; namespace Skills.HouYi.Phases { public class ExplosionPhase : TimedPhase { public override string PhaseName => "Explosion"; private GameObject _explosionEffect; public ExplosionPhase(float duration) : base(duration) { } public override void OnEnter(SkillContext context) { base.OnEnter(context); // 1. 播放爆炸特效 // _explosionEffect = GameObject.Instantiate(explosionPrefab, context.TargetPosition, Quaternion.identity); // 2. 计算并施加伤害 ApplyDamage(context); // 3. 可能附加击退、燃烧等效果 ApplyKnockback(context); Debug.Log($"Explosion at {context.TargetPosition} with power {context.PowerMultiplier}"); } private void ApplyDamage(SkillContext context) { // 基础伤害 * 蓄力倍数 float baseDamage = 100f; float finalDamage = baseDamage * context.PowerMultiplier; // 查找爆炸范围内的所有敌人 Collider[] hitColliders = Physics.OverlapSphere(context.TargetPosition, 5f); // 5米半径 foreach (var collider in hitColliders) { var enemy = collider.GetComponent<EnemyHealth>(); // 假设有一个 EnemyHealth 组件 if (enemy != null) { enemy.TakeDamage(finalDamage); Debug.Log($"Dealt {finalDamage} damage to {enemy.name}"); } } } private void ApplyKnockback(SkillContext context) { // 击退逻辑... } public override void OnExit() { base.OnExit(); // 可选:特效播放完毕后销毁 // if (_explosionEffect != null) GameObject.Destroy(_explosionEffect, 1.0f); } } }

4.5 组装完整的后羿射日技能

创建一个HouYiSkill组件,将其挂载到英雄角色上,用于启动技能。在HouYi/HouYiSkill.cs中:

// Assets/Scripts/Skills/HouYi/HouYiSkill.cs using Skills.Core; using Skills.HouYi.Phases; using UnityEngine; namespace Skills.HouYi { public class HouYiSkill : MonoBehaviour { private SkillStateMachine _stateMachine; void Update() { // 每帧驱动状态机 _stateMachine?.Update(Time.deltaTime); // 测试:按下空格键释放技能 if (Input.GetKeyDown(KeyCode.Space)) { ExecuteSkill(); } } public void ExecuteSkill() { // 1. 创建技能上下文 var context = new SkillContext { Caster = this.gameObject, TargetPosition = GetMouseWorldPosition(), // 假设一个获取鼠标位置的方法 CastOrigin = transform.Find("HandPos") // 手部变换节点 }; // 2. 创建状态机并添加四个阶段 _stateMachine = new SkillStateMachine(context); _stateMachine.AddPhase(new AimPhase(0.8f)); // 瞄准0.8秒 _stateMachine.AddPhase(new ChargePhase(2.0f)); // 蓄力最多2秒 _stateMachine.AddPhase(new CastPhase()); // 释放箭矢(事件驱动) _stateMachine.AddPhase(new ExplosionPhase(0.5f));// 爆炸持续0.5秒 // 3. 启动状态机 _stateMachine.Start(); Debug.Log("Hou Yi Skill: Sun Piercer started!"); } private Vector3 GetMouseWorldPosition() { // 简化的鼠标位置获取,实际项目可能需要射线检测地面 Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 100f)) { return hit.point; } return ray.origin + ray.direction * 10f; } } }

5. 运行验证与调试

HouYiSkill脚本挂载到你的英雄角色GameObject上。运行游戏,将鼠标指向某个位置,按下空格键。在 Unity 的 Console 窗口中,你应该能看到按顺序输出的日志:

Hou Yi Skill: Sun Piercer started! Entering phase: Aim, Duration: 0.8s Aiming at position: (x, y, z) Exiting phase: Aim Entering phase: Charge, Duration: 2s Start charging... Charge completed with power: 1 Exiting phase: Charge Entering phase: Cast Arrow released! Arrow hit: Enemy (name) at (x, y, z) Exiting phase: Cast Entering phase: Explosion, Duration: 0.5s Explosion at (x, y, z) with power 2 Dealt 200 damage to Enemy (name) Exiting phase: Explosion Skill execution completed.

这验证了技能四段逻辑被状态机严格按顺序驱动。你可以通过调整每个TimedPhaseDuration参数来改变技能节奏。

6. 关键优化与生产环境考量

上述代码是一个清晰的教学示例,但在真实项目中,还需要考虑更多工程化问题。

6.1 性能优化:对象池与事件解耦

  • 对象池:频繁实例化/销毁特效(GameObject.Instantiate/Destroy)会产生GC(垃圾回收)压力。对于箭矢、爆炸特效等,应使用对象池进行复用。
    // 伪代码示例 public class EffectPool { private Queue<GameObject> _pool = new Queue<GameObject>(); public GameObject GetEffect(GameObject prefab) { if (_pool.Count > 0) return _pool.Dequeue(); return Instantiate(prefab); } public void ReturnEffect(GameObject effect) { _pool.Enqueue(effect); } }
  • 事件解耦CastPhase中箭矢命中回调是紧耦合的。更好的做法是使用一个全局或技能系统内的事件总线(Event Bus),让ArrowProjectile发布一个OnProjectileHit事件,CastPhase订阅该事件。这样箭矢逻辑与技能阶段完全解耦。

6.2 配置数据外置

将技能各阶段的持续时间、伤害值、特效预制体路径等硬编码数据,提取到ScriptableObject或 JSON/XML 配置文件中。这允许策划人员在不修改代码的情况下调整技能平衡。

// Assets/Scripts/Skills/HouYi/HouYiSkillConfig.asset (ScriptableObject) [CreateAssetMenu] public class HouYiSkillConfig : ScriptableObject { public float aimDuration = 0.8f; public float maxChargeDuration = 2.0f; public float explosionDuration = 0.5f; public float baseDamage = 100f; public float explosionRadius = 5f; // 引用特效预制体 public GameObject aimingReticlePrefab; public GameObject chargeEffectPrefab; public GameObject arrowPrefab; public GameObject explosionPrefab; }

然后在HouYiSkill中引用这个配置资产,并在创建阶段时传入对应的数值。

6.3 网络同步(对于多人游戏)

在多人对战游戏中,技能释放必须同步。上述状态机模型非常适合做网络同步,因为其状态明确、阶段清晰。

  • 权威服务器:在服务器上运行完整的状态机。
  • 客户端预测:客户端本地也运行状态机以提供即时反馈,但需与服务器状态进行校正。
  • 同步关键事件:只需要同步“技能开始”、“阶段切换”、“命中目标”等关键事件和时序,而非每一帧的状态。可以使用帧编号或服务器时间进行同步。

6.4 可中断性与资源清理

我们的SkillStateMachine提供了Interrupt()方法。在实际项目中,当角色被控制(眩晕、沉默)、死亡或主动取消时,必须调用此方法。每个PhaseOnExit必须确保清理所有临时创建的对象(特效、飞行物)、重置动画状态和取消未完成的协程,防止资源泄漏和状态残留。

7. 常见问题排查清单

在实现和调试此类技能系统时,你可能会遇到以下典型问题:

问题现象可能原因检查与解决思路
技能释放后没有任何反应,日志也无输出。1.HouYiSkill组件未正确挂载或启用。
2.ExecuteSkill()方法未被触发(按键检测失败)。
3. 状态机Start()未被调用。
1. 检查 Inspector 面板确认组件存在且 enabled 为 true。
2. 在ExecuteSkill()第一行加Debug.Log,确认方法被调用。
3. 检查Update中驱动状态机的代码是否执行。
技能卡在某个阶段无法进入下一阶段。1. 该阶段的IsFinished()条件始终不满足。
2. 该阶段的OnUpdate或事件回调未正确设置完成标志。
1. 在IsFinished()内加日志,查看返回值。
2. 对于事件驱动的阶段(如CastPhase),检查回调函数是否被正确触发和调用。
特效残留或对象未销毁。1. 阶段OnExit()中未销毁临时创建的对象。
2. 技能被中断 (Interrupt) 时,OnExit可能未被调用。
1. 确保每个创建了GameObject的阶段都在OnExit中清理。
2. 在Interrupt方法中,手动调用当前阶段的OnExit
伤害计算不正确或未生效。1.ExplosionPhase中查找敌人的逻辑有问题(层过滤、半径)。
2. 伤害数值未从上下文 (SkillContext) 正确传递。
1. 使用Physics.OverlapSphere的 layerMask 参数过滤非敌人物体。在场景中可视化爆炸半径(Gizmos.DrawWireSphere)。
2. 调试context.PowerMultiplierChargePhase退出时的值,确保它被正确设置。
动画与逻辑不同步。1. 动画状态机 (Animator) 的参数设置时机不对。
2. 动画片段长度与代码中设置的阶段时长不匹配。
1. 在OnEnter中设置动画触发器,在OnExit中重置布尔值。使用动画事件来驱动逻辑更精确。
2. 调整代码中的Duration或使用动画的 normalizedTime 来驱动阶段切换。

8. 扩展方向与最佳实践

掌握了四段式技能的基础框架后,你可以从以下几个方向进行深化和扩展:

  1. 技能编辑器可视化:基于 Unity 的ScriptableObject和自定义编辑器窗口,打造一个可视化的技能编辑工具,让策划通过拖拽节点、连线的方式来配置技能阶段、时间和事件,彻底将逻辑与数据分离。
  2. 条件分支与组合技能:扩展状态机,使其支持阶段之间的条件跳转(例如,如果蓄力时间不足1秒,则跳过一个强化阶段)。甚至可以定义技能链,一个技能结束后自动触发下一个技能。
  3. 更精细的命中检测:将CastPhase中的简单命中检测,升级为带有弹道速度、重力下坠、穿透、弹射等复杂规则的投射物系统。
  4. Buff/Debuff 系统集成:在SkillContext中引入施法者和目标的 Buff 列表,在伤害计算、阶段时长、可中断性等逻辑中查询这些效果,实现“减CD”、“增伤”、“免疫控制”等复杂效果。
  5. 使用协程 (Coroutine) 替代部分 Update:对于简单的延时等待,可以使用yield return new WaitForSeconds(duration)的协程,让代码更线性易读。但需注意协程在对象销毁时的停止问题,以及与状态机主循环的配合。

最关键的最佳实践是:始终保持阶段职责单一。一个阶段只做一件事(播放动画、聚集能量、发射投射物、结算伤害)。这样当需要修改、调试或复用某个部分时,代价最小,也最符合“秒级”响应所追求的清晰与高效。

返回列表