Files
zeling_v2/Assets/_Game/Scripts/Enemies/AIBrain/Modules/AiStateFragments.cs
T
joywayer 68f615e9c1 fix(enemy): AiStateFragments 空能力 id 建图期报错,补 OnExit 收尾测试
Ability/AbilityOnce 建图期校验 abilityId 非空,与 BrainBuilder.RequireState
同一类"漏配即快速失败"约束保持一致,避免定制 AiScript 漏配 id 时产出静默
不作为的状态。同时补 4 个测试:Locomotion/Ability 的 OnExit 收尾行为,以及
两个原语对空 id 的报错行为。
2026-07-29 11:42:47 +08:00

76 lines
3.6 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 BaseGames.AI;
namespace BaseGames.Enemies
{
/// <summary>
/// 建态原语。骨架、各模块、以及定制 AiScript 写自定义态时共用这几个形状,
/// 保证"进入声明意图 / 离开收尾"的契约在全项目一致。
/// </summary>
public static class AiStateFragments
{
/// <summary>共享条件:脱离全部感知(追逐区与视野区都不在)。</summary>
public static readonly Func<IAiContext, bool> LostAllZones =
x => !x.Sensor.InChaseZone() && !x.Sensor.InVisionZone();
/// <summary>由 EnemyLocomotion 驱动的态:进入 / 每帧声明移动意图,离开时停。</summary>
public static BrainBuilder.StateBuilder Locomotion(BrainBuilder b, string state, LocomotionMode mode)
=> b.State(state)
.OnEnter(x => ApplyLocomotion(x, mode))
.Tick (x => ApplyLocomotion(x, mode))
.OnExit(x => x.Locomotion.Stop());
/// <summary>
/// 由能力驱动的持续态:进入 / 每帧确保能力在跑(受击打断后自动重触发),离开中断。
/// 用于追击、冲锋这类"只要还在这个状态就该一直在做"的能力。
/// </summary>
public static BrainBuilder.StateBuilder Ability(BrainBuilder b, string state, string abilityId)
{
RequireAbilityId(state, abilityId);
return b.State(state)
.OnEnter(x => EnsureAbility(x, abilityId))
.Tick (x => EnsureAbility(x, abilityId))
.OnExit(x => x.Combat.InterruptAbilities());
}
/// <summary>
/// 由能力驱动的一次性态:只在进入时触发,不每帧重触发。
/// 用于死亡等一次性演出——若用 Ability(),演出播完会被 Tick 无限重播。
/// </summary>
public static BrainBuilder.StateBuilder AbilityOnce(BrainBuilder b, string state, string abilityId)
{
RequireAbilityId(state, abilityId);
return b.State(state).OnEnter(x => EnsureAbility(x, abilityId));
}
/// <summary>
/// 无行为的终态。不需要在这里停移动——转入本态时,上一个态的 OnExit 已经收尾。
/// 用于"死亡演出交给物理状态机(EnemyBase.PerformDeath"的敌人。
/// </summary>
public static BrainBuilder.StateBuilder Terminal(BrainBuilder b, string state)
=> b.State(state);
static void ApplyLocomotion(IAiContext x, LocomotionMode mode)
{
if (mode == LocomotionMode.Face) x.Locomotion.Face(x.Sensor.LastKnown);
else x.Locomotion.SetMode(mode);
}
// 建图期校验:空 id 会让状态存在却什么都不做——敌人杵着不动、无任何报错,
// 正是 BrainBuilder.RequireState 要防的同一类静默失败。放建图期而非运行时回调里,
// 既能快速失败,也省掉热路径上每帧一次的判空。
static void RequireAbilityId(string state, string abilityId)
{
if (string.IsNullOrEmpty(abilityId))
throw new ArgumentException(
$"AiStateFragments: 状态 '{state}' 未配置能力 id。若该状态本就不需要能力" +
"(如死亡演出交给物理状态机),请改用 Terminal()。", nameof(abilityId));
}
static void EnsureAbility(IAiContext x, string abilityId)
{
if (!x.Combat.IsAbilityRunning(abilityId)) x.Combat.UseAbility(abilityId);
}
}
}