122 lines
5.4 KiB
C#
122 lines
5.4 KiB
C#
using UnityEngine;
|
||
|
||
namespace BaseGames.Player
|
||
{
|
||
/// <summary>
|
||
/// 独立墙壁检测组件(架构 05_PlayerModule §13)。
|
||
/// ⚠️ 不嵌入 PlayerMovement,以保持单一职责。
|
||
/// 每侧发两根射线(Top + Bottom),两根均命中才视为接触墙壁(防卡角误判)。
|
||
/// WallSlideState / WallJumpState 通过 PlayerController.WallDetector 访问。
|
||
/// </summary>
|
||
[RequireComponent(typeof(PlayerMovement))]
|
||
public class PlayerWallDetector : MonoBehaviour
|
||
{
|
||
[SerializeField] private PlayerMovementConfigSO _config;
|
||
|
||
[Header("墙壁 Layer(默认使用 \"Wall\" + \"Ground\")")]
|
||
[SerializeField] private LayerMask _wallLayer;
|
||
|
||
/// <summary>当前是否正在触碰墙壁。</summary>
|
||
public bool IsTouchingWall { get; private set; }
|
||
|
||
/// <summary>触碰到的墙壁方向:+1 = 右墙,-1 = 左墙,0 = 无墙。</summary>
|
||
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];
|
||
|
||
/// <summary>
|
||
/// 指定方向上是否存在墙壁接触(射线命中 OR 物理接触点命中,任一为 true)。
|
||
/// 用于在 FallState / JumpState 中防止角色卡在矮墙边角:<br/>
|
||
/// • 射线检测覆盖"检测点略高于墙顶、仅一根射线命中"的情况;<br/>
|
||
/// • 物理接触点覆盖"两根射线均高于墙顶,但碰撞体底角已卡在墙顶角"的极端情况。
|
||
/// </summary>
|
||
public bool HasPartialContact(int direction) =>
|
||
direction > 0 ? _anyRightContact : _anyLeftContact;
|
||
|
||
private void Awake()
|
||
{
|
||
Debug.Assert(_config != null, "[PlayerWallDetector] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
|
||
_rb = GetComponent<Rigidbody2D>();
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 通过物理接触点判断指定方向是否有墙壁(法线 X 分量超过 0.5 的水平接触)。
|
||
/// direction = +1 检查右侧(接触法线指向左,normal.x < -0.5),
|
||
/// direction = -1 检查左侧(接触法线指向右,normal.x > +0.5)。
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 每侧发两根射线(TopRay + BottomRay),两根均命中才返回 true。
|
||
/// <paramref name="anyContact"/> 在任意一根命中时为 true(用于防卡角判断)。
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
}
|