输入、技能意图与英雄状态机 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.TryLaunchSkill | SkillType、FacingDir、DisRatio |
PlayerInputSystem | 本地 Hero 是否有 EntityID |
ApplyPlayerInputSystem.OnLaunchSkill | 相机修正后的 PickingRay |
Hero.LaunchSkill | NeedLaunchSkill、CannotLaunchSkillType |
具体 CheckLaunchSkill | 切到哪个 State |
| 状态 Begin | 动画名、MoveData、SkillInstId |
| 状态 Update | normalizedTime、打断阈值 |
| 状态 End | Timer/Interval、移动策略和动画参数是否清理 |
9. 常见错误
| 现象 | 首查 |
|---|---|
| 按键无反应 | TriggerSkill 是否发出,PlayerInputSystem 是否找到本地 EntityID |
| 朝向和摇杆相反 | 相机方向修正、移动反向 Buff、LastNotZeroDirection |
| 技能偶尔丢失 | 单槽意图被覆盖、当前状态一直不可打断 |
| CD 提示不对 | SkillUpdator 本地缓存与服务端 UpdateRoleSkillCD |
| 远端英雄不触发 | SyncLaunchSkill 的 sender/entityId、状态包顺序 |