单向平台

This commit is contained in:
2026-05-21 11:44:01 +08:00
parent 94113a9c1b
commit 67e2d3a8ff
7 changed files with 107 additions and 14 deletions

View File

@@ -0,0 +1,12 @@
namespace BaseGames.Core
{
/// <summary>
/// 单向平台接口:挂载在平台上,允许角色从上方穿落。
/// 定义在 BaseGames.Core 以避免 Player ↔ World 程序集循环引用。
/// </summary>
public interface IDropThrough
{
/// <summary>触发穿落:临时禁用碰撞器,使角色向下穿过平台。</summary>
void TriggerDropThrough();
}
}

View File

@@ -95,7 +95,9 @@ namespace BaseGames.Editor
Transform groundCheckT = GetOrCreateChild(root.transform, "GroundCheck"); Transform groundCheckT = GetOrCreateChild(root.transform, "GroundCheck");
groundCheckT.localPosition = new Vector3(0f, -0.75f, 0f); groundCheckT.localPosition = new Vector3(0f, -0.75f, 0f);
AssignReference(playerMovement, "_groundCheck", groundCheckT, report); AssignReference(playerMovement, "_groundCheck", groundCheckT, report);
AssignLayerMask(playerMovement, "_groundLayer", "Platform", report); AssignLayerMask(playerMovement, "_groundLayer",
new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
report);
// ── SkillHitBox_Slot 子节点(技能 HitBox 实例化挂点)──────────────── // ── SkillHitBox_Slot 子节点(技能 HitBox 实例化挂点)────────────────
Transform skillSocketT = GetOrCreateChild(root.transform, "SkillHitBox_Slot"); Transform skillSocketT = GetOrCreateChild(root.transform, "SkillHitBox_Slot");
@@ -1001,6 +1003,31 @@ namespace BaseGames.Editor
so.ApplyModifiedPropertiesWithoutUndo(); so.ApplyModifiedPropertiesWithoutUndo();
} }
/// <summary>将多个 Layer 名称合并为一个 LayerMask 并写入 SerializedProperty。</summary>
private static void AssignLayerMask(Object target, string propName, string[] layerNames, List<string> report)
{
int mask = 0;
foreach (var name in layerNames)
{
int layer = LayerMask.NameToLayer(name);
if (layer == -1)
report.Add($"Layer '{name}' 不存在,已跳过({target.GetType().Name}.{propName})。");
else
mask |= 1 << layer;
}
if (mask == 0) return;
var so = new SerializedObject(target);
var sp = so.FindProperty(propName);
if (sp == null)
{
report.Add($"{target.GetType().Name}.{propName} 字段不存在,跳过 LayerMask 赋值。");
return;
}
sp.intValue = mask;
so.ApplyModifiedPropertiesWithoutUndo();
}
private static void AssignInt(Object target, string propName, int value) private static void AssignInt(Object target, string propName, int value)
{ {
var so = new SerializedObject(target); var so = new SerializedObject(target);

View File

@@ -1,3 +1,4 @@
using BaseGames.Core;
using UnityEngine; using UnityEngine;
namespace BaseGames.Player namespace BaseGames.Player
@@ -34,7 +35,8 @@ namespace BaseGames.Player
private int _facingDirection = 1; private int _facingDirection = 1;
private bool _cancelWindowOpen; private bool _cancelWindowOpen;
private SurfaceType _currentSurface = SurfaceType.Ground; private SurfaceType _currentSurface = SurfaceType.Ground;
private readonly Collider2D[] _groundBuffer = new Collider2D[4]; private readonly Collider2D[] _groundBuffer = new Collider2D[4];
private int _groundHitCount;
#if UNITY_EDITOR #if UNITY_EDITOR
// ── 运行时调试Inspector 中可见)─────────────────────────────── // ── 运行时调试Inspector 中可见)───────────────────────────────
@@ -43,6 +45,7 @@ namespace BaseGames.Player
[SerializeField] private float _dbg_VelocityX; [SerializeField] private float _dbg_VelocityX;
[SerializeField] private float _dbg_VelocityY; [SerializeField] private float _dbg_VelocityY;
[SerializeField] private bool _dbg_IsGrounded; [SerializeField] private bool _dbg_IsGrounded;
[SerializeField] private bool _dbg_OnOneWayPlatform;
[SerializeField] private bool _dbg_HasCoyoteTime; [SerializeField] private bool _dbg_HasCoyoteTime;
[SerializeField] private bool _dbg_IsWallLeft; [SerializeField] private bool _dbg_IsWallLeft;
[SerializeField] private bool _dbg_IsWallRight; [SerializeField] private bool _dbg_IsWallRight;
@@ -91,10 +94,11 @@ namespace BaseGames.Player
_coyoteTimer = Mathf.Max(0f, _coyoteTimer - Time.fixedDeltaTime); _coyoteTimer = Mathf.Max(0f, _coyoteTimer - Time.fixedDeltaTime);
#if UNITY_EDITOR #if UNITY_EDITOR
_dbg_VelocityX = _rb.velocity.x; _dbg_VelocityX = _rb.velocity.x;
_dbg_VelocityY = _rb.velocity.y; _dbg_VelocityY = _rb.velocity.y;
_dbg_IsGrounded = _isGrounded; _dbg_IsGrounded = _isGrounded;
_dbg_HasCoyoteTime = _coyoteTimer > 0f; _dbg_OnOneWayPlatform = _onOneWayPlatform;
_dbg_HasCoyoteTime = _coyoteTimer > 0f;
_dbg_IsWallLeft = _isWallLeft; _dbg_IsWallLeft = _isWallLeft;
_dbg_IsWallRight = _isWallRight; _dbg_IsWallRight = _isWallRight;
_dbg_CancelWindowOpen = _cancelWindowOpen; _dbg_CancelWindowOpen = _cancelWindowOpen;
@@ -243,19 +247,54 @@ namespace BaseGames.Player
_rb.velocity = new Vector2(_rb.velocity.x, 0f); _rb.velocity = new Vector2(_rb.velocity.x, 0f);
} }
/// <summary>单向平台穿透(输入下行 + 跳跃键时触发)。</summary> /// <summary>
public void DropThroughPlatform() { } /// 单向平台穿落(↓ + 跳跃键触发)。
/// 找到脚下的 <see cref="IDropThrough"/> 并临时禁用其碰撞器;
/// 同帧清除 IsGrounded / OnOneWayPlatform状态机无需等到下一物理帧即可转换到 FallState。
/// </summary>
public void DropThroughPlatform()
{
if (!_onOneWayPlatform) return;
for (int i = 0; i < _groundHitCount; i++)
{
if (_groundBuffer[i] != null &&
_groundBuffer[i].TryGetComponent<IDropThrough>(out var platform))
{
platform.TriggerDropThrough();
// 立即清除着地状态,让状态机本帧即可切换到 FallState
_isGrounded = false;
_onOneWayPlatform = false;
_coyoteTimer = 0f;
break;
}
}
}
// ── Physics 检测 ────────────────────────────────────────────────────── // ── Physics 检测 ──────────────────────────────────────────────────────
private void CheckGrounded() private void CheckGrounded()
{ {
if (_groundCheck == null) return; if (_groundCheck == null) return;
bool wasGrounded = _isGrounded; _groundHitCount = Physics2D.OverlapBoxNonAlloc(
_isGrounded = Physics2D.OverlapBoxNonAlloc(_groundCheck.position, _groundCheckSize, 0f, _groundBuffer, _groundLayer) > 0; _groundCheck.position, _groundCheckSize, 0f, _groundBuffer, _groundLayer);
_isGrounded = _groundHitCount > 0;
if (_isGrounded && !wasGrounded) // 检测是否站在单向平台(含 IDropThrough 组件的碰撞体)
_coyoteTimer = _config.CoyoteTime; _onOneWayPlatform = false;
for (int i = 0; i < _groundHitCount; i++)
{
if (_groundBuffer[i] != null &&
_groundBuffer[i].TryGetComponent<IDropThrough>(out _))
{
_onOneWayPlatform = true;
break;
}
}
_currentSurface = (_isGrounded && _onOneWayPlatform)
? SurfaceType.OneWayPlatform
: SurfaceType.Ground;
} }
private void CheckWalls() private void CheckWalls()

