输入、技能意图与英雄状态机 private

输入、技能意图与英雄状态机

返回栏目首页

1. UI 只产生意图

技能按钮入口在 UIInGamePanelSkill.TryLaunchSkill。它根据技能 A/B/C/D 各自的摇杆,选择技能方向;普攻还可以优先沿当前移动方向。最终只投递一份数据:

MessageCenter.Instance.PostMessage(
    LogicMessageType.TriggerSkill,
    new TriggerSkillData {
        SkillType = skillType,
        FacingDir = new Vector3(h, 0.0f, v),
        UsePickingRay = false,
        PickingRay = ray,
        IsUsingMoveControllerDirection = isUsingMoveControllerDirection,
        DisRatio = disRatio
    });

这里没有 Animator.SetTrigger,也没有直接生成伤害。这样 UI、键鼠、手柄、引导脚本都可以复用同一条逻辑入口。

2. 为什么方向要过两次转换

UIInGamePanelSkill 先保存屏幕摇杆方向;ApplyPlayerInputSystem.OnLaunchSkill 再按当前摄像机朝向转换成世界方向:

Vector3 offsetDir =
    MathTool.CorrectMoveDirWithLookDirection(camera, rawData.FacingDir);

pickingRay.origin = hero.GetLogicPosition() + Vector3.up;
pickingRay.direction =
    hero.GetLogicPosition() + offsetDir - pickingRay.origin;

这一步解决“摇杆向上永远表示屏幕上方,而不是世界 Z 正方向”。新增锁定目标、自由镜头或俯视角旋转时,方向修正应留在输入应用层,不要散进每个英雄状态机。

3. EntityId 路由

PlayerInputSystem 只处理本地英雄,把 TryToLaunchSkill 转成携带实体 ID 的 SyncLaunchSkill:

TransformMessageWithEntityID(
    LogicMessageType.TryToLaunchSkill,
    LogicMessageType.SyncLaunchSkill);

void TransformMessageWithEntityID(LogicMessageType from,
    LogicMessageType to) => _messageCenter
    .ObserveOn(from)
    .Select(message => (message.Data, Entity: GetHero()))
    .Where(x => x.Entity != null && x.Entity.hasEntityID)
    .Subscribe(x => _messageCenter.PostMessage(
        to, x.Data, x.Entity.entityID.Value));

ApplyPlayerInputSystem 再通过 EntityId 找到 Hero,最终调用 hero.LaunchSkill(skillData)。多人同屏时不要使用“场景中第一个 Hero”替代这条索引。

4. Hero 不是立即切状态,而是缓存输入

public void LaunchSkill(LaunchSkillData skillData)
{
    if (skillData != null && SkillUpdator != null)
    {
        if (SkillUpdator.IsAvailable(skillData.SkillType))
        {
            NeedLaunchSkill = true;
            LaunchSkillType = skillData.SkillType;
            LaunchSkillPickingRay = skillData.PickingRay;
            LaunchSkillDisRatio = skillData.DisRatio;
        }
        else
        {
            NeedLaunchSkill = false;
            NeedTipCannotLaunchSkill = true;
            TipCannotLaunchSkillType = skillData.SkillType;
            CannotLaunchSkillType =
                SkillUpdator.GetAvailableType(skillData.SkillType);
        }
    }
}

这是手感的关键:输入到达时不强行打断当前状态,而是设置“待释放技能”。当前状态每帧通过 CommonCheckLaunchSkill 在允许的窗口消费该意图。它天然形成一帧级输入缓冲。

注意这不是无限输入队列:当前实现主要保存一个 LaunchSkillType,后来的输入可能覆盖前面的待处理意图。需要真正的多键缓冲时,应新增带时间戳的有限队列,而不是继续堆布尔字段。

5. 三层可释放检查

HeroStateMachine.CommonCheckLaunchSkill 顺序是:

bool handled = IgnoreStateInterruptLaunchSkill();
if (!handled) handled = CheckLaunchEquipSkill();
if (!handled) handled = CheckLaunchSkill(checkOnlyAttack, excludeAttack);
return handled;

含义:

  • IgnoreStateInterruptLaunchSkill:某些技能可无视当前状态的普通打断规则。
  • CheckLaunchEquipSkill:装备技能 E1/E2 独立处理。
  • CheckLaunchSkill:由具体英雄把 A/B/C/D 映射到自己的状态。

通用状态机负责规则骨架,具体英雄负责动作设计。新增英雄时不要复制一套 UI 和网络系统,只补自己的状态枚举、注册和 CheckLaunchSkill。

6. CD、沉默、能量的前置检查

SkillUpdator.GetAvailableType 会区分:

  • 技能冷却中。
  • 技能被沉默。
  • 可用次数耗尽。
  • 能量不足。
  • 其他技能配置限制。

前置检查的价值是立即给 UI 反馈;但联网模式下,客户端检查不是最终权威。服务端还有 StartSkillCd、能量消耗和技能缓存。两端配置不一致时会出现“本地能按、服务端不结算”或“本地灰掉、服务端其实可用”。

7. 状态消费顺序

SkillCommonUpdate 的核心优先级是:

受击/死亡等强制状态
  -> 待释放技能
  -> 移动取消
  -> 动画自然完成回 Idle
  -> Animator 锁定检查

源码中在可打断后依次执行 CheckHurt、CommonCheckLaunchSkill、CheckRun,最后检查动画是否结束。技能优先于跑步,能避免连招窗口内轻微摇杆输入抢走下一段攻击。

8. 调试断点

按一次技能至少观察这些字段:

位置观察值
UIInGamePanelSkill.TryLaunchSkillSkillType、FacingDir、DisRatio
PlayerInputSystem本地 Hero 是否有 EntityID
ApplyPlayerInputSystem.OnLaunchSkill相机修正后的 PickingRay
Hero.LaunchSkillNeedLaunchSkill、CannotLaunchSkillType
具体 CheckLaunchSkill切到哪个 State
状态 Begin动画名、MoveData、SkillInstId
状态 UpdatenormalizedTime、打断阈值
状态 EndTimer/Interval、移动策略和动画参数是否清理

9. 常见错误

现象首查
按键无反应TriggerSkill 是否发出,PlayerInputSystem 是否找到本地 EntityID
朝向和摇杆相反相机方向修正、移动反向 Buff、LastNotZeroDirection
技能偶尔丢失单槽意图被覆盖、当前状态一直不可打断
CD 提示不对SkillUpdator 本地缓存与服务端 UpdateRoleSkillCD
远端英雄不触发SyncLaunchSkill 的 sender/entityId、状态包顺序