From 0559d2cd360a416d43af13bbc61881f22a5fd7f1 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 19 May 2026 15:30:27 +0800 Subject: [PATCH] linkdoor --- Assets/_Game/Scenes/Testings/TestRoomA.unity | 10 +- .../Editor/Scene/SceneObjectPlacerTool.cs | 104 ++++++++++ .../Scripts/World/LinkedDoorTransition.cs | 186 ++++++++++++++++++ 3 files changed, 295 insertions(+), 5 deletions(-) create mode 100644 Assets/_Game/Scripts/World/LinkedDoorTransition.cs diff --git a/Assets/_Game/Scenes/Testings/TestRoomA.unity b/Assets/_Game/Scenes/Testings/TestRoomA.unity index 47e5bc8..1a9d34a 100644 --- a/Assets/_Game/Scenes/Testings/TestRoomA.unity +++ b/Assets/_Game/Scenes/Testings/TestRoomA.unity @@ -7686,7 +7686,7 @@ MonoBehaviour: _input: {fileID: 0} _formController: {fileID: 0} _modifiers: {fileID: 0} - _skillSocket: {fileID: 0} + _skillSocket: {fileID: 676050686} _formSkillSets: [] --- !u!114 &430284920 MonoBehaviour: @@ -7716,7 +7716,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: _config: {fileID: 11400000, guid: 8f33c49c9bd20ac47a6867f1f19f24a9, type: 2} - _input: {fileID: 0} + _input: {fileID: 11400000, guid: 88fadef8bc554e04483edd7418d20aa2, type: 2} _onFormChanged: {fileID: 0} _onSkillSetChanged: {fileID: 0} --- !u!114 &430284922 @@ -7750,7 +7750,6 @@ MonoBehaviour: _groundLayer: serializedVersion: 2 m_Bits: 128 - _spriteRenderer: {fileID: 0} _dbg_Position: _dbg_VelocityX: 0 _dbg_VelocityY: 0 @@ -8078,6 +8077,7 @@ MonoBehaviour: m_EditorClassIdentifier: _transitionId: default _facingDirection: 1 + _exitDoor: {fileID: 0} --- !u!4 &442873752 Transform: m_ObjectHideFlags: 0 @@ -19133,7 +19133,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 1 + m_IsActive: 0 --- !u!61 &1354690326 BoxCollider2D: m_ObjectHideFlags: 0 @@ -26350,7 +26350,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 1 + m_IsActive: 0 --- !u!61 &1865796629 BoxCollider2D: m_ObjectHideFlags: 0 diff --git a/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs b/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs index a4da45f..a98ef8a 100644 --- a/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs +++ b/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs @@ -499,6 +499,110 @@ namespace BaseGames.Editor MarkDirtyAndLog("Room Transition", go, report); } + [MenuItem("BaseGames/Scene/Place/Door Transition", priority = 141)] + public static void PlaceDoorTransition() + { + var report = new List(); + int undoGroup = Undo.GetCurrentGroup(); + Undo.SetCurrentGroupName("Place Door Transition"); + + GameObject go = new GameObject("DoorTransition"); + Undo.RegisterCreatedObjectUndo(go, "Place Door Transition"); + go.transform.position = GetDropPosition(); + SetLayer(go, "TriggerZone", report); + + // 触发碰撞体(门宽×门高) + BoxCollider2D col = GetOrAddComponent(go); + col.isTrigger = true; + col.size = new Vector2(1.5f, 2.5f); + + // 精灵渲染器 + 动画(门对象通常有外观与开关动画) + SetupSpriteRenderer(go); + GetOrAddComponent(go); + AnimancerComponent animancer = GetOrAddComponent(go); + + // DoorTransition 组件 + DoorTransition door = GetOrAddComponent(go); + AssignBool(door, "_autoTrigger", false); // 默认需玩家按交互键 + AssignReference(door, "_animancer", animancer, report); + + AssignAsset(door, "_onSceneLoadRequest", report, false, "EVT_SceneLoadRequest"); + + report.Add("填写 _targetSceneAddress(目标场景 Addressable Key)与 _targetTransitionId(目标 PlayerSpawnPoint 的 _transitionId)。"); + report.Add("将开门动画片段拖入 _openClip;若目标场景有玩家从门中走出的动画,拖入 _enterClip 并在目标场景 PlayerSpawnPoint._exitDoor 引用该侧的 DoorTransition。"); + report.Add("过渡类型默认 Room(极短淡出);若跨大区域,将 _transitionType 改为 Scene。"); + report.Add("若需钥匙解锁,设 _requiresKeyItem = true 并填写 _requiredItemId。"); + + Undo.CollapseUndoOperations(undoGroup); + + Selection.activeGameObject = go; + MarkDirtyAndLog("Door Transition", go, report); + } + + [MenuItem("BaseGames/Scene/Place/Linked Door Pair (Same-Scene)", priority = 142)] + public static void PlaceLinkedDoorPair() + { + var report = new List(); + int undoGroup = Undo.GetCurrentGroup(); + Undo.SetCurrentGroupName("Place Linked Door Pair"); + + Vector3 basePos = GetDropPosition(); + + // ── 门 A ───────────────────────────────────────────────────────── + GameObject goA = new GameObject("LinkedDoor_A"); + Undo.RegisterCreatedObjectUndo(goA, "Place LinkedDoor_A"); + goA.transform.position = basePos + new Vector3(-3f, 0f, 0f); + SetLayer(goA, "TriggerZone", report); + + BoxCollider2D colA = GetOrAddComponent(goA); + colA.isTrigger = true; + colA.size = new Vector2(1.5f, 2.5f); + + SetupSpriteRenderer(goA); + GetOrAddComponent(goA); + AnimancerComponent animancerA = GetOrAddComponent(goA); + + LinkedDoorTransition doorA = GetOrAddComponent(goA); + AssignBool(doorA, "_autoTrigger", true); + AssignReference(doorA, "_animancer", animancerA, report); + AssignInt(doorA, "_facingDirectionOnArrive", 1); // 朝右走出 + + // ── 门 B ───────────────────────────────────────────────────────── + GameObject goB = new GameObject("LinkedDoor_B"); + Undo.RegisterCreatedObjectUndo(goB, "Place LinkedDoor_B"); + goB.transform.position = basePos + new Vector3(3f, 0f, 0f); + SetLayer(goB, "TriggerZone", report); + + BoxCollider2D colB = GetOrAddComponent(goB); + colB.isTrigger = true; + colB.size = new Vector2(1.5f, 2.5f); + + SetupSpriteRenderer(goB); + GetOrAddComponent(goB); + AnimancerComponent animancerB = GetOrAddComponent(goB); + + LinkedDoorTransition doorB = GetOrAddComponent(goB); + AssignBool(doorB, "_autoTrigger", true); + AssignReference(doorB, "_animancer", animancerB, report); + AssignInt(doorB, "_facingDirectionOnArrive", -1); // 朝左走出 + + // ── 互相绑定 ───────────────────────────────────────────────────── + AssignReference(doorA, "_linkedDoor", doorB, report); + AssignReference(doorB, "_linkedDoor", doorA, report); + + report.Add("LinkedDoor_A ↔ LinkedDoor_B 已互相绑定。"); + report.Add("将两扇门分别移到场景中正确位置(如两个区域的入口处)。"); + report.Add("调整 _spawnOffset 使玩家传送后不被触发器再次捕获(如偏移 1f 让玩家落在门外侧)。"); + report.Add("_facingDirectionOnArrive:A→B 时玩家朝向由 B 的该值决定,B→A 反之。"); + report.Add("若需门动画,将 AnimationClip 拖入各自的 _openClip / _enterClip。"); + + Undo.CollapseUndoOperations(undoGroup); + + // 选中 A(便于立即调整位置) + Selection.activeGameObject = goA; + MarkDirtyAndLog("Linked Door Pair (Same-Scene)", goA, report); + } + [MenuItem("BaseGames/Scene/Place/Camera Area", priority = 140)] public static void PlaceCameraArea() => PlaceCameraArea("CameraArea"); diff --git a/Assets/_Game/Scripts/World/LinkedDoorTransition.cs b/Assets/_Game/Scripts/World/LinkedDoorTransition.cs new file mode 100644 index 0000000..1be9a89 --- /dev/null +++ b/Assets/_Game/Scripts/World/LinkedDoorTransition.cs @@ -0,0 +1,186 @@ +using System.Collections; +using Animancer; +using UnityEngine; + +namespace BaseGames.World +{ + /// + /// 同场景成对门传送组件。两扇门互相引用,玩家进入其中一扇门时被传送到另一扇。 + /// + /// 触发流程: + /// + /// 玩家进入触发器或按交互键 → 播放 开门动画(本门) + /// 传送玩家到 的出生位置,并设定朝向 + /// 目标门播放 玩家走出动画 + /// + /// + /// 不涉及场景加载;适用于同房间内不同小区域之间的传送门、电梯、秘道等。 + /// + [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; + } + + /// 玩家从门内侧走出时的动画。由传送发起方在传送完成后调用。 + public void PlayEnterAnimation() + { + if (_animancer != null && _enterClip != null) + _animancer.Play(_enterClip); + } + + /// 启动传送冷却,在此期间本门不会再次触发传送。 + public void StartCooldown(float duration) + { + _cooldownUntil = Time.time + duration; + } + + // ── 辅助 ────────────────────────────────────────────────────────────── + + /// 返回传送目标门的出生世界坐标(含偏移)。 + 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(); + 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()); + } + } + } +}