Files
zeling_v2/Assets/_Game/Scripts/Player/PlayerWallDetector.cs
2026-05-19 11:50:21 +08:00

122 lines
5.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 &lt; -0.5
/// direction = -1 检查左侧接触法线指向右normal.x &gt; +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);
}
}
}