using UnityEngine; namespace BaseGames.Player { /// /// 独立墙壁检测组件(架构 05_PlayerModule §13)。 /// ⚠️ 不嵌入 PlayerMovement,以保持单一职责。 /// 每侧发两根射线(Top + Bottom),两根均命中才视为接触墙壁(防卡角误判)。 /// WallSlideState / WallJumpState 通过 PlayerController.WallDetector 访问。 /// [RequireComponent(typeof(PlayerMovement))] public class PlayerWallDetector : MonoBehaviour { [SerializeField] private PlayerMovementConfigSO _config; [Header("墙壁 Layer(默认使用 \"Wall\" + \"Ground\")")] [SerializeField] private LayerMask _wallLayer; /// 当前是否正在触碰墙壁。 public bool IsTouchingWall { get; private set; } /// 触碰到的墙壁方向:+1 = 右墙,-1 = 左墙,0 = 无墙。 public int WallDirection { get; private set; } // 每侧"任意一根射线命中 OR 物理接触点命中"的结果,用于防止下落时卡在矮墙边角 private bool _anyRightContact; private bool _anyLeftContact; // 物理接触点缓冲区(避免每帧 GC) private Rigidbody2D _rb; private static readonly ContactPoint2D[] _contactBuffer = new ContactPoint2D[8]; /// /// 指定方向上是否存在墙壁接触(射线命中 OR 物理接触点命中,任一为 true)。 /// 用于在 FallState / JumpState 中防止角色卡在矮墙边角:
/// • 射线检测覆盖"检测点略高于墙顶、仅一根射线命中"的情况;
/// • 物理接触点覆盖"两根射线均高于墙顶,但碰撞体底角已卡在墙顶角"的极端情况。 ///
public bool HasPartialContact(int direction) => direction > 0 ? _anyRightContact : _anyLeftContact; private void Awake() { Debug.Assert(_config != null, "[PlayerWallDetector] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this); _rb = GetComponent(); } private void FixedUpdate() { bool rightWall = CheckSide(Vector2.right, out bool anyRightRay); bool leftWall = CheckSide(Vector2.left, out bool anyLeftRay); // 物理接触点兜底:两根射线都在墙顶以上时,仍可通过接触点检测到卡角 bool physRight = CheckPhysicalContact(1); bool physLeft = CheckPhysicalContact(-1); _anyRightContact = anyRightRay || physRight; _anyLeftContact = anyLeftRay || physLeft; IsTouchingWall = rightWall || leftWall; WallDirection = rightWall ? 1 : (leftWall ? -1 : 0); } /// /// 通过物理接触点判断指定方向是否有墙壁(法线 X 分量超过 0.5 的水平接触)。 /// direction = +1 检查右侧(接触法线指向左,normal.x < -0.5), /// direction = -1 检查左侧(接触法线指向右,normal.x > +0.5)。 /// private bool CheckPhysicalContact(int direction) { if (_rb == null) return false; LayerMask mask = _wallLayer != 0 ? _wallLayer : LayerMask.GetMask("Wall", "Ground"); var filter = new ContactFilter2D(); filter.SetLayerMask(mask); filter.useTriggers = false; int count = _rb.GetContacts(filter, _contactBuffer); for (int i = 0; i < count; i++) { float nx = _contactBuffer[i].normal.x; // 右侧墙接触:法线指向左(nx < -0.5);左侧墙接触:法线指向右(nx > +0.5) if (direction > 0 && nx < -0.5f) return true; if (direction < 0 && nx > 0.5f) return true; } return false; } /// /// 每侧发两根射线(TopRay + BottomRay),两根均命中才返回 true。 /// 在任意一根命中时为 true(用于防卡角判断)。 /// private bool CheckSide(Vector2 dir, out bool anyContact) { Vector2 center = transform.position; float len = _config.WallRayLength; float oy = _config.WallRayOffsetY; int layer = _wallLayer != 0 ? (int)_wallLayer : LayerMask.GetMask("Wall", "Ground"); bool top = Physics2D.Raycast(center + Vector2.up * oy, dir, len, layer); bool bot = Physics2D.Raycast(center + Vector2.down * oy, dir, len, layer); anyContact = top || bot; return top && bot; } private void OnDrawGizmosSelected() { if (_config == null) return; float len = _config.WallRayLength; float oy = _config.WallRayOffsetY; Vector2 center = transform.position; Gizmos.color = IsTouchingWall ? Color.red : Color.cyan; // 右侧两根射线 Gizmos.DrawRay(center + Vector2.up * oy, Vector2.right * len); Gizmos.DrawRay(center + Vector2.down * oy, Vector2.right * len); // 左侧两根射线 Gizmos.DrawRay(center + Vector2.up * oy, Vector2.left * len); Gizmos.DrawRay(center + Vector2.down * oy, Vector2.left * len); } } }