移动、攻击转向与取消窗口 private
移动、攻击转向与取消窗口
1. 移动与攻击为什么没有硬断层
本项目没有把“移动”和“技能动画”绑定成互斥的两个 MonoBehaviour。状态机决定当前规则,MoveController 独立执行位移,Animator 独立表现姿态。技能可以选择:
- 进入技能时只旋转,不平移。
- 用 DirectionMove 做冲刺。
- 保留默认 NavMove,让持续施法时慢速移动。
- 在后摇窗口切回 Run。
- 受击时用新的 DirectionMove 做击退。
所以动画切换、逻辑状态和实际位移能在不同时间交接。
2. 普攻转向
以 Girl03 A 为例,技能开始先算目标,再切 Rotation 策略:
Vector3 target = CalculateSkillTargetPos(hero.transform.position);
hero.MoveController.ChangeMoveStrategy(MoveStrategyType.Rotation);
MoveDataFactory.CreateRotationMoveData(
target, param.TurnSpeed, ref _moveData);
hero.MoveController.Begin(
_moveData,
MoveControllerSyncParam.FROM_BEGIN_WAITTING_STATE);
这比瞬间 transform.LookAt 更自然,也能把转向意图通过移动同步链路交给远端。只有像冲刺 B 这类必须立刻锁朝向的技能,才直接设置 Quaternion.LookRotation。
3. 技能中持续移动
通用函数 CalculateMoveWhenUsingSkill 只在当前策略为 CharacterDirectionMove 且允许动态目标时工作。它可以按固定倍率降低移动速度,也可以读取技能移动曲线:
float t = Mathf.Clamp01(time / skillParam.MoveDuration);
float speedFactor = skillParam.MoveCurve?.Evaluate(t) ?? t;
hero.MoveController.SetMoveSpeed(
hero.entityObject.property.MoveWalk * speedFactor);
hero.MoveController.SetMoveDestination(
hero.MoveController.MoveDestination);
持续施法不需要每帧新建控制器,只更新速度和目标。只有旧移动已结束时才重新创建 NavMove 数据。这样减少频繁 End/Begin 带来的卡顿和网络包抖动。
4. 打断百分比的真实算法
float startInterruptTime =
isCalcStartInterruptTime
? 1.0f - Mathf.Clamp01(postInterruptPercentage)
: Mathf.Clamp01(postInterruptPercentage);
return AnimatorController.GetCurrentAnimationTime()
>= startInterruptTime;
例如 PostInterruptPercentage = 0.3,表示动画最后 30% 可被打断,阈值是 0.7;它不是“播放到 30% 时可打断”。这是最容易配反的字段。
前摇检查使用另一种含义:PreInterruptPercentage 直接作为阈值,并通过参数关闭 1 - value 的换算。调配置前必须确认调用的是哪一个重载。
5. 技能、移动、受击的优先级
SkillCommonUpdate 在打断窗口内按以下顺序处理:
if (CheckHurt()) return true;
if (CommonCheckLaunchSkill(false, false)) return true;
if (moveInterruptPercentage <= 0 && CheckRun()) return true;
if (animationFinished) SwitchState(Idle);
if (moveInterruptPercentage > 0 &&
IsInterruptable(moveInterruptPercentage))
if (CheckRun()) return true;
由此得到:
- 受击优先于普通技能衔接。
- 技能衔接优先于移动。
- 配了单独移动窗口后,移动只能在自己的阈值后接管。
- 动画完成是兜底,不是唯一退出条件。
6. 输入缓冲的上限
Hero 当前保存一个待释放技能,而不是队列。这能覆盖“在后摇前提前按下一技能”的常用场景,但有三个边界:
- 同一窗口连续按两个技能,后者可能覆盖前者。
- 技能一直不可打断时,意图可能保留到很晚。
- 没有显式输入过期时间。
若要增强连招系统,推荐添加:
struct BufferedSkillInput {
SkillType Type;
Ray PickingRay;
float DistanceRatio;
double InputTime;
}
队列长度控制在 2 到 3,设置 150 到 300 ms 的有效期,并在状态切换、死亡、Cutscene、断线重建时清空。不要做无限队列,否则玩家会感觉角色在自动执行旧指令。
7. Root Motion 的边界
从 Girl03 B 的实际实现看,逻辑位移由 CreateLinearMoveData 驱动,而非 Root Motion。这是联网状态同步项目的合理选择:
| 方案 | 优点 | 风险 |
|---|---|---|
| MoveController 驱动 | 可配置、可同步、可纠偏 | 动画脚底可能滑,需要动作速度匹配 |
| Root Motion 驱动 | 单机观感自然 | 网络重放、碰撞和服务端位置难统一 |
| 混合 | 可保留细小身体重心变化 | 必须明确谁写最终 Transform |
项目中新增技能默认沿用 MoveController 驱动;若确实使用 Root Motion,只应用视觉骨骼或把位移采样转换为 MoveData,不能让 Animator 和 NavMeshAgent 同时写世界位置。
8. 攻击冲刺的清理协议
每个有特殊位移的状态 End 至少要做:
AnimatorController.FinishPlayAnimation();
MoveController.End();
MoveController.DynamicHandleNewDestination = false;
MoveController.ChangeMoveStrategy(
MoveController.DefaultMoveStrategy);
CollisionController.IsTriggerCollision(
CollisionLayer.Hero, false);
timer?.Dispose();
interval?.Dispose();
不是每个状态都需要全部调用,但需要逐项判断。C 的内部 Pre 到 Loop 跳转就是例外:它有意保留移动和发射任务。
9. 调参顺序
- 固定网络延迟为本地,先把动作 Clip 和移动曲线调顺。
- 标出动画接触帧,设置 AttackLogicDelay。
- 对齐 AttackEffectDelay 和声音,不要先调取消窗口。
- 设置 Pre/Post/MoveInterruptPercentage。
- 验证 A 接 B、A 接移动、B 接 C、受击打断、死亡强断。
- 再加入 50/100/200 ms 延迟和丢包,观察远端是否追帧过猛。
- 最后验证不同 Animator Controller 变体的 Clip 长度。
10. 症状定位
| 症状 | 高概率原因 |
|---|---|
| 技能结束后继续滑 | End 未结束特殊 MoveStrategy |
| 技能一按就被移动取消 | MoveInterruptPercentage 为 0 或配反 |
| 连招必须等动作播完 | PostInterruptPercentage 太小或 CheckLaunchSkill 未执行 |
| 转向突然瞬移 | 直接写 rotation,未用 Rotation 策略 |
| 远端冲刺晚一拍 | MoveBegin 等 State、PacketCache 堆积 |
| 换皮后手感变了 | 新 Controller 的 Clip 长度不同,normalizedTime 窗口实际秒数变化 |