From acfba66c5d7f56522dafd5feb8441ba5f08a0b3f Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 29 Jul 2026 12:19:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(enemy):=20=E6=9C=AA=E5=8F=91=E7=8E=B0?= =?UTF-8?q?=E5=B1=82=E4=B8=89=E6=A8=A1=E5=9D=97=EF=BC=88=E5=8D=95=E4=B8=80?= =?UTF-8?q?=E6=80=81/=E5=8D=95=E5=90=91=E9=99=8D=E7=BA=A7/=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E4=BA=A4=E6=9B=BF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tests/EditMode/AI/UnawareModuleTests.cs | 70 +++++++++++++++++++ .../EditMode/AI/UnawareModuleTests.cs.meta | 11 +++ .../Enemies/AIBrain/Modules/Unaware.meta | 8 +++ .../Modules/Unaware/AlternatingIdlePatrol.cs | 41 +++++++++++ .../Unaware/AlternatingIdlePatrol.cs.meta | 11 +++ .../Modules/Unaware/DisguiseThenPatrol.cs | 40 +++++++++++ .../Unaware/DisguiseThenPatrol.cs.meta | 11 +++ .../AIBrain/Modules/Unaware/SinglePost.cs | 28 ++++++++ .../Modules/Unaware/SinglePost.cs.meta | 11 +++ 9 files changed, 231 insertions(+) create mode 100644 Assets/Tests/EditMode/AI/UnawareModuleTests.cs create mode 100644 Assets/Tests/EditMode/AI/UnawareModuleTests.cs.meta create mode 100644 Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware.meta create mode 100644 Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/AlternatingIdlePatrol.cs create mode 100644 Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/AlternatingIdlePatrol.cs.meta create mode 100644 Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/DisguiseThenPatrol.cs create mode 100644 Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/DisguiseThenPatrol.cs.meta create mode 100644 Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/SinglePost.cs create mode 100644 Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/SinglePost.cs.meta diff --git a/Assets/Tests/EditMode/AI/UnawareModuleTests.cs b/Assets/Tests/EditMode/AI/UnawareModuleTests.cs new file mode 100644 index 00000000..7356356a --- /dev/null +++ b/Assets/Tests/EditMode/AI/UnawareModuleTests.cs @@ -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 回站立 + } + } +} diff --git a/Assets/Tests/EditMode/AI/UnawareModuleTests.cs.meta b/Assets/Tests/EditMode/AI/UnawareModuleTests.cs.meta new file mode 100644 index 00000000..e5d3e00f --- /dev/null +++ b/Assets/Tests/EditMode/AI/UnawareModuleTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f24cb7c97b01d1e48bfad2dc149f8bdf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware.meta b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware.meta new file mode 100644 index 00000000..6db23741 --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 897dfc19ae815c9428d471e354151724 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/AlternatingIdlePatrol.cs b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/AlternatingIdlePatrol.cs new file mode 100644 index 00000000..8b853a81 --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/AlternatingIdlePatrol.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using BaseGames.AI; + +namespace BaseGames.Enemies +{ + /// 站立 ⇄ 巡逻定时交替,脱战回站立。内部计时边经 Link 挂载,优先级低于骨架升级边。 + [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 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); + } + } +} diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/AlternatingIdlePatrol.cs.meta b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/AlternatingIdlePatrol.cs.meta new file mode 100644 index 00000000..347b4835 --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/AlternatingIdlePatrol.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e6e5fc2afebbf144b95b40b1f01a038c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/DisguiseThenPatrol.cs b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/DisguiseThenPatrol.cs new file mode 100644 index 00000000..9978c8fa --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/DisguiseThenPatrol.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using BaseGames.AI; + +namespace BaseGames.Enemies +{ + /// + /// 单向降级:出生时伪装静止,一旦交战过就只回巡逻,伪装态不可逆。 + /// 刻意不声明两态之间的内部边——伪装只在出生那一次可达。 + /// + [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 States => StatesArray; + + public void Declare(BrainBuilder b) + { + AiStateFragments.Locomotion(b, Disguise, _disguiseMode); + AiStateFragments.Locomotion(b, Patrol, _patrolMode); + } + + public void Link(BrainBuilder b) { } // 无内部边:伪装态单向不可逆 + } +} diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/DisguiseThenPatrol.cs.meta b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/DisguiseThenPatrol.cs.meta new file mode 100644 index 00000000..0a3ba093 --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/DisguiseThenPatrol.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 92aa888ba3d46244bb0592cd8fd2860e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/SinglePost.cs b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/SinglePost.cs new file mode 100644 index 00000000..588ee897 --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/SinglePost.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using BaseGames.AI; + +namespace BaseGames.Enemies +{ + /// 单一未发现态:站桩(Idle)或单一巡逻(Patrol)。出生态与脱战态是同一个。 + [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 States => StatesArray; + + public void Declare(BrainBuilder b) => AiStateFragments.Locomotion(b, Post, _mode); + public void Link(BrainBuilder b) { } // 单态,无内部边 + } +} diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/SinglePost.cs.meta b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/SinglePost.cs.meta new file mode 100644 index 00000000..3be4a884 --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/SinglePost.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 50f6c02637b111f44bbe3eea938871be +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: