feat(enemy): 新增 IEnemyBody 身体几何权威接口与 EnemyBody 实现(+14 纯函数单测)
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
using NUnit.Framework;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Tests.EditMode.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// EnemyBody 方向相关纯函数测试(无场景依赖)。
|
||||
/// 这三个函数的左右符号最易写反,故对两个方向与边界相等情形都做断言。
|
||||
/// </summary>
|
||||
public class EnemyBodyTests
|
||||
{
|
||||
// 身体占据 [2, 4],中心 3,半宽 1
|
||||
const float MinX = 2f, MaxX = 4f;
|
||||
|
||||
[Test]
|
||||
public void FrontEdge_Right_IsMaxX()
|
||||
=> Assert.AreEqual(MaxX, EnemyBody.FrontEdge(MinX, MaxX, 1f));
|
||||
|
||||
[Test]
|
||||
public void FrontEdge_Left_IsMinX()
|
||||
=> Assert.AreEqual(MinX, EnemyBody.FrontEdge(MinX, MaxX, -1f));
|
||||
|
||||
[Test]
|
||||
public void FrontEdge_ZeroDir_TreatedAsRight()
|
||||
=> Assert.AreEqual(MaxX, EnemyBody.FrontEdge(MinX, MaxX, 0f));
|
||||
|
||||
[Test]
|
||||
public void RearEdge_Right_IsMinX()
|
||||
=> Assert.AreEqual(MinX, EnemyBody.RearEdge(MinX, MaxX, 1f));
|
||||
|
||||
[Test]
|
||||
public void RearEdge_Left_IsMaxX()
|
||||
=> Assert.AreEqual(MaxX, EnemyBody.RearEdge(MinX, MaxX, -1f));
|
||||
|
||||
[Test]
|
||||
public void RearEdge_ZeroDir_TreatedAsRight()
|
||||
=> Assert.AreEqual(MinX, EnemyBody.RearEdge(MinX, MaxX, 0f));
|
||||
|
||||
// ── BodyPassed:向右 ─────────────────────────────────────────────
|
||||
[Test]
|
||||
public void BodyPassed_Right_NotReached_False()
|
||||
=> Assert.IsFalse(EnemyBody.BodyPassed(MinX, MaxX, 5f, 1f)); // 目标在身体右侧外
|
||||
|
||||
[Test]
|
||||
public void BodyPassed_Right_OverlappingTarget_False()
|
||||
=> Assert.IsFalse(EnemyBody.BodyPassed(MinX, MaxX, 3f, 1f)); // 目标仍在身体内,未整体越过
|
||||
|
||||
[Test]
|
||||
public void BodyPassed_Right_ExactlyAtRearEdge_True()
|
||||
=> Assert.IsTrue(EnemyBody.BodyPassed(MinX, MaxX, 2f, 1f)); // 后缘恰好等于目标 = 已越过
|
||||
|
||||
[Test]
|
||||
public void BodyPassed_Right_FullyPassed_True()
|
||||
=> Assert.IsTrue(EnemyBody.BodyPassed(MinX, MaxX, 1f, 1f));
|
||||
|
||||
// ── BodyPassed:向左 ─────────────────────────────────────────────
|
||||
[Test]
|
||||
public void BodyPassed_Left_NotReached_False()
|
||||
=> Assert.IsFalse(EnemyBody.BodyPassed(MinX, MaxX, 1f, -1f)); // 目标在身体左侧外
|
||||
|
||||
[Test]
|
||||
public void BodyPassed_Left_OverlappingTarget_False()
|
||||
=> Assert.IsFalse(EnemyBody.BodyPassed(MinX, MaxX, 3f, -1f));
|
||||
|
||||
[Test]
|
||||
public void BodyPassed_Left_ExactlyAtRearEdge_True()
|
||||
=> Assert.IsTrue(EnemyBody.BodyPassed(MinX, MaxX, 4f, -1f)); // 后缘(max)恰好等于目标
|
||||
|
||||
[Test]
|
||||
public void BodyPassed_Left_FullyPassed_True()
|
||||
=> Assert.IsTrue(EnemyBody.BodyPassed(MinX, MaxX, 5f, -1f));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色身体碰撞体的唯一权威查询口。
|
||||
/// 几何实时取自碰撞体 bounds(随位置/动画变化,不缓存数值),仅缓存碰撞体引用。
|
||||
/// 唯一数据源见 <see cref="EnemyBase.Body"/>——其它组件不得自行解析身体碰撞体。
|
||||
/// </summary>
|
||||
public interface IEnemyBody
|
||||
{
|
||||
Collider2D Collider { get; }
|
||||
/// <summary>世界空间包围盒;实时查询。</summary>
|
||||
Bounds Bounds { get; }
|
||||
float Width { get; }
|
||||
float Height { get; }
|
||||
float HalfWidth { get; }
|
||||
float HalfHeight { get; }
|
||||
Vector2 Center { get; }
|
||||
/// <summary>脚底中点 (center.x, min.y)。</summary>
|
||||
Vector2 FootPoint { get; }
|
||||
|
||||
/// <summary>朝 <paramref name="dir"/> 的前缘 X(dir≥0 取 max.x,否则 min.x)。</summary>
|
||||
float FrontEdgeX(float dir);
|
||||
/// <summary>朝 <paramref name="dir"/> 的后缘 X(前缘的相反侧)。</summary>
|
||||
float RearEdgeX(float dir);
|
||||
/// <summary>
|
||||
/// 身体是否已**整体**越过世界 X 坐标:
|
||||
/// dir≥0 时后缘(min.x) ≥ worldX;dir<0 时后缘(max.x) ≤ worldX。
|
||||
/// </summary>
|
||||
bool HasPassed(float worldX, float dir);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IEnemyBody"/> 的实现(纯 C# 类,非 MonoBehaviour)。
|
||||
/// 懒解析碰撞体 → 编辑器模式(Gizmo / OnValidate,Awake 未执行)同样可用。
|
||||
/// </summary>
|
||||
public sealed class EnemyBody : IEnemyBody
|
||||
{
|
||||
private readonly GameObject _owner;
|
||||
private readonly Collider2D _explicitCollider;
|
||||
private Collider2D _resolved;
|
||||
|
||||
/// <param name="owner">宿主 GameObject(未显式指定碰撞体时从其上取 Collider2D)。</param>
|
||||
/// <param name="explicitCollider">显式指定的身体碰撞体;可为 null。</param>
|
||||
public EnemyBody(GameObject owner, Collider2D explicitCollider)
|
||||
{
|
||||
_owner = owner;
|
||||
_explicitCollider = explicitCollider;
|
||||
}
|
||||
|
||||
public Collider2D Collider
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_explicitCollider != null) return _explicitCollider;
|
||||
if (_resolved == null && _owner != null) _resolved = _owner.GetComponent<Collider2D>();
|
||||
return _resolved;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 碰撞体缺失时返回以宿主位置为中心的零尺寸包围盒。
|
||||
/// 该错误已由 <see cref="EnemyBase"/> 在 Awake/OnValidate 显式报出,此处不重复每帧刷屏。
|
||||
/// </summary>
|
||||
public Bounds Bounds
|
||||
{
|
||||
get
|
||||
{
|
||||
var col = Collider;
|
||||
if (col != null) return col.bounds;
|
||||
return new Bounds(_owner != null ? _owner.transform.position : Vector3.zero, Vector3.zero);
|
||||
}
|
||||
}
|
||||
|
||||
public float Width => Bounds.size.x;
|
||||
public float Height => Bounds.size.y;
|
||||
public float HalfWidth => Bounds.extents.x;
|
||||
public float HalfHeight => Bounds.extents.y;
|
||||
public Vector2 Center => Bounds.center;
|
||||
public Vector2 FootPoint { get { var b = Bounds; return new Vector2(b.center.x, b.min.y); } }
|
||||
|
||||
public float FrontEdgeX(float dir) { var b = Bounds; return FrontEdge(b.min.x, b.max.x, dir); }
|
||||
public float RearEdgeX (float dir) { var b = Bounds; return RearEdge (b.min.x, b.max.x, dir); }
|
||||
public bool HasPassed(float worldX, float dir)
|
||||
{
|
||||
var b = Bounds;
|
||||
return BodyPassed(b.min.x, b.max.x, worldX, dir);
|
||||
}
|
||||
|
||||
// ── 方向纯函数(static,便于无场景依赖单测;符号最易写反,集中于此)──────
|
||||
public static float FrontEdge(float minX, float maxX, float dir) => dir >= 0f ? maxX : minX;
|
||||
public static float RearEdge (float minX, float maxX, float dir) => dir >= 0f ? minX : maxX;
|
||||
public static bool BodyPassed(float minX, float maxX, float worldX, float dir)
|
||||
=> dir >= 0f ? minX >= worldX : maxX <= worldX;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user