修复内容:
PlayerMovement:新增 _facingLocked 字段 + LockFacing(bool) 方法;UpdateFacing() 锁定时直接返回 WallSlideState:OnStateEnter 调用 LockFacing(true) + FlipFacing(_wallDir);OnStateExit 调用 LockFacing(false) 解锁 WallJumpState:OnStateEnter 保险性再调一次 LockFacing(false);WallJumpAway/Toward 同步写入 _inputVelocityX,确保解锁后 UpdateFacing 朝向正确(背墙跳 = 离墙方向,对墙跳 = 朝墙方向)
This commit is contained in:
@@ -18,6 +18,11 @@ namespace BaseGames.World
|
||||
[SerializeField] private SceneFeedback _crumbleFeedback; // 预警震动 + 碎裂粒子 + 音效
|
||||
[SerializeField] private BoxCollider2D _passengerSensor; // IsTrigger,检测玩家踩踏
|
||||
|
||||
[Header("持久化(_isOneShot = true 时生效)")]
|
||||
[Tooltip("平台唯一 ID。_isOneShot=true 时碎裂状态写入 WorldStateRegistry,重载场景后不复原。留空则不持久化。")]
|
||||
[SerializeField] private string _platformId;
|
||||
[SerializeField] private WorldStateRegistry _worldState;
|
||||
|
||||
private BoxCollider2D _col;
|
||||
private SpriteRenderer _sr;
|
||||
private bool _isCrumbling;
|
||||
@@ -28,6 +33,18 @@ namespace BaseGames.World
|
||||
_sr = GetComponent<SpriteRenderer>();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 读档恢复:_isOneShot 平台已碎裂则直接禁用,无需等待触发
|
||||
if (!_isOneShot || string.IsNullOrEmpty(_platformId) || _worldState == null) return;
|
||||
if (!_worldState.HasFlag("crumble_" + _platformId)) return;
|
||||
|
||||
_isCrumbling = true;
|
||||
_col.enabled = false;
|
||||
_sr.enabled = false;
|
||||
if (_passengerSensor != null) _passengerSensor.enabled = false;
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (_isCrumbling) return;
|
||||
@@ -53,7 +70,12 @@ namespace BaseGames.World
|
||||
_passengerSensor.enabled = false;
|
||||
|
||||
if (_isOneShot || _respawnDelay <= 0f)
|
||||
yield break; // 永久消失
|
||||
{
|
||||
// 持久化一次性碎裂状态,场景重载后不复原
|
||||
if (_isOneShot && !string.IsNullOrEmpty(_platformId))
|
||||
_worldState?.SetFlag("crumble_" + _platformId);
|
||||
yield break;
|
||||
}
|
||||
|
||||
// 4. Respawn
|
||||
yield return new WaitForSeconds(_respawnDelay);
|
||||
|
||||
@@ -51,10 +51,15 @@ namespace BaseGames.World
|
||||
|
||||
// ── Physics Triggers ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 判断碰撞体是否为有效触发来源。子类可覆写以扩展触发主体(如幻影身体)。
|
||||
/// </summary>
|
||||
protected virtual bool IsValidTriggerBody(Collider2D col) => col.CompareTag("Player");
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (_triggerCondition != TriggerCondition.PlayerBody) return;
|
||||
if (!other.CompareTag("Player")) return;
|
||||
if (!IsValidTriggerBody(other)) return;
|
||||
if (!CheckSide(other.transform.position)) return;
|
||||
TryActivate();
|
||||
}
|
||||
@@ -62,7 +67,7 @@ namespace BaseGames.World
|
||||
private void OnTriggerExit2D(Collider2D other)
|
||||
{
|
||||
if (_triggerCondition != TriggerCondition.PlayerBody) return;
|
||||
if (!other.CompareTag("Player") || _isOneShot) return;
|
||||
if (!IsValidTriggerBody(other) || _isOneShot) return;
|
||||
_activated = false;
|
||||
_deactivationChannel?.Raise();
|
||||
}
|
||||
@@ -106,13 +111,14 @@ namespace BaseGames.World
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 读档恢复:若机关已激活则静默还原
|
||||
// 读档恢复:仅标记 _activated = true,不广播激活事件。
|
||||
// 下游组件(PuzzleReceiver / 动画门等)已通过各自的 WorldStateRegistry 检查在 Start 中自行恢复,
|
||||
// 重复广播会导致它们再次播放开门动画等副作用。
|
||||
if (_isOneShot && !string.IsNullOrEmpty(_interactableId)
|
||||
&& _worldState != null
|
||||
&& _worldState.HasFlag("mechanism_" + _interactableId))
|
||||
{
|
||||
_activated = true;
|
||||
_activationChannel?.Raise();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,36 +4,67 @@ using UnityEngine;
|
||||
namespace BaseGames.World
|
||||
{
|
||||
/// <summary>
|
||||
/// 危险区域。玩家进入时触发即死或持续伤害。
|
||||
/// 危险区域。玩家进入时触发即死(调用 Kill(),无视无敌帧)或持续伤害。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Collider2D))]
|
||||
public class HazardZone : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private bool _isInstantKill = true;
|
||||
[SerializeField] private int _damage = 9999;
|
||||
[SerializeField] private bool _isInstantKill = true;
|
||||
[SerializeField] private int _damage = 1;
|
||||
[Tooltip("持续伤害模式下每次造成伤害的间隔(秒)。仅 _isInstantKill = false 时生效。")]
|
||||
[SerializeField] private float _damageInterval = 0.5f;
|
||||
|
||||
private PlayerStats _cachedStats;
|
||||
private float _damageTimer;
|
||||
private bool _playerInside;
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (!other.CompareTag("Player")) return;
|
||||
_cachedStats = other.GetComponentInParent<PlayerStats>();
|
||||
if (_cachedStats == null) return;
|
||||
|
||||
var stats = other.GetComponentInParent<PlayerStats>();
|
||||
if (stats == null) return;
|
||||
_playerInside = true;
|
||||
ApplyDamage();
|
||||
_damageTimer = _damageInterval;
|
||||
}
|
||||
|
||||
private void OnTriggerExit2D(Collider2D other)
|
||||
{
|
||||
if (!other.CompareTag("Player")) return;
|
||||
_playerInside = false;
|
||||
_cachedStats = null;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!_playerInside || _isInstantKill || _cachedStats == null) return;
|
||||
|
||||
_damageTimer -= Time.deltaTime;
|
||||
if (_damageTimer <= 0f)
|
||||
{
|
||||
ApplyDamage();
|
||||
_damageTimer = _damageInterval;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyDamage()
|
||||
{
|
||||
if (_cachedStats == null) return;
|
||||
if (_isInstantKill)
|
||||
stats.TakeDamage(stats.MaxHP * 2); // 确保即死(超过最大血量)
|
||||
_cachedStats.Kill();
|
||||
else
|
||||
stats.TakeDamage(_damage);
|
||||
_cachedStats.TakeDamage(_damage);
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
Gizmos.color = new Color(1f, 0f, 0f, 0.3f);
|
||||
var col = GetComponent<Collider2D>();
|
||||
if (col != null)
|
||||
Gizmos.DrawCube(transform.position, col.bounds.size);
|
||||
if (col == null) return;
|
||||
Gizmos.color = new Color(1f, 0f, 0f, 0.3f);
|
||||
Gizmos.DrawCube(transform.position, col.bounds.size);
|
||||
Gizmos.color = new Color(1f, 0f, 0f, 0.8f);
|
||||
if (col != null)
|
||||
Gizmos.DrawWireCube(transform.position, col.bounds.size);
|
||||
Gizmos.DrawWireCube(transform.position, col.bounds.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Input;
|
||||
using UnityEngine;
|
||||
@@ -22,8 +23,19 @@ namespace BaseGames.World
|
||||
// 预分配检测缓冲区,避免 OverlapCircleAll 每帧 GC 分配
|
||||
private readonly Collider2D[] _overlapBuffer = new Collider2D[16];
|
||||
|
||||
private void OnEnable() => _inputReader.InteractEvent += TryInteract;
|
||||
private void OnDisable() => _inputReader.InteractEvent -= TryInteract;
|
||||
// Collider → IInteractable 缓存,避免 FindNearest 每帧重复 GetComponentInParent
|
||||
private readonly Dictionary<Collider2D, IInteractable> _componentCache = new();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_inputReader.InteractEvent += TryInteract;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_inputReader.InteractEvent -= TryInteract;
|
||||
_componentCache.Clear(); // 清理缓存,防止跨场景持有旧引用
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
@@ -57,20 +69,29 @@ namespace BaseGames.World
|
||||
|
||||
private IInteractable FindNearest(Collider2D[] hits, int count)
|
||||
{
|
||||
IInteractable best = null;
|
||||
float bestDist = float.MaxValue;
|
||||
IInteractable best = null;
|
||||
float bestSqrDist = float.MaxValue;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var col = hits[i];
|
||||
var interactable = col.GetComponentInParent<IInteractable>();
|
||||
if (col == null) continue;
|
||||
|
||||
// 查缓存,未命中时才调用 GetComponentInParent(避免每帧反射开销)
|
||||
if (!_componentCache.TryGetValue(col, out var interactable))
|
||||
{
|
||||
interactable = col.GetComponentInParent<IInteractable>();
|
||||
_componentCache[col] = interactable;
|
||||
}
|
||||
|
||||
if (interactable == null || !interactable.CanInteract) continue;
|
||||
|
||||
float dist = Vector2.Distance(transform.position, col.transform.position);
|
||||
if (dist < bestDist)
|
||||
// 用 sqrMagnitude 比较距离,省去 Distance 的 sqrt 开销
|
||||
float sqrDist = ((Vector2)transform.position - (Vector2)col.transform.position).sqrMagnitude;
|
||||
if (sqrDist < bestSqrDist)
|
||||
{
|
||||
bestDist = dist;
|
||||
best = interactable;
|
||||
bestSqrDist = sqrDist;
|
||||
best = interactable;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ namespace BaseGames.World
|
||||
|
||||
if (_moveType == MoveType.LinearAB)
|
||||
{
|
||||
if (_wayPoints.Length < 2) return; // 至少需要两个路径点
|
||||
_movingForward = !_movingForward;
|
||||
_waypointIndex = _movingForward ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace BaseGames.World
|
||||
{
|
||||
/// <summary>
|
||||
/// 幻影可交互机关。继承 DirectionalInteractable,额外响应 PhantomBody 层(太虚斩形态)。
|
||||
/// 通过覆写 IsValidTriggerBody 扩展触发主体,_triggerCondition 条件检查由父类统一处理。
|
||||
/// </summary>
|
||||
public class PhantomInteractable : DirectionalInteractable
|
||||
{
|
||||
@@ -11,13 +12,7 @@ namespace BaseGames.World
|
||||
|
||||
private void Awake() => _phantomBodyLayer = LayerMask.NameToLayer("PhantomBody");
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
bool isPlayer = other.CompareTag("Player");
|
||||
bool isPhantom = other.gameObject.layer == _phantomBodyLayer;
|
||||
|
||||
if (!isPlayer && !isPhantom) return;
|
||||
TryActivate();
|
||||
}
|
||||
protected override bool IsValidTriggerBody(Collider2D col)
|
||||
=> col.CompareTag("Player") || col.gameObject.layer == _phantomBodyLayer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System;
|
||||
using Animancer;
|
||||
using BaseGames.Feedback;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.World;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -38,6 +39,9 @@ namespace BaseGames.Puzzle
|
||||
[Header("持久化(SO 注入,非 Instance 单例)")]
|
||||
[SerializeField] private WorldStateRegistry _worldState;
|
||||
|
||||
[Header("Hold 模式输入(SwitchTriggerMode.Hold 时必填)")]
|
||||
[SerializeField] private InputReaderSO _inputReader;
|
||||
|
||||
private bool _isActive;
|
||||
|
||||
public bool IsActive => _isActive;
|
||||
@@ -59,16 +63,41 @@ namespace BaseGames.Puzzle
|
||||
|
||||
// ── IInteractable ────────────────────────────────────────────────────
|
||||
public string InteractPrompt => _mode == SwitchTriggerMode.Hold ? "按住交互" : "交互";
|
||||
public bool CanInteract => true;
|
||||
|
||||
/// <summary>
|
||||
/// 压板模式不需要交互提示(物理触发);InteractOnce 已激活后隐藏提示。
|
||||
/// </summary>
|
||||
public bool CanInteract => _mode switch
|
||||
{
|
||||
SwitchTriggerMode.InteractOnce => !_isActive,
|
||||
SwitchTriggerMode.InteractToggle => true,
|
||||
SwitchTriggerMode.Hold => true,
|
||||
SwitchTriggerMode.Pressure => false,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
public void Interact(Transform player)
|
||||
{
|
||||
// Hold 模式通过 OnPlayerEnterRange 订阅输入事件处理,此处不响应
|
||||
if (_mode == SwitchTriggerMode.Hold) return;
|
||||
if (_mode == SwitchTriggerMode.InteractOnce && _isActive) return;
|
||||
SetState(!_isActive);
|
||||
}
|
||||
|
||||
public void OnPlayerEnterRange(Transform player) { }
|
||||
public void OnPlayerExitRange() { }
|
||||
public void OnPlayerEnterRange(Transform player)
|
||||
{
|
||||
if (_mode != SwitchTriggerMode.Hold || _inputReader == null) return;
|
||||
_inputReader.InteractEvent += OnHoldStarted;
|
||||
_inputReader.InteractCancelledEvent += OnHoldCancelled;
|
||||
}
|
||||
|
||||
public void OnPlayerExitRange()
|
||||
{
|
||||
UnsubscribeHold();
|
||||
// 玩家离开范围时停用 Hold 开关
|
||||
if (_mode == SwitchTriggerMode.Hold && _isActive)
|
||||
SetState(false);
|
||||
}
|
||||
|
||||
// ── ISwitchable ──────────────────────────────────────────────────────
|
||||
public void ForceState(bool active) => SetState(active);
|
||||
@@ -106,5 +135,19 @@ namespace BaseGames.Puzzle
|
||||
else _worldState?.ClearFlag("switch_" + _switchId);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hold 模式辅助 ──────────────────────────────────────────────────────
|
||||
|
||||
private void OnHoldStarted() => SetState(true);
|
||||
private void OnHoldCancelled() => SetState(false);
|
||||
|
||||
private void UnsubscribeHold()
|
||||
{
|
||||
if (_inputReader == null) return;
|
||||
_inputReader.InteractEvent -= OnHoldStarted;
|
||||
_inputReader.InteractCancelledEvent -= OnHoldCancelled;
|
||||
}
|
||||
|
||||
private void OnDestroy() => UnsubscribeHold();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user