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