Files
zeling_v2/Assets/_Game/Scripts/Enemies/AIBrain/Modules/PerceptionSkeleton.cs
T

84 lines
4.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using BaseGames.AI;
namespace BaseGames.Enemies
{
/// <summary>
/// 感知骨架:把「未发现 → 警觉 → 交战」的升降级规则,挂到两个模块声明的态上。
/// 骨架永远只做这一件事——未发现态长什么样、交战怎么打,全部归模块。
///
/// 规则:
/// 未发现:在追逐区 → 交战(优先);否则 有警觉态且在视野 → 警觉;否则保持。
/// 警觉: 在追逐区 → 交战;否则 脱离视野 → Rest;否则保持(朝向最后已知位置)。
/// 交战: 脱战条件由交战模块自定,脱战一律回 Rest —— 永不回警觉。
/// 死亡: 全局事件边,优先级最高,且不受 IsControllable 门阻挡。
///
/// 死亡态由骨架自己声明为无行为终态,不做成可插拔层——死亡在本项目里完全归物理层
/// EnemyBase.PerformDeath 切终态/关碰撞/播死亡动画/归还池;EnemyDeathSequence 做
/// 死亡前摇演出并可经动画事件生成小怪)。且 PerformDeath 先 ForceState(Dead) 再发
/// Died 信号,此后 IsControllable 永久为假、AiRuntime 不再求值任何条件边,
/// 所以死亡链里放条件转换必然卡住。AI 图需要这个态只是为了让全局边有去处、决策停止。
///
/// 入口默认取 unaware.Entry;需要前置态(掉落链 / 出场链)的敌人在调用本方法后
/// 再调 b.Entry("自己的态") 覆盖即可。
/// </summary>
public static class PerceptionSkeleton
{
public const string Alert = "Alert";
public const string Death = "Death";
static readonly Func<IAiContext, bool> HasAlertAndInVision =
x => x is IEnemyActor a && a.HasAlertState && x.Sensor.InVisionZone();
static readonly Func<IAiContext, bool> 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<string> list, string value)
{
for (int i = 0; i < list.Count; i++)
if (list[i] == value) return true;
return false;
}
}
}