187 lines
7.5 KiB
C#
187 lines
7.5 KiB
C#
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());
|
||
}
|
||
}
|
||
}
|
||
}
|