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

187 lines
7.5 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 Animancer;
using UnityEngine;
namespace BaseGames.World
{
/// <summary>
/// 同场景成对门传送组件。两扇门互相引用,玩家进入其中一扇门时被传送到另一扇。
/// <para>
/// 触发流程:
/// <list type="number">
/// <item>玩家进入触发器或按交互键 → 播放 <see cref="_openClip"/> 开门动画(本门)</item>
/// <item>传送玩家到 <see cref="_linkedDoor"/> 的出生位置,并设定朝向</item>
/// <item>目标门播放 <see cref="_enterClip"/> 玩家走出动画</item>
/// </list>
/// </para>
/// <para>不涉及场景加载;适用于同房间内不同小区域之间的传送门、电梯、秘道等。</para>
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class LinkedDoorTransition : MonoBehaviour, IInteractable
{
[Header("配对")]
[Tooltip("与此门配对的另一扇门。玩家进入本门后传送到此目标门的出生点。")]
[SerializeField] private LinkedDoorTransition _linkedDoor;
[Header("出生配置")]
[Tooltip("传送后玩家在此门的出生位置偏移(相对于本 GameObject 中心,默认 0。")]
[SerializeField] private Vector2 _spawnOffset = new Vector2(0f, 0f);
[Tooltip("传送到此门后玩家的朝向(+1 = 朝右,-1 = 朝左)。")]
[SerializeField] private int _facingDirectionOnArrive = 1;
[Header("触发方式")]
[Tooltip("true = 玩家进入触发器自动触发false = 需玩家按交互键。")]
[SerializeField] private bool _autoTrigger = false;
[Header("门动画")]
[SerializeField] private AnimancerComponent _animancer;
[Tooltip("玩家从外侧进入时的开门动画。留空则跳过。")]
[SerializeField] private AnimationClip _openClip;
[Tooltip("玩家从内侧走出时的动画(目标门调用)。留空则跳过。")]
[SerializeField] private AnimationClip _enterClip;
[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 (_animancer != null && _openClip != null)
{
var state = _animancer.Play(_openClip);
yield return state;
}
// 2. 传送玩家到目标门
Vector3 destination = _linkedDoor.GetSpawnWorldPosition();
player.position = destination;
// 朝向(通过 localScale X 翻转,或通知 PlayerController
ApplyFacing(player, _linkedDoor._facingDirectionOnArrive);
// 3. 目标门播放玩家走出动画,并进入冷却
_linkedDoor.PlayEnterAnimation();
_linkedDoor.StartCooldown(_cooldown);
_triggered = false;
}
/// <summary>玩家从门内侧走出时的动画。由传送发起方在传送完成后调用。</summary>
public void PlayEnterAnimation()
{
if (_animancer != null && _enterClip != null)
_animancer.Play(_enterClip);
}
/// <summary>启动传送冷却,在此期间本门不会再次触发传送。</summary>
public void StartCooldown(float duration)
{
_cooldownUntil = Time.time + duration;
}
// ── 辅助 ──────────────────────────────────────────────────────────────
/// <summary>返回传送目标门的出生世界坐标(含偏移)。</summary>
public Vector3 GetSpawnWorldPosition()
=> transform.position + new Vector3(_spawnOffset.x, _spawnOffset.y, 0f);
private static void ApplyFacing(Transform player, int facing)
{
if (facing == 0) return;
Vector3 s = player.localScale;
float absX = Mathf.Abs(s.x);
s.x = facing > 0 ? absX : -absX;
player.localScale = s;
}
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);
}
private void OnDrawGizmos()
{
var col = GetComponent<Collider2D>();
if (col != null)
{
// 青色区分成对门
Gizmos.color = new Color(0.2f, 0.9f, 0.9f, 0.7f);
Gizmos.DrawWireCube(transform.position, col.bounds.size);
Gizmos.color = new Color(0.2f, 0.9f, 0.9f, 0.2f);
Gizmos.DrawCube(transform.position, 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());
}
}
}
}