feat(ai): BrainBuilder.DeclareState——重复声明状态即报错,防模块间同名静默覆盖
State() 仍保持宽松(有则取),用于给已声明的态挂边;DeclareState() 只用于 首次声明行为,重复声明同名状态会抛异常。AiStateFragments 的四个建态原语 (Locomotion/Ability/AbilityOnce/Terminal)改用 DeclareState。同时给 IEngagementModule/IDeathModule/IUnawareModule 补充不变量说明。
This commit is contained in:
@@ -85,5 +85,34 @@ namespace BaseGames.Tests.EditMode.AI
|
|||||||
b.State("Real");
|
b.State("Real");
|
||||||
Assert.DoesNotThrow(() => b.RequireState("Real"));
|
Assert.DoesNotThrow(() => b.RequireState("Real"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DeclareState_Throws_OnDuplicateName()
|
||||||
|
{
|
||||||
|
var b = new BrainBuilder();
|
||||||
|
b.DeclareState("Dup");
|
||||||
|
var ex = Assert.Throws<System.InvalidOperationException>(() => b.DeclareState("Dup"));
|
||||||
|
StringAssert.Contains("Dup", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DeclareState_ReturnsUsableBuilder_OnFirstDeclare()
|
||||||
|
{
|
||||||
|
var b = new BrainBuilder();
|
||||||
|
b.Entry("A");
|
||||||
|
b.DeclareState("A").To("B").When(c => true, "go");
|
||||||
|
b.DeclareState("B");
|
||||||
|
Assert.DoesNotThrow(() => b.Build());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void State_StaysLenient_ForAttachingToDeclaredState()
|
||||||
|
{
|
||||||
|
// State() 仍是"有则取"——骨架要给模块已声明的态挂升级边,靠的就是这个。
|
||||||
|
var b = new BrainBuilder();
|
||||||
|
b.Entry("A");
|
||||||
|
b.DeclareState("A");
|
||||||
|
Assert.DoesNotThrow(() => b.State("A").To("A").When(c => false, "noop"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,20 @@ namespace BaseGames.AI
|
|||||||
$"BrainBuilder: 状态 '{name}' 尚未声明。请先声明它的行为(AiStateFragments.* 或 State(name)),再挂转换。");
|
$"BrainBuilder: 状态 '{name}' 尚未声明。请先声明它的行为(AiStateFragments.* 或 State(name)),再挂转换。");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首次声明一个状态。名字已存在即抛——防止两个独立模块取了同名状态时,
|
||||||
|
/// 后者的 OnEnter/Tick/OnExit 静默覆盖前者(敌人跑错行为且零报错)。
|
||||||
|
/// 只有"首次声明行为"的调用点用它;给已声明的态挂边仍用 State()。
|
||||||
|
/// </summary>
|
||||||
|
public StateBuilder DeclareState(string name)
|
||||||
|
{
|
||||||
|
if (_states.ContainsKey(name))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"BrainBuilder: 状态 '{name}' 已被声明过。两个模块取了同名状态会互相覆盖回调——" +
|
||||||
|
"请给其中一个换个不冲突的名字。");
|
||||||
|
return State(name);
|
||||||
|
}
|
||||||
|
|
||||||
internal void AddGlobal(Transition t) => _globals.Add(t);
|
internal void AddGlobal(Transition t) => _globals.Add(t);
|
||||||
|
|
||||||
public AiGraph Build()
|
public AiGraph Build()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace BaseGames.Enemies
|
|||||||
|
|
||||||
/// <summary>由 EnemyLocomotion 驱动的态:进入 / 每帧声明移动意图,离开时停。</summary>
|
/// <summary>由 EnemyLocomotion 驱动的态:进入 / 每帧声明移动意图,离开时停。</summary>
|
||||||
public static BrainBuilder.StateBuilder Locomotion(BrainBuilder b, string state, LocomotionMode mode)
|
public static BrainBuilder.StateBuilder Locomotion(BrainBuilder b, string state, LocomotionMode mode)
|
||||||
=> b.State(state)
|
=> b.DeclareState(state)
|
||||||
.OnEnter(x => ApplyLocomotion(x, mode))
|
.OnEnter(x => ApplyLocomotion(x, mode))
|
||||||
.Tick (x => ApplyLocomotion(x, mode))
|
.Tick (x => ApplyLocomotion(x, mode))
|
||||||
.OnExit(x => x.Locomotion.Stop());
|
.OnExit(x => x.Locomotion.Stop());
|
||||||
@@ -27,7 +27,7 @@ namespace BaseGames.Enemies
|
|||||||
public static BrainBuilder.StateBuilder Ability(BrainBuilder b, string state, string abilityId)
|
public static BrainBuilder.StateBuilder Ability(BrainBuilder b, string state, string abilityId)
|
||||||
{
|
{
|
||||||
RequireAbilityId(state, abilityId);
|
RequireAbilityId(state, abilityId);
|
||||||
return b.State(state)
|
return b.DeclareState(state)
|
||||||
.OnEnter(x => EnsureAbility(x, abilityId))
|
.OnEnter(x => EnsureAbility(x, abilityId))
|
||||||
.Tick (x => EnsureAbility(x, abilityId))
|
.Tick (x => EnsureAbility(x, abilityId))
|
||||||
.OnExit(x => x.Combat.InterruptAbilities());
|
.OnExit(x => x.Combat.InterruptAbilities());
|
||||||
@@ -40,7 +40,7 @@ namespace BaseGames.Enemies
|
|||||||
public static BrainBuilder.StateBuilder AbilityOnce(BrainBuilder b, string state, string abilityId)
|
public static BrainBuilder.StateBuilder AbilityOnce(BrainBuilder b, string state, string abilityId)
|
||||||
{
|
{
|
||||||
RequireAbilityId(state, abilityId);
|
RequireAbilityId(state, abilityId);
|
||||||
return b.State(state).OnEnter(x => EnsureAbility(x, abilityId));
|
return b.DeclareState(state).OnEnter(x => EnsureAbility(x, abilityId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -48,7 +48,7 @@ namespace BaseGames.Enemies
|
|||||||
/// 用于"死亡演出交给物理状态机(EnemyBase.PerformDeath)"的敌人。
|
/// 用于"死亡演出交给物理状态机(EnemyBase.PerformDeath)"的敌人。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static BrainBuilder.StateBuilder Terminal(BrainBuilder b, string state)
|
public static BrainBuilder.StateBuilder Terminal(BrainBuilder b, string state)
|
||||||
=> b.State(state);
|
=> b.DeclareState(state);
|
||||||
|
|
||||||
static void ApplyLocomotion(IAiContext x, LocomotionMode mode)
|
static void ApplyLocomotion(IAiContext x, LocomotionMode mode)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ namespace BaseGames.Enemies
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 死亡层:单态终结(演出交给物理状态机)/ 单段死亡能力 / 两段演出(可带生成物)。
|
/// 死亡层:单态终结(演出交给物理状态机)/ 单段死亡能力 / 两段演出(可带生成物)。
|
||||||
/// 骨架只挂一条 Global → EntryState 的 Died 事件边,链条内容全归本模块。
|
/// 骨架只挂一条 Global → EntryState 的 Died 事件边,链条内容全归本模块。
|
||||||
|
///
|
||||||
|
/// 同 IEngagementModule:骨架只把全局 Died 边指向 EntryState,不在死亡链内部挂边,
|
||||||
|
/// 故单阶段 Build 足够。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IDeathModule
|
public interface IDeathModule
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ namespace BaseGames.Enemies
|
|||||||
///
|
///
|
||||||
/// 实现注意:模块会经 [SerializeReference] 反序列化,构造函数不保证被调用。
|
/// 实现注意:模块会经 [SerializeReference] 反序列化,构造函数不保证被调用。
|
||||||
/// 不要在构造函数里预计算缓存或做参数校验——缓存用惰性属性,校验放 Build()。
|
/// 不要在构造函数里预计算缓存或做参数校验——缓存用惰性属性,校验放 Build()。
|
||||||
|
///
|
||||||
|
/// 为何本层只需单阶段 Build(而未发现层要拆 Declare/Link):骨架只把边**指向**
|
||||||
|
/// EntryState,从不在本模块自己的状态上挂边,所以 Build 内声明的边不会被外部抢占。
|
||||||
|
/// 将来若修改骨架、使其向交战态挂边,必须先回来重新审视本契约。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IEngagementModule
|
public interface IEngagementModule
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ namespace BaseGames.Enemies
|
|||||||
/// <summary>挂内部转换(如站立 ⇄ 巡逻的计时边)。没有内部边的模块留空实现。</summary>
|
/// <summary>挂内部转换(如站立 ⇄ 巡逻的计时边)。没有内部边的模块留空实现。</summary>
|
||||||
void Link(BrainBuilder b);
|
void Link(BrainBuilder b);
|
||||||
|
|
||||||
/// <summary>图入口(出生态)。</summary>
|
/// <summary>图入口(出生态)。注意与交战/死亡层的 EntryState 不同:
|
||||||
|
/// 本属性会成为整张图唯一的 BrainBuilder.Entry,而 EntryState 只是别处 To() 的目标。</summary>
|
||||||
string Entry { get; }
|
string Entry { get; }
|
||||||
|
|
||||||
/// <summary>脱战 / 警觉丢失后回到的态。必须属于 States。</summary>
|
/// <summary>脱战 / 警觉丢失后回到的态。必须属于 States。</summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user