Files
zeling_v2/Assets/_Game/Scripts/World/LinkedDoorTransition.cs
2026-05-19 16:21:27 +08:00

185 lines
8.1 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 System.Collections;
using BaseGames.Feedback;
using BaseGames.Player;
using UnityEngine;
namespace BaseGames.World
{
/// <summary>
/// 同场景成对门传送组件。两扇门互相引用,玩家进入其中一扇门时被传送到另一扇。
/// <para>
/// 触发流程:
/// <list type="number">
/// <item>玩家进入触发器或按交互键 → 播放 <see cref="_transitionOut"/> 转场反馈(淡出)</item>
/// <item>等待反馈完成后传送玩家到 <see cref="_linkedDoor"/> 的 <see cref="_spawnPoint"/> 位置,并设定朝向</item>
/// <item>播放 <see cref="_linkedDoor"/> 的 <see cref="_transitionIn"/> 转场反馈(淡入),目标门进入冷却</item>
/// </list>
/// </para>
/// <para>不涉及场景加载;适用于同房间内不同小区域之间的传送门、电梯、秘道等。</para>
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class LinkedDoorTransition : MonoBehaviour, IInteractable
{
[Header("配对")]
[Tooltip("与此门配对的另一扇门。玩家进入本门后传送到此目标门的出生点。")]
[SerializeField] private LinkedDoorTransition _linkedDoor;
[Header("出生配置")]
[Tooltip("传送到此门后玩家的出生位置。拖动场景中的子节点 SpawnPoint 来调整。\n为空时回退到 GameObject 中心。")]
[SerializeField] private Transform _spawnPoint;
[Tooltip("传送到此门后玩家的朝向(+1 = 朝右,-1 = 朝左)。")]
[SerializeField] private int _facingDirectionOnArrive = 1;
[Header("触发方式")]
[Tooltip("true = 玩家进入触发器自动触发false = 需玩家按交互键。")]
[SerializeField] private bool _autoTrigger = false;
[Header("转场反馈")]
[Tooltip("传送前播放(淡出)。留空则跳过,直接传送。")]
[SerializeField] private SceneFeedback _transitionOut;
[Tooltip("传送后播放(淡入)。留空则跳过。由目标门在玩家到达后自动播放。")]
[SerializeField] private SceneFeedback _transitionIn;
[Header("钥匙物品校验")]
[SerializeField] private bool _requiresKeyItem;
[SerializeField] private string _requiredItemId;
[Header("世界状态")]
[SerializeField] private WorldStateRegistry _worldState;
[Header("冷却")]
[Tooltip("传送完成后本门的冷却时间(秒)。防止玩家被反复来回传送。")]
[SerializeField] private float _cooldown = 1.5f;
private bool _triggered;
private float _cooldownUntil;
// ── IInteractable ─────────────────────────────────────────────────────
public bool CanInteract => !_autoTrigger;
public string InteractPrompt => "进入";
public void Interact(Transform player) => TryTrigger(player);
public void OnPlayerEnterRange(Transform player) { }
public void OnPlayerExitRange() { }
// ── 触发 ──────────────────────────────────────────────────────────────
private void OnTriggerEnter2D(Collider2D other)
{
if (!_autoTrigger) return;
if (!other.CompareTag("Player")) return;
TryTrigger(other.transform);
}
private void TryTrigger(Transform player)
{
if (_triggered) return;
if (Time.time < _cooldownUntil) return;
if (_linkedDoor == null)
{
Debug.LogWarning($"[LinkedDoorTransition] {name}:未配置 _linkedDoor传送中止。");
return;
}
if (_requiresKeyItem && !HasItem(_requiredItemId)) return;
_triggered = true;
StartCoroutine(TransitionCoroutine(player));
}
private void OnDisable() => _triggered = false;
// ── 传送流程 ──────────────────────────────────────────────────────────
private IEnumerator TransitionCoroutine(Transform player)
{
// 1. 淡出
if (_transitionOut != null)
yield return _transitionOut.PlayAndWait();
// 2. 传送
player.position = _linkedDoor.GetSpawnWorldPosition();
// 通过 PlayerMovement 设定朝向(同步清零速度,防止旧速度覆盖朝向)
var movement = player.GetComponentInChildren<PlayerMovement>()
?? player.GetComponent<PlayerMovement>();
if (movement != null)
movement.SetFacingImmediate(_linkedDoor._facingDirectionOnArrive);
// 4. 淡入(目标门播放)+ 冷却
_linkedDoor.PlayTransitionIn();
_linkedDoor.StartCooldown(_cooldown);
_triggered = false;
}
/// <summary>播放此门的淡入反馈。由传送发起方在玩家到达后调用。</summary>
public void PlayTransitionIn() => _transitionIn?.Play();
/// <summary>启动传送冷却,在此期间本门不会再次触发传送。</summary>
public void StartCooldown(float duration) => _cooldownUntil = Time.time + duration;
// ── 辅助 ──────────────────────────────────────────────────────────────
/// <summary>返回传送目标位置。优先使用 _spawnPoint 子节点,否则回退到门中心。</summary>
public Vector3 GetSpawnWorldPosition()
=> _spawnPoint != null ? _spawnPoint.position : transform.position;
private bool HasItem(string itemId)
{
if (string.IsNullOrEmpty(itemId)) return true;
if (_worldState == null)
{
Debug.LogWarning($"[LinkedDoorTransition] WorldStateRegistry 未配置,钥匙 {itemId} 检查跳过");
return false;
}
return _worldState.IsCollected(itemId);
}
// ── Gizmos ────────────────────────────────────────────────────────────
#if UNITY_EDITOR
private void OnDrawGizmos()
{
var col = GetComponent<Collider2D>();
if (col != null)
{
Gizmos.color = new Color(0.2f, 0.9f, 0.9f, 0.7f);
Gizmos.DrawWireCube(col.bounds.center, col.bounds.size);
Gizmos.color = new Color(0.2f, 0.9f, 0.9f, 0.15f);
Gizmos.DrawCube(col.bounds.center, col.bounds.size);
}
Vector3 spawn = GetSpawnWorldPosition();
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(spawn, 0.2f);
if (_linkedDoor != null)
{
Gizmos.color = new Color(0.2f, 0.9f, 0.9f, 0.8f);
Gizmos.DrawLine(spawn, _linkedDoor.GetSpawnWorldPosition());
}
Vector3 labelPos = col != null
? col.bounds.center + Vector3.up * (col.bounds.extents.y + 0.3f)
: transform.position + Vector3.up * 1.5f;
string facing = _facingDirectionOnArrive >= 0 ? "→" : "←";
string trigger = _autoTrigger ? "Auto" : "Interact";
string linkName = _linkedDoor != null ? _linkedDoor.name : "未配对";
string keyInfo = _requiresKeyItem ? $" 🔑{_requiredItemId}" : "";
string grandparent = transform.parent != null ? $"[{transform.parent.name}] " : "";
string label = $"{grandparent}{name}\n→ {linkName} | {trigger} | 到达朝向:{facing}{keyInfo}";
UnityEditor.Handles.color = Color.white;
UnityEditor.Handles.Label(labelPos, label);
}
#endif
}
}