技能释放、连招与 Girl03 完整案例 private

技能释放、连招与 Girl03 完整案例

返回栏目首页

1. 技能不是一段动画

一项完整技能至少包含:

输入意图
 -> 可释放检查
 -> 状态 Begin
 -> 动画/转向/移动
 -> 前摇等待
 -> 逻辑命中或生成子弹
 -> 命中特效与音效
 -> 可取消窗口
 -> 状态 End 清理
 -> Idle/Run/下一技能

本项目把这些步骤写在具体英雄状态机里。SkillParam 提供时间和数值,AnimatorController 负责动画状态,MoveController 负责位移,SkillNotifier 负责向 Battle 上报。

2. Girl03 普攻 A:表现时点与逻辑时点分离

入口同时启动 CD、动画和转向:

SkillData skillData = new SkillData {
    SkillType = SkillType.Skill_A,
    SkillTypeUI = SkillType.Skill_A,
    CoolDownDuration =
        _heroInstance.Attribute.Skill_A_Param.CooldownInterval
};
_heroInstance.SkillUpdator.StartUseSkill(skillData);

_heroInstance.AnimatorController.PlayAnimation(
    "Skill_A", "Skill", Convert.ToInt32(SkillType.Skill_A));

Vector3 targetPosition =
    CalculateSkillTargetPos(_heroInstance.transform.position);
_heroInstance.MoveController.ChangeMoveStrategy(
    MoveStrategyType.Rotation);
MoveDataFactory.CreateRotationMoveData(
    targetPosition,
    _heroInstance.Attribute.Skill_A_Param.TurnSpeed,
    ref _moveData);

视觉特效使用 AttackEffectDelay,真正构造攻击数据使用 AttackLogicDelay。两者可以不同:

Observable.Timer(TimeSpan.FromSeconds(
    param.AttackEffectDelay))
    .Subscribe(_ => EffectPlayer.Instance.StartEffect(...));

Observable.Timer(TimeSpan.FromSeconds(
    param.AttackLogicDelay))
    .Subscribe(_ => {
        PendingAttackData.HurtDelay = param.HurtDelay;
        PendingAttackData.HurtValue = param.HurtValue;
        PendingAttackData.AngleRange = param.AttackAngle;
        PendingAttackData.FreezeAnimTime =
            param.AttackFreezeAnimTimeFactor *
            hero.Attribute.AttackFreezeAnimTime;
        SkillNotifier.StartUseSkill(
            skill.instId, _moveData, useSkillGuid);
    });

调参时先对齐武器接触画面的那一帧,再让逻辑命中点和 VFX 相差尽可能小;不能因为“特效看起来晚”就直接改伤害值或动画速度。

3. A 的取消窗口

float launchInterrupt =
    CheckLaunchSkillInterruptThisPercentage(
        _heroInstance.Attribute.Skill_A_Param.PreInterruptPercentage);

SkillCommonUpdate(
    launchInterrupt != 0.0f
        ? launchInterrupt
        : _heroInstance.Attribute.Skill_A_Param.PostInterruptPercentage,
    _heroInstance.Attribute.Skill_A_Param.PreInterruptPercentage,
    _heroInstance.Attribute.Skill_A_Param.MoveInterruptPercentage);

三个百分比承担不同职责:

字段作用
PreInterruptPercentage前摇期间是否允许技能衔接,但通常排除再次普攻
PostInterruptPercentage动画后段允许其他技能接管
MoveInterruptPercentage动画后段允许移动接管

它们用 Animator 的 normalizedTime 判断,不是秒数。更换长度不同的 Clip 时,同一百分比会自动随动画长度缩放,但命中时点也要同步复核。

4. Girl03 冲刺 B:位移不是 Root Motion

Vector3 targetPosition =
    CalculateDashSkillTargetPos(_heroInstance.transform.position);
Vector3 moveDir = GetNormalizedForwardDir2D(targetPosition);

_heroInstance.MoveController.ChangeMoveStrategy(
    MoveStrategyType.DirectionMove);

MoveDataFactory.CreateLinearMoveData(
    _heroInstance.transform.position,
    moveDir,
    param.MoveDuration,
    param.MoveDistance,
    param.MoveCurve,
    param.MoveCurveEnum,
    1000.0f,
    moveDir.normalized,
    false, false, true,
    ref _moveData);

