From 5cfacf199a556772a79bed9df520ed33af6c70de Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 28 Jul 2026 11:01:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(enemy):=20=E6=96=B0=E5=A2=9E=20StallDetect?= =?UTF-8?q?or=20=E6=8E=A8=E8=BF=9B=E5=81=9C=E6=BB=9E=E6=A3=80=E6=B5=8B(+8?= =?UTF-8?q?=20=E5=8D=95=E6=B5=8B),=E4=B8=BA=E6=94=B6=E6=95=9B=E4=B8=89?= =?UTF-8?q?=E5=A4=84=E9=87=8D=E5=A4=8D=E5=AE=9E=E7=8E=B0=E5=81=9A=E5=87=86?= =?UTF-8?q?=E5=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EditMode/Enemies/StallDetectorTests.cs | 83 +++++++++++++++++++ Assets/_Game/Scripts/Enemies/StallDetector.cs | 56 +++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 Assets/Tests/EditMode/Enemies/StallDetectorTests.cs create mode 100644 Assets/_Game/Scripts/Enemies/StallDetector.cs diff --git a/Assets/Tests/EditMode/Enemies/StallDetectorTests.cs b/Assets/Tests/EditMode/Enemies/StallDetectorTests.cs new file mode 100644 index 00000000..e83e9ab7 --- /dev/null +++ b/Assets/Tests/EditMode/Enemies/StallDetectorTests.cs @@ -0,0 +1,83 @@ +using NUnit.Framework; +using BaseGames.Enemies; + +namespace BaseGames.Tests.EditMode.Enemies +{ + /// + /// StallDetector:窗口内净位移不足即判停滞。 + /// 该逻辑原本在 3 处各写一遍(巡逻 Pace / 导航层 / 冲锋),此处集中钉住行为。 + /// + public class StallDetectorTests + { + const float Window = 0.5f, MinProgress = 0.05f; + + [Test] + public void BeforeWindowElapses_NeverStalled() + { + var d = new StallDetector(Window, MinProgress); + d.Reset(0f); + Assert.IsFalse(d.Tick(0f, 0.1f)); // 完全没动,但窗口未到 + Assert.IsFalse(d.Tick(0f, 0.1f)); + } + + [Test] + public void NoProgressOverWindow_Stalled() + { + var d = new StallDetector(Window, MinProgress); + d.Reset(0f); + Assert.IsFalse(d.Tick(0f, 0.4f)); + Assert.IsTrue (d.Tick(0f, 0.2f)); // 累计 0.6s ≥ 窗口,净位移 0 + } + + [Test] + public void EnoughProgressOverWindow_NotStalled() + { + var d = new StallDetector(Window, MinProgress); + d.Reset(0f); + Assert.IsFalse(d.Tick(1f, 0.6f)); // 位移 1m 远超阈值 + } + + [Test] + public void ProgressJustBelowThreshold_Stalled() + { + var d = new StallDetector(Window, MinProgress); + d.Reset(0f); + Assert.IsTrue(d.Tick(0.04f, 0.6f)); // 0.04 < 0.05 + } + + [Test] + public void ProgressCountsBothDirections() + { + var d = new StallDetector(Window, MinProgress); + d.Reset(0f); + Assert.IsFalse(d.Tick(-1f, 0.6f)); // 向左移动同样算推进 + } + + [Test] + public void AfterStalled_WindowRestartsFromCurrentPosition() + { + var d = new StallDetector(Window, MinProgress); + d.Reset(0f); + Assert.IsTrue (d.Tick(0f, 0.6f)); // 第一次判停滞 + Assert.IsFalse(d.Tick(0f, 0.1f)); // 窗口已重开,未到期 + Assert.IsTrue (d.Tick(0f, 0.6f)); // 再次到期且仍无推进 + } + + [Test] + public void Reset_ClearsAccumulatedTime() + { + var d = new StallDetector(Window, MinProgress); + d.Reset(0f); + d.Tick(0f, 0.4f); + d.Reset(0f); // 重开窗口 + Assert.IsFalse(d.Tick(0f, 0.2f)); // 只累计了 0.2s + } + + [Test] + public void WithoutReset_FirstTickInitializes() + { + var d = new StallDetector(Window, MinProgress); + Assert.IsFalse(d.Tick(5f, 1f)); // 未 Reset 时首帧只做初始化,不误判 + } + } +} diff --git a/Assets/_Game/Scripts/Enemies/StallDetector.cs b/Assets/_Game/Scripts/Enemies/StallDetector.cs new file mode 100644 index 00000000..32d740a2 --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/StallDetector.cs @@ -0,0 +1,56 @@ +using UnityEngine; + +namespace BaseGames.Enemies +{ + /// + /// 推进停滞检测:滑动窗口内的**净水平位移**不足阈值即判为停滞。 + /// 用于"命令了移动却推不动"的兜底(撞上未纳入检测层的碰撞体、贴墙微抖等)。 + /// + /// 用净位移而非逐帧差值,避免"贴墙推进-被弹回"的微抖每帧重置计数。 + /// 调用方须以字段持有本结构(struct 的方法会就地修改字段); + /// 开始移动 / 更换目标后调用 重开窗口。 + /// + public struct StallDetector + { + private readonly float _windowSeconds; + private readonly float _minProgress; + private float _timer; + private float _startX; + private bool _initialized; + + /// 判定窗口时长(秒)。 + /// 窗口内所需的最小净位移(米)。 + public StallDetector(float windowSeconds, float minProgress) + { + _windowSeconds = windowSeconds; + _minProgress = minProgress; + _timer = 0f; + _startX = 0f; + _initialized = false; + } + + /// 以当前位置重开窗口。 + public void Reset(float currentX) + { + _startX = currentX; + _timer = 0f; + _initialized = true; + } + + /// + /// 推进一帧。返回 true 表示本窗口内推进不足(停滞);返回 true 后窗口自动以当前位置重开。 + /// + public bool Tick(float currentX, float deltaTime) + { + if (!_initialized) { Reset(currentX); return false; } + + _timer += deltaTime; + if (_timer < _windowSeconds) return false; + + bool stalled = Mathf.Abs(currentX - _startX) < _minProgress; + _startX = currentX; + _timer = 0f; + return stalled; + } + } +}