diff --git a/Assets/_Game/Scripts/Core/IDropThrough.cs b/Assets/_Game/Scripts/Core/IDropThrough.cs
new file mode 100644
index 0000000..6792402
--- /dev/null
+++ b/Assets/_Game/Scripts/Core/IDropThrough.cs
@@ -0,0 +1,12 @@
+namespace BaseGames.Core
+{
+ ///
+ /// 单向平台接口:挂载在平台上,允许角色从上方穿落。
+ /// 定义在 BaseGames.Core 以避免 Player ↔ World 程序集循环引用。
+ ///
+ public interface IDropThrough
+ {
+ /// 触发穿落:临时禁用碰撞器,使角色向下穿过平台。
+ void TriggerDropThrough();
+ }
+}
diff --git a/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs b/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs
index 9533174..cbedfe4 100644
--- a/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs
+++ b/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs
@@ -95,7 +95,9 @@ namespace BaseGames.Editor
Transform groundCheckT = GetOrCreateChild(root.transform, "GroundCheck");
groundCheckT.localPosition = new Vector3(0f, -0.75f, 0f);
AssignReference(playerMovement, "_groundCheck", groundCheckT, report);
- AssignLayerMask(playerMovement, "_groundLayer", "Platform", report);
+ AssignLayerMask(playerMovement, "_groundLayer",
+ new[] { "Platform", "OneWayPlatform", "MovingOneWayPlatform", "MidHeightOneWayPlatform" },
+ report);
// ── SkillHitBox_Slot 子节点(技能 HitBox 实例化挂点)────────────────
Transform skillSocketT = GetOrCreateChild(root.transform, "SkillHitBox_Slot");
@@ -1001,6 +1003,31 @@ namespace BaseGames.Editor
so.ApplyModifiedPropertiesWithoutUndo();
}
+ /// 将多个 Layer 名称合并为一个 LayerMask 并写入 SerializedProperty。
+ private static void AssignLayerMask(Object target, string propName, string[] layerNames, List 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)
{
var so = new SerializedObject(target);
diff --git a/Assets/_Game/Scripts/Player/PlayerMovement.cs b/Assets/_Game/Scripts/Player/PlayerMovement.cs
index 1d4a5ff..95251d3 100644
--- a/Assets/_Game/Scripts/Player/PlayerMovement.cs
+++ b/Assets/_Game/Scripts/Player/PlayerMovement.cs
@@ -1,3 +1,4 @@
+using BaseGames.Core;
using UnityEngine;
namespace BaseGames.Player
@@ -34,7 +35,8 @@ namespace BaseGames.Player
private int _facingDirection = 1;
private bool _cancelWindowOpen;
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
// ── 运行时调试(Inspector 中可见)───────────────────────────────
@@ -43,6 +45,7 @@ namespace BaseGames.Player
[SerializeField] private float _dbg_VelocityX;
[SerializeField] private float _dbg_VelocityY;
[SerializeField] private bool _dbg_IsGrounded;
+ [SerializeField] private bool _dbg_OnOneWayPlatform;
[SerializeField] private bool _dbg_HasCoyoteTime;
[SerializeField] private bool _dbg_IsWallLeft;
[SerializeField] private bool _dbg_IsWallRight;
@@ -91,10 +94,11 @@ namespace BaseGames.Player
_coyoteTimer = Mathf.Max(0f, _coyoteTimer - Time.fixedDeltaTime);
#if UNITY_EDITOR
- _dbg_VelocityX = _rb.velocity.x;
- _dbg_VelocityY = _rb.velocity.y;
- _dbg_IsGrounded = _isGrounded;
- _dbg_HasCoyoteTime = _coyoteTimer > 0f;
+ _dbg_VelocityX = _rb.velocity.x;
+ _dbg_VelocityY = _rb.velocity.y;
+ _dbg_IsGrounded = _isGrounded;
+ _dbg_OnOneWayPlatform = _onOneWayPlatform;
+ _dbg_HasCoyoteTime = _coyoteTimer > 0f;
_dbg_IsWallLeft = _isWallLeft;
_dbg_IsWallRight = _isWallRight;
_dbg_CancelWindowOpen = _cancelWindowOpen;
@@ -243,19 +247,54 @@ namespace BaseGames.Player
_rb.velocity = new Vector2(_rb.velocity.x, 0f);
}
- /// 单向平台穿透(输入下行 + 跳跃键时触发)。
- public void DropThroughPlatform() { }
+ ///
+ /// 单向平台穿落(↓ + 跳跃键触发)。
+ /// 找到脚下的 并临时禁用其碰撞器;
+ /// 同帧清除 IsGrounded / OnOneWayPlatform,状态机无需等到下一物理帧即可转换到 FallState。
+ ///
+ public void DropThroughPlatform()
+ {
+ if (!_onOneWayPlatform) return;
+
+ for (int i = 0; i < _groundHitCount; i++)
+ {
+ if (_groundBuffer[i] != null &&
+ _groundBuffer[i].TryGetComponent(out var platform))
+ {
+ platform.TriggerDropThrough();
+ // 立即清除着地状态,让状态机本帧即可切换到 FallState
+ _isGrounded = false;
+ _onOneWayPlatform = false;
+ _coyoteTimer = 0f;
+ break;
+ }
+ }
+ }
// ── Physics 检测 ──────────────────────────────────────────────────────
private void CheckGrounded()
{
if (_groundCheck == null) return;
- bool wasGrounded = _isGrounded;
- _isGrounded = Physics2D.OverlapBoxNonAlloc(_groundCheck.position, _groundCheckSize, 0f, _groundBuffer, _groundLayer) > 0;
+ _groundHitCount = Physics2D.OverlapBoxNonAlloc(
+ _groundCheck.position, _groundCheckSize, 0f, _groundBuffer, _groundLayer);
+ _isGrounded = _groundHitCount > 0;
- if (_isGrounded && !wasGrounded)
- _coyoteTimer = _config.CoyoteTime;
+ // 检测是否站在单向平台(含 IDropThrough 组件的碰撞体)
+ _onOneWayPlatform = false;
+ for (int i = 0; i < _groundHitCount; i++)
+ {
+ if (_groundBuffer[i] != null &&
+ _groundBuffer[i].TryGetComponent(out _))
+ {
+ _onOneWayPlatform = true;
+ break;
+ }
+ }
+
+ _currentSurface = (_isGrounded && _onOneWayPlatform)
+ ? SurfaceType.OneWayPlatform
+ : SurfaceType.Ground;
}
private void CheckWalls()
diff --git a/Assets/_Game/Scripts/Player/States/IdleState.cs b/Assets/_Game/Scripts/Player/States/IdleState.cs
index 1cdba21..368e889 100644
--- a/Assets/_Game/Scripts/Player/States/IdleState.cs
+++ b/Assets/_Game/Scripts/Player/States/IdleState.cs
@@ -26,6 +26,13 @@ namespace BaseGames.Player.States
_owner.TransitionTo(_owner.GetState());
return;
}
+ // 单向平台穿落:↓ + 跳跃键,优先于普通跳跃,避免误消耗跳跃缓冲
+ if (Move.OnOneWayPlatform && Input.MoveInput.y < -0.5f && Buffer.ConsumeJump())
+ {
+ Move.DropThroughPlatform();
+ _owner.TransitionTo(_owner.GetState());
+ return;
+ }
if (Buffer.ConsumeJump())
{
_owner.TransitionTo(_owner.GetState());
diff --git a/Assets/_Game/Scripts/Player/States/RunState.cs b/Assets/_Game/Scripts/Player/States/RunState.cs
index 459549c..97e5dac 100644
--- a/Assets/_Game/Scripts/Player/States/RunState.cs
+++ b/Assets/_Game/Scripts/Player/States/RunState.cs
@@ -25,6 +25,13 @@ namespace BaseGames.Player.States
_owner.TransitionTo(_owner.GetState());
return;
}
+ // 单向平台穿落:↓ + 跳跃键,优先于普通跳跃
+ if (Move.OnOneWayPlatform && Input.MoveInput.y < -0.5f && Buffer.ConsumeJump())
+ {
+ Move.DropThroughPlatform();
+ _owner.TransitionTo(_owner.GetState());
+ return;
+ }
if (Buffer.ConsumeJump())
{
_owner.TransitionTo(_owner.GetState());
diff --git a/Assets/_Game/Scripts/World/PhantomPlate.cs b/Assets/_Game/Scripts/World/PhantomPlate.cs
index 90c3152..3bdbc3f 100644
--- a/Assets/_Game/Scripts/World/PhantomPlate.cs
+++ b/Assets/_Game/Scripts/World/PhantomPlate.cs
@@ -1,3 +1,4 @@
+using BaseGames.Core;
using UnityEngine;
namespace BaseGames.World
@@ -10,7 +11,7 @@ namespace BaseGames.World
///
[RequireComponent(typeof(Collider2D))]
[RequireComponent(typeof(PlatformEffector2D))]
- public class PhantomPlate : MonoBehaviour
+ public class PhantomPlate : MonoBehaviour, IDropThrough
{
[Header("下蹲跌落")]
[Tooltip("按住下方向 + 跳跃时临时禁用碰撞器,允许玩家向下穿过平台")]
diff --git a/ProjectSettings/Physics2DSettings.asset b/ProjectSettings/Physics2DSettings.asset
index 0985f84..e8de74c 100644
--- a/ProjectSettings/Physics2DSettings.asset
+++ b/ProjectSettings/Physics2DSettings.asset
@@ -45,4 +45,4 @@ Physics2DSettings:
m_ReuseCollisionCallbacks: 1
m_AutoSyncTransforms: 0
m_GizmoOptions: 10
- m_LayerCollisionMatrix: ffffffffffffffffffffffffbffffffffffffffffffffffff7ebfffffff0ffff7ffdffff7fdeffff3fffffff7fbfffffbfffffffffbdffffffd7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+ m_LayerCollisionMatrix: ffffffffffffffffffffffffbffffffffffffffffffffffff7ebfffffff0ffff7fffffff7fdfffff3fffffff7fbfffffbffffef7ffbdffffff57ffdfffbffeffff6fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdfffffffffffefffffffffffffffbffffdffffffffffffffff