merge: 角色身体碰撞体唯一权威源(IEnemyBody)
EnemyBase 持有 _bodyCollider 并暴露 Body;EnemyMovement 5 处探测与 EdgeSafeMargin 改走权威,删除重复 4 次的碰撞体解析与 GroundColliderOrSelf; 行为等价(两个敌人原本即回落到根碰撞体),EditMode 191/191 + PlayMode 崖边夹紧验证通过。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 14c6fa0cbd7b47a4289998755807c3ee
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -43,6 +43,11 @@ namespace BaseGames.Enemies
|
||||
[SerializeField] protected EnemyFeedback _feedback;
|
||||
[SerializeField] protected HurtBox _hurtBox;
|
||||
|
||||
[Header("身体碰撞体(唯一权威源)")]
|
||||
[Tooltip("用于身体几何(宽高/边缘/探测起点)的碰撞体。留空 = 使用本物体上的 Collider2D。" +
|
||||
"移动探测、导航内缩、冲刺终点等全部以此为准,不要在各处自行取碰撞体。")]
|
||||
[SerializeField] private Collider2D _bodyCollider;
|
||||
|
||||
[Header("区域(可选)")]
|
||||
[Tooltip("地图固定巡逻/追击区域;配置后 BD_ChasePlayer 以区域边界替代 MaxChaseDistance,BD_ReturnToHome 归位至区域中心。留空则沿用出生点 + MaxChaseDistance 旧逻辑。")]
|
||||
[SerializeField] private EnemyPatrolZone _patrolZone;
|
||||
@@ -69,6 +74,9 @@ namespace BaseGames.Enemies
|
||||
// 碰撞体缓存:Awake 时收集一次,避免 Die()/OnSpawn() 中频繁 GetComponentsInChildren 分配
|
||||
private Collider2D[] _colliders;
|
||||
|
||||
// 身体几何权威(懒创建;OnValidate 改引用后置空重建)
|
||||
private EnemyBody _body;
|
||||
|
||||
// ── 对象池支持 ─────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 本 GameObject 上的 PooledObject 组件(可选)。
|
||||
@@ -224,6 +232,8 @@ namespace BaseGames.Enemies
|
||||
/// <summary>攻击选择器(从已注册能力里筛出 category==Attack 的候选,Awake 时构建)。</summary>
|
||||
public Abilities.EnemyAttackSelector AttackSelector => _attackSelector;
|
||||
private Abilities.EnemyAttackSelector _attackSelector;
|
||||
/// <summary>身体碰撞体几何的唯一权威查询口。移动/导航/能力一律经此获取身体尺寸。</summary>
|
||||
public IEnemyBody Body => _body ??= new EnemyBody(gameObject, _bodyCollider);
|
||||
/// <summary>由 _onPlayerSpawned 事件缓存的玩家 Transform,供 BD 任务读取。</summary>
|
||||
public Transform PlayerTransform => _playerTransform;
|
||||
/// <summary>感知 Hub;供 BD 任务及 QuotaManager 暂停/恢复感知使用。</summary>
|
||||
@@ -543,6 +553,11 @@ namespace BaseGames.Enemies
|
||||
BuildAttackSelector();
|
||||
_colliders = GetComponentsInChildren<Collider2D>(true);
|
||||
|
||||
// 身体几何权威校验:解析不到碰撞体 → 显式报错(根因暴露,不静默兜底)
|
||||
if (Body.Collider == null)
|
||||
Debug.LogError($"[EnemyBase] {name} 找不到身体碰撞体:请在 Inspector 指定 _bodyCollider," +
|
||||
"或确保本物体上挂有 Collider2D。移动探测/导航内缩将不可用。", this);
|
||||
|
||||
// 收集配置型行为模块(零代码扩展点)
|
||||
_deathSequence = GetComponentInChildren<Behaviors.IEnemyDeathSequence>(true);
|
||||
GetComponentsInChildren(true, _spawnHandlers);
|
||||
@@ -757,6 +772,7 @@ namespace BaseGames.Enemies
|
||||
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
_body = null; // 身体碰撞体引用可能在 Inspector 被改,缓存无条件失效(与警告抑制无关)
|
||||
if (SuppressValidationWarnings) return;
|
||||
if (_statsSO == null)
|
||||
Debug.LogWarning($"[EnemyBase] {gameObject.name} 缺少 EnemyStatsSO 配置(运行时会 NullRef)。", this);
|
||||
@@ -764,6 +780,8 @@ namespace BaseGames.Enemies
|
||||
Debug.LogWarning($"[EnemyBase] {gameObject.name} 未绑定 EnemyStats 组件引用。", this);
|
||||
if (_animancer == null)
|
||||
Debug.LogWarning($"[EnemyBase] {gameObject.name} 未绑定 AnimancerComponent 引用。", this);
|
||||
if (_bodyCollider == null && GetComponent<Collider2D>() == null)
|
||||
Debug.LogWarning($"[EnemyBase] {gameObject.name} 找不到身体碰撞体(未指定 _bodyCollider 且本物体无 Collider2D)。", this);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 472dc3c92b5d4b244ab6e2e80adde30a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -18,6 +18,9 @@ namespace BaseGames.Enemies
|
||||
/// ⚠️ 使用 Rigidbody2D.velocity(Unity 2022 LTS)。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Rigidbody2D))]
|
||||
// 身体几何权威在 EnemyBase.Body 上,本组件的所有探测都依赖它;
|
||||
// 固化"必须与 EnemyBase 同物体"这一不变量,避免缺失时探测静默失效。
|
||||
[RequireComponent(typeof(EnemyBase))]
|
||||
public class EnemyMovement : MonoBehaviour, INavLinkHandler
|
||||
{
|
||||
[SerializeField] private EnemyStatsSO _config;
|
||||
@@ -42,8 +45,6 @@ namespace BaseGames.Enemies
|
||||
[SerializeField] private float _navJumpMaxHeight = 6f;
|
||||
[Tooltip("可处理的最大跳跃水平距离")]
|
||||
[SerializeField] private float _navJumpMaxDist = 10f;
|
||||
[Tooltip("用于确定射线起点宽度和底边的 Collider2D;留空则 Awake 时自动查找")]
|
||||
[SerializeField] private Collider2D _groundCheckCollider;
|
||||
[Tooltip("从碰撞体底边向下的射线检测距离")]
|
||||
[SerializeField] private float _groundCheckDist = 0.15f;
|
||||
[Tooltip("射线数量(1 = 仅中心,>1 时沿碰撞体底边均匀分布)")]
|
||||
@@ -67,6 +68,17 @@ namespace BaseGames.Enemies
|
||||
private int _facingDir = 1;
|
||||
private Coroutine _linkCoroutine;
|
||||
|
||||
// 身体几何权威(EnemyBase.Body)。懒获取以支持编辑器模式(Awake 未执行)下的 Gizmo/探测预览。
|
||||
private EnemyBase _enemy;
|
||||
private IEnemyBody Body
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_enemy == null) _enemy = GetComponent<EnemyBase>();
|
||||
return _enemy != null ? _enemy.Body : null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 转身状态 ────────────────────────────────────────────────────────
|
||||
private bool _isTurning;
|
||||
private int _pendingFacingDir; // 转身目标方向,转身完成后 ApplyFacingFlip 使用
|
||||
@@ -184,12 +196,10 @@ namespace BaseGames.Enemies
|
||||
|
||||
private Vector2 GetGroundRayOrigin(int index)
|
||||
{
|
||||
// 优先用序列化字段,编辑器模式下 Awake 未执行时也能直接 GetComponent
|
||||
var col = _groundCheckCollider != null ? _groundCheckCollider : GetComponent<Collider2D>();
|
||||
if (col == null)
|
||||
return (Vector2)transform.position;
|
||||
var body = Body;
|
||||
if (body == null || body.Collider == null) return (Vector2)transform.position;
|
||||
|
||||
Bounds b = col.bounds;
|
||||
Bounds b = body.Bounds;
|
||||
float x = _groundCheckCount <= 1
|
||||
? b.center.x
|
||||
: Mathf.Lerp(b.min.x, b.max.x, (float)index / (_groundCheckCount - 1));
|
||||
@@ -206,31 +216,23 @@ namespace BaseGames.Enemies
|
||||
return false;
|
||||
}
|
||||
|
||||
// 墙体射线起点:碰撞体朝向侧边缘中心高度
|
||||
// 墙体射线起点:身体朝向侧边缘、中心高度
|
||||
private Vector2 GetWallRayOrigin()
|
||||
{
|
||||
var col = _groundCheckCollider != null ? _groundCheckCollider : GetComponent<Collider2D>();
|
||||
if (col == null) return (Vector2)transform.position;
|
||||
Bounds b = col.bounds;
|
||||
float x = _facingDir >= 0 ? b.max.x : b.min.x;
|
||||
return new Vector2(x, b.center.y);
|
||||
var body = Body;
|
||||
if (body == null || body.Collider == null) return (Vector2)transform.position;
|
||||
return new Vector2(body.FrontEdgeX(_facingDir), body.Bounds.center.y);
|
||||
}
|
||||
|
||||
// 悬崖射线起点:碰撞体前下角再向前偏移 _ledgeCheckFwdOffset
|
||||
// 悬崖射线起点:身体前下角再向前偏移 _ledgeCheckFwdOffset
|
||||
private Vector2 GetLedgeRayOrigin()
|
||||
{
|
||||
var col = _groundCheckCollider != null ? _groundCheckCollider : GetComponent<Collider2D>();
|
||||
if (col == null) return (Vector2)transform.position;
|
||||
Bounds b = col.bounds;
|
||||
float x = _facingDir >= 0
|
||||
? b.max.x + _ledgeCheckFwdOffset
|
||||
: b.min.x - _ledgeCheckFwdOffset;
|
||||
return new Vector2(x, b.min.y);
|
||||
var body = Body;
|
||||
if (body == null || body.Collider == null) return (Vector2)transform.position;
|
||||
float sign = _facingDir >= 0 ? 1f : -1f;
|
||||
return new Vector2(body.FrontEdgeX(_facingDir) + sign * _ledgeCheckFwdOffset, body.Bounds.min.y);
|
||||
}
|
||||
|
||||
private Collider2D GroundColliderOrSelf()
|
||||
=> _groundCheckCollider != null ? _groundCheckCollider : GetComponent<Collider2D>();
|
||||
|
||||
/// <summary>
|
||||
/// 朝 <paramref name="dir"/> 前进是否会撞墙或让碰撞体前缘越过地形边缘(墙 或 悬崖)。
|
||||
/// 用碰撞体前缘(bounds.max.x / min.x)判定,天然考虑碰撞体宽度——
|
||||
@@ -242,37 +244,36 @@ namespace BaseGames.Enemies
|
||||
/// <summary>朝 <paramref name="dir"/> 前缘水平方向是否有墙。</summary>
|
||||
public bool WouldHitWallAhead(float dir)
|
||||
{
|
||||
var col = GroundColliderOrSelf();
|
||||
if (col == null || _wallCheckDist <= 0f) return false;
|
||||
Bounds b = col.bounds;
|
||||
float sign = Mathf.Sign(dir);
|
||||
float edgeX = sign >= 0f ? b.max.x : b.min.x;
|
||||
return Physics2D.Raycast(new Vector2(edgeX, b.center.y), new Vector2(sign, 0f), _wallCheckDist, _wallMask);
|
||||
var body = Body;
|
||||
if (body == null || body.Collider == null || _wallCheckDist <= 0f) return false;
|
||||
float sign = Mathf.Sign(dir);
|
||||
return Physics2D.Raycast(new Vector2(body.FrontEdgeX(dir), body.Bounds.center.y),
|
||||
new Vector2(sign, 0f), _wallCheckDist, _wallMask);
|
||||
}
|
||||
|
||||
/// <summary>朝 <paramref name="dir"/> 前缘外是否为悬崖(脚边地面缺失,会让碰撞体越出地形边缘)。</summary>
|
||||
public bool WouldFallAhead(float dir)
|
||||
{
|
||||
var col = GroundColliderOrSelf();
|
||||
if (col == null || _ledgeCheckDownDist <= 0f) return false;
|
||||
Bounds b = col.bounds;
|
||||
float sign = Mathf.Sign(dir);
|
||||
float edgeX = sign >= 0f ? b.max.x : b.min.x;
|
||||
return !Physics2D.Raycast(new Vector2(edgeX + sign * _ledgeCheckFwdOffset, b.min.y),
|
||||
Vector2.down, _ledgeCheckDownDist, _groundMask);
|
||||
var body = Body;
|
||||
if (body == null || body.Collider == null || _ledgeCheckDownDist <= 0f) return false;
|
||||
float sign = Mathf.Sign(dir);
|
||||
return !Physics2D.Raycast(
|
||||
new Vector2(body.FrontEdgeX(dir) + sign * _ledgeCheckFwdOffset, body.Bounds.min.y),
|
||||
Vector2.down, _ledgeCheckDownDist, _groundMask);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 身体中心到"能站住的平台边缘最近点"的水平安全余量 = 碰撞体水平半宽 + 悬崖检测前向偏移。
|
||||
/// WouldFallAhead 在"碰撞体前缘 + _ledgeCheckFwdOffset"处探地,故身体中心必须离平台边缘至少此距离,
|
||||
/// 否则前缘探出崖沿被夹停。供 nav 段内随机游走把随机点从段两端各内缩此值,避免随机到身体站不住的崖沿点。
|
||||
/// 身体中心到"能站住的平台边缘最近点"的水平安全余量 = 身体半宽 + 悬崖探测前向偏移。
|
||||
/// 半宽取自唯一权威 <see cref="EnemyBase.Body"/>;探测偏移是本移动层自己的射线调参。
|
||||
/// WouldFallAhead 在"身体前缘 + _ledgeCheckFwdOffset"处探地,故身体中心必须离平台边缘至少此距离,
|
||||
/// 否则前缘探出崖沿被夹停。供 nav 段内随机游走/目标点吸附把点从段两端内缩此值。
|
||||
/// </summary>
|
||||
public float EdgeSafeMargin
|
||||
{
|
||||
get
|
||||
{
|
||||
var col = GroundColliderOrSelf();
|
||||
float halfWidth = col != null ? col.bounds.extents.x : 0f;
|
||||
var body = Body;
|
||||
float halfWidth = body != null ? body.HalfWidth : 0f;
|
||||
return halfWidth + Mathf.Max(0f, _ledgeCheckFwdOffset);
|
||||
}
|
||||
}
|
||||
@@ -302,8 +303,6 @@ namespace BaseGames.Enemies
|
||||
Debug.LogError($"[EnemyMovement] '{name}' 启用了墙检测(_wallCheckDist>0)但 _wallMask 为 Nothing," +
|
||||
"撞墙判定将失效(会穿墙)。请在 Inspector 配置墙体层(推荐 Platform + Wall)。", this);
|
||||
_rb = GetComponent<Rigidbody2D>();
|
||||
if (_groundCheckCollider == null)
|
||||
_groundCheckCollider = GetComponent<Collider2D>();
|
||||
|
||||
// 从 Sprite 或 localScale 的初始状态推断朝向,并统一切换为 localScale 翻转。
|
||||
// 这样子对象(含 RaySensor2D)会随 localScale 正确翻转,不再依赖 flipX。
|
||||
@@ -331,8 +330,8 @@ namespace BaseGames.Enemies
|
||||
transform.localScale = new Vector3(signX, s.y, s.z);
|
||||
|
||||
// 将 Visual 子节点的 localPosition 对齐到 Collider2D offset,使视觉中心与碰撞体中心重合
|
||||
if (_visualRoot != null && _groundCheckCollider != null)
|
||||
_visualRoot.localPosition = _groundCheckCollider.offset;
|
||||
if (_visualRoot != null && Body != null && Body.Collider != null)
|
||||
_visualRoot.localPosition = Body.Collider.offset;
|
||||
|
||||
if (_enableTurnAnimation)
|
||||
{
|
||||
@@ -722,7 +721,7 @@ namespace BaseGames.Enemies
|
||||
}
|
||||
|
||||
// 2. 对齐 localPosition 到 Collider2D offset
|
||||
var col = _groundCheckCollider != null ? _groundCheckCollider : GetComponent<Collider2D>();
|
||||
var col = Body != null ? Body.Collider : GetComponent<Collider2D>();
|
||||
if (col != null)
|
||||
{
|
||||
UnityEditor.Undo.RecordObject(visual, "Set Visual LocalPosition");
|
||||
|
||||
Reference in New Issue
Block a user