View File

@@ -26,6 +26,13 @@ namespace BaseGames.Player.States
_owner.TransitionTo(_owner.GetState<FallState>()); _owner.TransitionTo(_owner.GetState<FallState>());
return; return;
} }
// 单向平台穿落:↓ + 跳跃键,优先于普通跳跃,避免误消耗跳跃缓冲
if (Move.OnOneWayPlatform && Input.MoveInput.y < -0.5f && Buffer.ConsumeJump())
{
Move.DropThroughPlatform();
_owner.TransitionTo(_owner.GetState<FallState>());
return;
}
if (Buffer.ConsumeJump()) if (Buffer.ConsumeJump())
{ {
_owner.TransitionTo(_owner.GetState<JumpState>()); _owner.TransitionTo(_owner.GetState<JumpState>());

View File

@@ -25,6 +25,13 @@ namespace BaseGames.Player.States
_owner.TransitionTo(_owner.GetState<FallState>()); _owner.TransitionTo(_owner.GetState<FallState>());
return; return;
} }
// 单向平台穿落:↓ + 跳跃键,优先于普通跳跃
if (Move.OnOneWayPlatform && Input.MoveInput.y < -0.5f && Buffer.ConsumeJump())
{
Move.DropThroughPlatform();
_owner.TransitionTo(_owner.GetState<FallState>());
return;
}
if (Buffer.ConsumeJump()) if (Buffer.ConsumeJump())
{ {
_owner.TransitionTo(_owner.GetState<JumpState>()); _owner.TransitionTo(_owner.GetState<JumpState>());

View File

@@ -1,3 +1,4 @@
using BaseGames.Core;
using UnityEngine; using UnityEngine;
namespace BaseGames.World namespace BaseGames.World
@@ -10,7 +11,7 @@ namespace BaseGames.World
/// </summary> /// </summary>
[RequireComponent(typeof(Collider2D))] [RequireComponent(typeof(Collider2D))]
[RequireComponent(typeof(PlatformEffector2D))] [RequireComponent(typeof(PlatformEffector2D))]
public class PhantomPlate : MonoBehaviour public class PhantomPlate : MonoBehaviour, IDropThrough
{ {
[Header("下蹲跌落")] [Header("下蹲跌落")]
[Tooltip("按住下方向 + 跳跃时临时禁用碰撞器,允许玩家向下穿过平台")] [Tooltip("按住下方向 + 跳跃时临时禁用碰撞器,允许玩家向下穿过平台")]

View File

@@ -45,4 +45,4 @@ Physics2DSettings:
m_ReuseCollisionCallbacks: 1 m_ReuseCollisionCallbacks: 1
m_AutoSyncTransforms: 0 m_AutoSyncTransforms: 0
m_GizmoOptions: 10 m_GizmoOptions: 10
m_LayerCollisionMatrix: ffffffffffffffffffffffffbffffffffffffffffffffffff7ebfffffff0ffff7ffdffff7fdeffff3fffffff7fbfffffbfffffffffbdffffffd7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff m_LayerCollisionMatrix: ffffffffffffffffffffffffbffffffffffffffffffffffff7ebfffffff0ffff7fffffff7fdfffff3fffffff7fbfffffbffffef7ffbdffffff57ffdfffbffeffff6fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdfffffffffffefffffffffffffffbffffdffffffffffffffff