移动、攻击转向与取消窗口 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. 调参顺序

  1. 固定网络延迟为本地,先把动作 Clip 和移动曲线调顺。
  2. 标出动画接触帧,设置 AttackLogicDelay。
  3. 对齐 AttackEffectDelay 和声音,不要先调取消窗口。
  4. 设置 Pre/Post/MoveInterruptPercentage。
  5. 验证 A 接 B、A 接移动、B 接 C、受击打断、死亡强断。
  6. 再加入 50/100/200 ms 延迟和丢包,观察远端是否追帧过猛。
  7. 最后验证不同 Animator Controller 变体的 Clip 长度。

10. 症状定位

症状高概率原因
技能结束后继续滑End 未结束特殊 MoveStrategy
技能一按就被移动取消MoveInterruptPercentage 为 0 或配反
连招必须等动作播完PostInterruptPercentage 太小或 CheckLaunchSkill 未执行
转向突然瞬移直接写 rotation,未用 Rotation 策略
远端冲刺晚一拍MoveBegin 等 State、PacketCache 堆积
换皮后手感变了新 Controller 的 Clip 长度不同,normalizedTime 窗口实际秒数变化