using System; using System.Collections.Generic; using BaseGames.AI; namespace BaseGames.Enemies { /// /// 感知骨架:把「未发现 → 警觉 → 交战」的升降级规则,挂到两个模块声明的态上。 /// 骨架永远只做这一件事——未发现态长什么样、交战怎么打,全部归模块。 /// /// 规则: /// 未发现:在追逐区 → 交战(优先);否则 有警觉态且在视野 → 警觉;否则保持。 /// 警觉: 在追逐区 → 交战;否则 脱离视野 → Rest;否则保持(朝向最后已知位置)。 /// 交战: 脱战条件由交战模块自定,脱战一律回 Rest —— 永不回警觉。 /// 死亡: 全局事件边,优先级最高,且不受 IsControllable 门阻挡。 /// /// 死亡态由骨架自己声明为无行为终态,不做成可插拔层——死亡在本项目里完全归物理层 /// (EnemyBase.PerformDeath 切终态/关碰撞/播死亡动画/归还池;EnemyDeathSequence 做 /// 死亡前摇演出并可经动画事件生成小怪)。且 PerformDeath 先 ForceState(Dead) 再发 /// Died 信号,此后 IsControllable 永久为假、AiRuntime 不再求值任何条件边, /// 所以死亡链里放条件转换必然卡住。AI 图需要这个态只是为了让全局边有去处、决策停止。 /// /// 入口默认取 unaware.Entry;需要前置态(掉落链 / 出场链)的敌人在调用本方法后 /// 再调 b.Entry("自己的态") 覆盖即可。 /// public static class PerceptionSkeleton { public const string Alert = "Alert"; public const string Death = "Death"; static readonly Func HasAlertAndInVision = x => x is IEnemyActor a && a.HasAlertState && x.Sensor.InVisionZone(); static readonly Func LeftVision = x => !x.Sensor.InVisionZone(); public static void Add(BrainBuilder b, IUnawareModule unaware, IEngagementModule engagement) { if (b == null) throw new ArgumentNullException(nameof(b)); if (unaware == null) throw new ArgumentNullException(nameof(unaware)); if (engagement == null) throw new ArgumentNullException(nameof(engagement)); // 1) 各模块先声明自己的态(未发现层此阶段只声明态,不挂内部边) unaware.Declare(b); engagement.Build(b, unaware.Rest); AiStateFragments.Terminal(b, Death); var states = unaware.States; if (states == null || states.Count == 0) throw new InvalidOperationException($"{unaware.GetType().Name}: States 不能为空。"); for (int i = 0; i < states.Count; i++) b.RequireState(states[i]); // Declare 漏声明的态在此暴露 if (!Contains(states, unaware.Rest)) throw new InvalidOperationException( $"{unaware.GetType().Name}: Rest='{unaware.Rest}' 必须属于 States。"); b.Entry(unaware.Entry); b.Global().To(Death).OnEvent(AiSignal.Died); // 2) 升级边先挂——AiRuntime 按声明顺序评估条件边,升级必须优先于 // 未发现层的内部边(如巡逻计时),否则玩家进追逐区那一帧可能被计时边抢走。 for (int i = 0; i < states.Count; i++) b.State(states[i]) .To(engagement.EntryState).When(engagement.CanEngage, engagement.EngageLabel) .To(Alert).When(HasAlertAndInVision, "InVision+HasAlert"); AiStateFragments.Locomotion(b, Alert, LocomotionMode.Face) .To(engagement.EntryState).When(engagement.CanEngage, engagement.EngageLabel) .To(unaware.Rest).When(LeftVision, "leftVision"); // 3) 未发现层内部边最后挂,优先级低于升级边 unaware.Link(b); } static bool Contains(IReadOnlyList list, string value) { for (int i = 0; i < list.Count; i++) if (list[i] == value) return true; return false; } } }