feat(enemy): 未发现层三模块(单一态/单向降级/定时交替)
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
using NUnit.Framework;
|
||||
using BaseGames.AI;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Tests.EditMode.AI
|
||||
{
|
||||
public class UnawareModuleTests
|
||||
{
|
||||
// 只跑未发现层自身(不挂骨架升级边),验证态、行为与内部边。
|
||||
static AiRuntime RunAlone(IUnawareModule m, FakeAiContext ctx)
|
||||
{
|
||||
var b = new BrainBuilder();
|
||||
m.Declare(b);
|
||||
m.Link(b);
|
||||
b.Entry(m.Entry);
|
||||
return new AiRuntime(b.Build(), ctx);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SinglePost_OneState_EntryEqualsRest()
|
||||
{
|
||||
var m = new SinglePost(LocomotionMode.Patrol);
|
||||
Assert.AreEqual(1, m.States.Count);
|
||||
Assert.AreEqual(m.Entry, m.Rest);
|
||||
var ctx = new FakeAiContext();
|
||||
RunAlone(m, ctx);
|
||||
Assert.AreEqual(LocomotionMode.Patrol, ctx.L.CurrentMode);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DisguiseThenPatrol_EntryIsDisguise_RestIsPatrol()
|
||||
{
|
||||
var m = new DisguiseThenPatrol();
|
||||
Assert.AreEqual(DisguiseThenPatrol.Disguise, m.Entry);
|
||||
Assert.AreEqual(DisguiseThenPatrol.Patrol, m.Rest);
|
||||
CollectionAssert.AreEquivalent(
|
||||
new[] { DisguiseThenPatrol.Disguise, DisguiseThenPatrol.Patrol }, m.States);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DisguiseThenPatrol_HasNoInternalEdges_DisguiseIsOneWay()
|
||||
{
|
||||
var ctx = new FakeAiContext();
|
||||
var rt = RunAlone(new DisguiseThenPatrol(), ctx);
|
||||
Assert.AreEqual(DisguiseThenPatrol.Disguise, rt.CurrentStateName);
|
||||
for (int i = 0; i < 20; i++) rt.Tick(1f);
|
||||
// 无内部边:伪装态自己不会跑掉,只能被骨架的升级边带走
|
||||
Assert.AreEqual(DisguiseThenPatrol.Disguise, rt.CurrentStateName);
|
||||
Assert.AreEqual(LocomotionMode.Idle, ctx.L.CurrentMode);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AlternatingIdlePatrol_SwitchesOnDwellTimers()
|
||||
{
|
||||
var m = new AlternatingIdlePatrol(idleDwell: 2f, patrolDwell: 3f);
|
||||
var ctx = new FakeAiContext();
|
||||
var rt = RunAlone(m, ctx);
|
||||
Assert.AreEqual(AlternatingIdlePatrol.Idle, rt.CurrentStateName);
|
||||
|
||||
rt.Tick(1f);
|
||||
Assert.AreEqual(AlternatingIdlePatrol.Idle, rt.CurrentStateName); // 未到 2s
|
||||
rt.Tick(1.5f);
|
||||
Assert.AreEqual(AlternatingIdlePatrol.Patrol, rt.CurrentStateName); // 累计 2.5s ≥ 2s
|
||||
Assert.AreEqual(LocomotionMode.Patrol, ctx.L.CurrentMode);
|
||||
|
||||
rt.Tick(3.5f);
|
||||
Assert.AreEqual(AlternatingIdlePatrol.Idle, rt.CurrentStateName); // ≥ 3s 回站立
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f24cb7c97b01d1e48bfad2dc149f8bdf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 897dfc19ae815c9428d471e354151724
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using BaseGames.AI;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>站立 ⇄ 巡逻定时交替,脱战回站立。内部计时边经 Link 挂载,优先级低于骨架升级边。</summary>
|
||||
[Serializable]
|
||||
public sealed class AlternatingIdlePatrol : IUnawareModule
|
||||
{
|
||||
public const string Idle = "Idle";
|
||||
public const string Patrol = "Patrol";
|
||||
static readonly string[] StatesArray = { Idle, Patrol };
|
||||
|
||||
[Tooltip("站立停留时长(秒),到点转巡逻")]
|
||||
[SerializeField, Min(0.1f)] float _idleDwell = 2f;
|
||||
[Tooltip("巡逻持续时长(秒),到点转站立")]
|
||||
[SerializeField, Min(0.1f)] float _patrolDwell = 4f;
|
||||
|
||||
public AlternatingIdlePatrol() { }
|
||||
public AlternatingIdlePatrol(float idleDwell, float patrolDwell)
|
||||
{ _idleDwell = idleDwell; _patrolDwell = patrolDwell; }
|
||||
|
||||
public string Entry => Idle;
|
||||
public string Rest => Idle;
|
||||
public IReadOnlyList<string> States => StatesArray;
|
||||
|
||||
public void Declare(BrainBuilder b)
|
||||
{
|
||||
AiStateFragments.Locomotion(b, Idle, LocomotionMode.Idle);
|
||||
AiStateFragments.Locomotion(b, Patrol, LocomotionMode.Patrol);
|
||||
}
|
||||
|
||||
public void Link(BrainBuilder b)
|
||||
{
|
||||
b.State(Idle) .To(Patrol).After(_idleDwell);
|
||||
b.State(Patrol).To(Idle) .After(_patrolDwell);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6e5fc2afebbf144b95b40b1f01a038c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using BaseGames.AI;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 单向降级:出生时伪装静止,一旦交战过就只回巡逻,伪装态不可逆。
|
||||
/// 刻意不声明两态之间的内部边——伪装只在出生那一次可达。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class DisguiseThenPatrol : IUnawareModule
|
||||
{
|
||||
public const string Disguise = "Disguise";
|
||||
public const string Patrol = "Patrol";
|
||||
static readonly string[] StatesArray = { Disguise, Patrol };
|
||||
|
||||
[Tooltip("出生伪装态的移动模式(通常 Idle)")]
|
||||
[SerializeField] LocomotionMode _disguiseMode = LocomotionMode.Idle;
|
||||
[Tooltip("脱战后常态的移动模式(通常 Patrol)")]
|
||||
[SerializeField] LocomotionMode _patrolMode = LocomotionMode.Patrol;
|
||||
|
||||
public DisguiseThenPatrol() { }
|
||||
public DisguiseThenPatrol(LocomotionMode disguiseMode, LocomotionMode patrolMode)
|
||||
{ _disguiseMode = disguiseMode; _patrolMode = patrolMode; }
|
||||
|
||||
public string Entry => Disguise;
|
||||
public string Rest => Patrol;
|
||||
public IReadOnlyList<string> States => StatesArray;
|
||||
|
||||
public void Declare(BrainBuilder b)
|
||||
{
|
||||
AiStateFragments.Locomotion(b, Disguise, _disguiseMode);
|
||||
AiStateFragments.Locomotion(b, Patrol, _patrolMode);
|
||||
}
|
||||
|
||||
public void Link(BrainBuilder b) { } // 无内部边:伪装态单向不可逆
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92aa888ba3d46244bb0592cd8fd2860e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using BaseGames.AI;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>单一未发现态:站桩(Idle)或单一巡逻(Patrol)。出生态与脱战态是同一个。</summary>
|
||||
[Serializable]
|
||||
public sealed class SinglePost : IUnawareModule
|
||||
{
|
||||
public const string Post = "Post";
|
||||
static readonly string[] StatesArray = { Post };
|
||||
|
||||
[Tooltip("未发现时的移动模式:Idle=站着不动,Patrol=按配置策略游走")]
|
||||
[SerializeField] LocomotionMode _mode = LocomotionMode.Patrol;
|
||||
|
||||
public SinglePost() { }
|
||||
public SinglePost(LocomotionMode mode) { _mode = mode; }
|
||||
|
||||
public string Entry => Post;
|
||||
public string Rest => Post;
|
||||
public IReadOnlyList<string> States => StatesArray;
|
||||
|
||||
public void Declare(BrainBuilder b) => AiStateFragments.Locomotion(b, Post, _mode);
|
||||
public void Link(BrainBuilder b) { } // 单态,无内部边
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50f6c02637b111f44bbe3eea938871be
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user