_heroInstance.MoveController.Begin(
    _moveData,
    MoveControllerSyncParam.FROM_BEGIN_WAITTING_STATE);

这段说明 B 的主体位移来自 MoveData 和曲线,而不是 Animator Root Motion。好处是:

  • 位移参数可以同步给远端重放。
  • 冲刺结束可恢复默认策略。
  • 碰撞、导航边缘和速度曲线能统一处理。
  • 动画只负责姿态,不成为逻辑位置唯一来源。

Begin 还临时改变 Hero 碰撞;End 必须恢复碰撞、结束移动、恢复默认策略并销毁延迟回调。遗漏任何一步都会产生“下一次冲刺穿人”“切技能后继续滑”等问题。

5. Girl03 持续技能 C:Pre、Loop、Post 三段

C 的状态拆为:

stateDiagram-v2
    [*] --> Skill_C
    Skill_C --> Skill_C_Loop: 起手动画结束
    Skill_C_Loop --> Skill_C_Loop: 继续触发/还有发射任务
    Skill_C_Loop --> OtherSkill: 0.1s 后收到可切技能
    Skill_C_Loop --> Skill_C_Post: 发射完成且移动稳定
    Skill_C_Post --> Idle: 后摇完成
    Skill_C_Post --> Run: 到达移动取消窗口

Skill_C_Begin 启动起手动画和首个 AttackLogicDelay。到达逻辑点后生成第一颗子弹,并用 Observable.Interval 发射余下子弹。每一轮都保存独立的 handler,避免新一轮覆盖旧一轮:

var intervalIndex = _SkillC_IntervalHandler_Count;
IDisposable handler =
    Observable.Interval(TimeSpan.FromSeconds(loopInterval))
    .Subscribe(_ => {
        SpawnBullet(...);
        if (--bulletCount <= 0) {
            handlerDict[intervalIndex].Dispose();
            handlerDict.Remove(intervalIndex);
        }
    });
handlerDict.Add(intervalIndex, handler);

Skill_C_Update 等起手动画完成后进入 Loop;Loop 中经过 0.1 秒才允许检查下一技能,防止同一帧输入造成状态抖动:

if (StateElapsedTime > 0.1f)
{
    _SkillC_Loop_Interrupt = true;
    if (CheckLaunchSkill(false, false) &&
        _SkillC_Loop_Interrupt)
        return;
}

6. 为什么状态切换时有条件清理

Skill_C_End 只有在下一状态不是 Skill_C_Loop 时,才销毁发射任务和恢复移动。否则从起手到循环段的内部跳转会误杀同一技能尚未完成的逻辑。

同理,Skill_C_Loop_End 若下一状态就是 Skill_C_Post,会把移动上下文留给后摇;如果被别的技能接管,则完整清理。这种“看 NextState 决定清理范围”的写法是连贯感的来源,也是最容易出资源泄漏和残留状态的地方。

7. useSkillGuid 与 UniqueId

两种 ID 不要混用:

  • useSkillGuid:一次技能释放链路,用于把技能、子弹、Buff 等关联起来。
  • AttackData.UniqueId:一次具体攻击/命中数据,用于伤害结果匹配和本地预测去重。
  • SkillInstId:玩家技能缓存中的实例 ID,不等于技能配置 ID,也不等于一次释放。

连招或多段技能必须明确哪些段共享 useSkillGuid,哪些命中需要新 UniqueId。

8. 新增技能的实现清单

  1. 在具体 HeroStateMachine 注册状态 Begin/Update/End。
  2. 在 CheckLaunchSkill 把 SkillType 映射到状态。
  3. Begin 中启动 UI CD、动画、目标计算、移动和声音。
  4. 用配置字段确定逻辑命中和特效时点。
  5. 只让本地拥有者生成上行攻击/子弹,远端只做表现重放。
  6. 每次命中生成稳定 UniqueId,整次释放生成合适的 useSkillGuid。
  7. Update 中声明受击、技能和移动的取消优先级。
  8. End 中 Dispose 所有 Timer/Interval,恢复碰撞和移动策略,Finish 动画参数。
  9. 测试 Idle、Run、受击、死亡、切图、断线和对象池复用时的强制退出。