diff --git a/Assets/_Game/Scripts/Core/DeathRespawnService.cs b/Assets/_Game/Scripts/Core/DeathRespawnService.cs index 24baf42..e05d9f5 100644 --- a/Assets/_Game/Scripts/Core/DeathRespawnService.cs +++ b/Assets/_Game/Scripts/Core/DeathRespawnService.cs @@ -56,6 +56,7 @@ namespace BaseGames.Core { SceneName = sm?.LastCheckpointScene, EntryTransitionId = sm?.LastCheckpointSpawnId, + TransitionType = TransitionType.Scene, ShowLoadingScreen = true, IsRespawn = true, }); diff --git a/Assets/_Game/Scripts/Core/Events/SceneLoadRequest.cs b/Assets/_Game/Scripts/Core/Events/SceneLoadRequest.cs index cfb8a9f..ab52775 100644 --- a/Assets/_Game/Scripts/Core/Events/SceneLoadRequest.cs +++ b/Assets/_Game/Scripts/Core/Events/SceneLoadRequest.cs @@ -12,7 +12,10 @@ namespace BaseGames.Core.Events public string EntryId; /// 玩家出生点 Transition ID(具体过渡门 ID,可为 null) public string EntryTransitionId; - /// 是否显示加载画面 + /// 过渡类型,决定 的演出行为(淡出时长、加载画面等)。 + /// 默认 ,向后兼容旧请求。 + public TransitionType TransitionType; + /// 是否显示加载画面(由 TransitionType 自动推导,通常无需手动设置)。 public bool ShowLoadingScreen; /// 死亡复活时为 true,不执行正常过渡动画 public bool IsRespawn; diff --git a/Assets/_Game/Scripts/Core/Events/TransitionType.cs b/Assets/_Game/Scripts/Core/Events/TransitionType.cs new file mode 100644 index 0000000..223b780 --- /dev/null +++ b/Assets/_Game/Scripts/Core/Events/TransitionType.cs @@ -0,0 +1,16 @@ +namespace BaseGames.Core.Events +{ + /// + /// 场景过渡类型,决定 的演出行为。 + /// + public enum TransitionType + { + /// 同区域相邻房间切换。极短淡出(≈0.05 s),无加载画面,相机硬切。 + /// 适用于走廊边界、隐藏通道等玩家感知连续的场景边界。 + Room, + + /// 跨大区域切换。完整淡出,显示加载画面。 + /// 适用于地图间传送、返回标题、大区域入口等有明显空间跳跃感的切换。 + Scene, + } +} diff --git a/Assets/_Game/Scripts/Core/Events/TransitionType.cs.meta b/Assets/_Game/Scripts/Core/Events/TransitionType.cs.meta new file mode 100644 index 0000000..c142fb3 --- /dev/null +++ b/Assets/_Game/Scripts/Core/Events/TransitionType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a9a71d44467e124ea9e7cda40603f30 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/Core/SceneService.cs b/Assets/_Game/Scripts/Core/SceneService.cs index c91a876..98baa3d 100644 --- a/Assets/_Game/Scripts/Core/SceneService.cs +++ b/Assets/_Game/Scripts/Core/SceneService.cs @@ -19,7 +19,11 @@ namespace BaseGames.Core } /// - /// 场景管理服务。 + /// 场景管理服务。根据 执行不同演出: + /// + /// Room:极短淡出(),无加载画面。 + /// Scene:完整淡出(),显示加载画面。 + /// /// [DefaultExecutionOrder(-900)] public class SceneService : MonoBehaviour, ISceneService @@ -32,7 +36,13 @@ namespace BaseGames.Core [SerializeField] private VoidEventChannelSO _onFadeOutRequest; [SerializeField] private SceneLoader _sceneLoader; - [SerializeField] private float _fadeDuration = 0.3f; + + [Header("淡出时长")] + [Tooltip("Room 过渡:极短淡出,用于相邻房间边界切换(推荐 0.05 s)。")] + [SerializeField] private float _roomFadeDuration = 0.05f; + + [Tooltip("Scene 过渡:完整淡出,用于大区域/地图间切换(推荐 0.4 s)。")] + [SerializeField] private float _sceneFadeDuration = 0.4f; private readonly CompositeDisposable _subscriptions = new(); @@ -48,8 +58,13 @@ namespace BaseGames.Core public IEnumerator LoadSceneCoroutine(SceneLoadRequest request) { + float fadeDuration = request.TransitionType == TransitionType.Scene + ? _sceneFadeDuration + : _roomFadeDuration; + _onFadeOutRequest?.Raise(); - yield return new WaitForSeconds(_fadeDuration); + if (fadeDuration > 0f) + yield return new WaitForSeconds(fadeDuration); if (_sceneLoader != null) yield return StartCoroutine(_sceneLoader.LoadSceneCoroutine(request)); @@ -71,6 +86,7 @@ namespace BaseGames.Core { SceneName = AddressKeys.SceneMainMenu, EntryTransitionId = null, + TransitionType = TransitionType.Scene, ShowLoadingScreen = false, IsRespawn = false }); diff --git a/Assets/_Game/Scripts/Quest/ChallengeRoomTrigger.cs b/Assets/_Game/Scripts/Quest/ChallengeRoomTrigger.cs index d287ff7..74e4cd6 100644 --- a/Assets/_Game/Scripts/Quest/ChallengeRoomTrigger.cs +++ b/Assets/_Game/Scripts/Quest/ChallengeRoomTrigger.cs @@ -30,6 +30,7 @@ namespace BaseGames.Challenge { SceneName = _challengeSceneName, EntryTransitionId = string.Empty, + TransitionType = TransitionType.Scene, ShowLoadingScreen = false, IsRespawn = false, }); diff --git a/Assets/_Game/Scripts/Support/AntiSoftlock/AntiSoftlockSystem.cs b/Assets/_Game/Scripts/Support/AntiSoftlock/AntiSoftlockSystem.cs index 0547257..2524802 100644 --- a/Assets/_Game/Scripts/Support/AntiSoftlock/AntiSoftlockSystem.cs +++ b/Assets/_Game/Scripts/Support/AntiSoftlock/AntiSoftlockSystem.cs @@ -111,10 +111,11 @@ namespace BaseGames.Support.AntiSoftlock _onSceneLoadRequest?.Raise(new SceneLoadRequest { - SceneName = scene, - EntryId = spawn, + SceneName = scene, + EntryId = spawn, + TransitionType = TransitionType.Scene, ShowLoadingScreen = true, - IsRespawn = true, + IsRespawn = true, }); } diff --git a/Assets/_Game/Scripts/UI/Menus/PauseMenuController.cs b/Assets/_Game/Scripts/UI/Menus/PauseMenuController.cs index 9970d4e..48c4527 100644 --- a/Assets/_Game/Scripts/UI/Menus/PauseMenuController.cs +++ b/Assets/_Game/Scripts/UI/Menus/PauseMenuController.cs @@ -51,7 +51,8 @@ namespace BaseGames.UI _uiManager.CloseTopPanel(); _onSceneLoadRequest?.Raise(new SceneLoadRequest { - SceneName = "MainMenu", + SceneName = "MainMenu", + TransitionType = TransitionType.Scene, ShowLoadingScreen = true }); } diff --git a/Assets/_Game/Scripts/World/DoorTransition.cs b/Assets/_Game/Scripts/World/DoorTransition.cs new file mode 100644 index 0000000..98cdd45 --- /dev/null +++ b/Assets/_Game/Scripts/World/DoorTransition.cs @@ -0,0 +1,142 @@ +using System.Collections; +using Animancer; +using BaseGames.Core.Events; +using UnityEngine; + +namespace BaseGames.World +{ + /// + /// 门过渡组件。挂载在有物理门对象的 GameObject 上。 + /// + /// 触发流程: + /// + /// 玩家进入触发器或按交互键 → 播放 开门动画 + /// 动画完成后发出 ,类型由 决定 + /// 目标场景出生点侧的 引用, + /// 由外部调用 播放玩家进入门的过渡动画 + /// + /// + /// + /// 若不需要门动画,直接使用 即可。 + /// + /// + [RequireComponent(typeof(Collider2D))] + public class DoorTransition : MonoBehaviour, IInteractable + { + [Header("目标")] + [SerializeField] private string _targetSceneAddress; + [SerializeField] private string _targetTransitionId; + + [Header("过渡类型")] + [Tooltip("Room:极短淡出,无加载画面。\nScene:完整淡出 + 加载画面。")] + [SerializeField] private TransitionType _transitionType = TransitionType.Room; + + [Header("触发方式")] + [Tooltip("true = 玩家进入触发器自动触发;false = 需要玩家按交互键。")] + [SerializeField] private bool _autoTrigger = false; + + [Header("门动画")] + [SerializeField] private AnimancerComponent _animancer; + + [Tooltip("玩家从外侧进入时播放的开门动画(出口侧)。留空则跳过动画直接过渡。")] + [SerializeField] private AnimationClip _openClip; + + [Tooltip("玩家从内侧走出时播放的动画(入口侧,由 PlayerSpawnPoint 在玩家出生后触发)。留空则跳过。")] + [SerializeField] private AnimationClip _enterClip; + + [Header("钥匙物品校验")] + [SerializeField] private bool _requiresKeyItem; + [SerializeField] private string _requiredItemId; + + [Header("事件频道")] + [SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest; + + [Header("世界状态")] + [SerializeField] private WorldStateRegistry _worldState; + + private bool _triggered; + + // ── IInteractable ───────────────────────────────────────────────────── + + public bool CanInteract => !_autoTrigger; + public string InteractPrompt => "进入"; + + public void Interact(Transform player) => TryTrigger(); + + public void OnPlayerEnterRange(Transform player) { } + public void OnPlayerExitRange() { } + + // ── 触发 ────────────────────────────────────────────────────────────── + + private void OnTriggerEnter2D(Collider2D other) + { + if (!_autoTrigger) return; + if (!other.CompareTag("Player")) return; + TryTrigger(); + } + + private void TryTrigger() + { + if (_triggered) return; + if (_requiresKeyItem && !HasItem(_requiredItemId)) return; + _triggered = true; + StartCoroutine(OpenAndTransition()); + } + + private void OnDisable() => _triggered = false; + + // ── 动画流程 ────────────────────────────────────────────────────────── + + private IEnumerator OpenAndTransition() + { + if (_animancer != null && _openClip != null) + { + var state = _animancer.Play(_openClip); + yield return state; // 等待动画完成 + } + + _onSceneLoadRequest?.Raise(new SceneLoadRequest + { + SceneName = _targetSceneAddress, + EntryTransitionId = _targetTransitionId, + TransitionType = _transitionType, + ShowLoadingScreen = _transitionType == TransitionType.Scene, + IsRespawn = false, + }); + } + + /// + /// 玩家从门内侧走出时的动画(由出生点侧的 在玩家出生后调用)。 + /// 若未配置 则静默跳过。 + /// + public void PlayEnterAnimation() + { + if (_animancer != null && _enterClip != null) + _animancer.Play(_enterClip); + } + + // ── 辅助 ────────────────────────────────────────────────────────────── + + private bool HasItem(string itemId) + { + if (string.IsNullOrEmpty(itemId)) return true; + if (_worldState == null) + { + Debug.LogWarning($"[DoorTransition] WorldStateRegistry 未配置,钥匙 {itemId} 检查跳过"); + return false; + } + return _worldState.IsCollected(itemId); + } + + private void OnDrawGizmos() + { + var col = GetComponent(); + if (col == null) return; + // 紫色区分门过渡 + Gizmos.color = new Color(0.7f, 0.3f, 1f, 0.7f); + Gizmos.DrawWireCube(transform.position, col.bounds.size); + Gizmos.color = new Color(0.7f, 0.3f, 1f, 0.3f); + Gizmos.DrawCube(transform.position, col.bounds.size); + } + } +} diff --git a/Assets/_Game/Scripts/World/DoorTransition.cs.meta b/Assets/_Game/Scripts/World/DoorTransition.cs.meta new file mode 100644 index 0000000..6678d53 --- /dev/null +++ b/Assets/_Game/Scripts/World/DoorTransition.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8a0d07af1ccd0284889188cf32516e1b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/World/PlayerSpawnPoint.cs b/Assets/_Game/Scripts/World/PlayerSpawnPoint.cs index 985f7f1..e187b47 100644 --- a/Assets/_Game/Scripts/World/PlayerSpawnPoint.cs +++ b/Assets/_Game/Scripts/World/PlayerSpawnPoint.cs @@ -12,16 +12,36 @@ namespace BaseGames.World [Tooltip("+1 = 朝右出生,-1 = 朝左出生")] [SerializeField] private int _facingDirection = 1; - public string TransitionId => _transitionId; - public Vector2 SpawnPosition => transform.position; + [Tooltip("(可选)此出生点对应的出口门。\n" + + "玩家在此出生后,调用 TriggerExitDoor() 播放门的进入动画(玩家从门里走出)。\n" + + "仅在使用 DoorTransition 的场景中需要配置。")] + [SerializeField] private DoorTransition _exitDoor; + + public string TransitionId => _transitionId; + public Vector2 SpawnPosition => transform.position; /// 玩家出生时的朝向(+1 右,-1 左)。 - public int FacingDirection => _facingDirection; + public int FacingDirection => _facingDirection; + /// 此出生点关联的出口门(可为 null)。 + public DoorTransition ExitDoor => _exitDoor; + + /// + /// 触发出口门的进入动画(玩家从门内侧走出)。 + /// 无关联门时静默跳过。由负责放置玩家的系统在玩家落点后调用。 + /// + public void TriggerExitDoor() => _exitDoor?.PlayEnterAnimation(); private void OnDrawGizmos() { Gizmos.color = Color.green; Gizmos.DrawWireSphere(transform.position, 0.3f); Gizmos.DrawLine(transform.position, transform.position + Vector3.up * 0.5f); + + // 门关联指示线 + if (_exitDoor != null) + { + Gizmos.color = new Color(0.7f, 0.3f, 1f, 0.6f); + Gizmos.DrawLine(transform.position, _exitDoor.transform.position); + } } } } diff --git a/Assets/_Game/Scripts/World/RoomTransition.cs b/Assets/_Game/Scripts/World/RoomTransition.cs index e096418..5359447 100644 --- a/Assets/_Game/Scripts/World/RoomTransition.cs +++ b/Assets/_Game/Scripts/World/RoomTransition.cs @@ -4,8 +4,15 @@ using UnityEngine; namespace BaseGames.World { /// - /// 房间传送点。玩家进入触发器或按交互键时,广播 , - /// 由 SceneLoader 监听并执行 Additive 场景加载/卸载。 + /// 房间/场景过渡触发器。玩家进入触发器或按交互键时,广播 。 + /// + /// 通过 控制过渡演出: + /// + /// Room:极短淡出,无加载画面,适合相邻房间边界。 + /// Scene:完整淡出 + 加载画面,适合大区域切换。 + /// + /// 如需门动画,请改用 组件,它会在动画完成后内部触发本组件。 + /// /// [RequireComponent(typeof(Collider2D))] public class RoomTransition : MonoBehaviour, IInteractable @@ -17,6 +24,11 @@ namespace BaseGames.World [SerializeField] private string _targetSceneAddress; // Addressable key(目标场景) [SerializeField] private string _targetTransitionId; // 目标房间出生点 ID + [Header("过渡类型")] + [Tooltip("Room:极短淡出,无加载画面,相邻房间边界专用。\n" + + "Scene:完整淡出 + 加载画面,大区域/地图间切换专用。")] + [SerializeField] private TransitionType _transitionType = TransitionType.Room; + [Header("触发方式")] [SerializeField] private bool _autoTrigger = true; // true = 玩家进入触发器自动触发 @@ -32,7 +44,7 @@ namespace BaseGames.World // ── IInteractable ───────────────────────────────────────────────────── public bool CanInteract => !_autoTrigger; - public string InteractPrompt => "前往下一区域"; + public string InteractPrompt => _transitionType == TransitionType.Scene ? "前往下一区域" : "进入"; public void Interact(Transform player) => RequestTransition(); @@ -47,7 +59,8 @@ namespace BaseGames.World RequestTransition(); } - private void RequestTransition() + /// 触发过渡。可由触发器、交互系统或 调用。 + public void RequestTransition() { if (_requiresKeyItem && !HasItem(_requiredItemId)) return; @@ -55,7 +68,8 @@ namespace BaseGames.World { SceneName = _targetSceneAddress, EntryTransitionId = _targetTransitionId, - ShowLoadingScreen = true, + TransitionType = _transitionType, + ShowLoadingScreen = _transitionType == TransitionType.Scene, IsRespawn = false, }); } @@ -66,7 +80,7 @@ namespace BaseGames.World if (string.IsNullOrEmpty(itemId)) return true; if (_worldState == null) { - Debug.LogWarning($"[RoomTransition] WorldStateRegistry 未配置,销 {itemId} 检查跳过"); + Debug.LogWarning($"[RoomTransition] WorldStateRegistry 未配置,钥匙 {itemId} 检查跳过"); return false; } return _worldState.IsCollected(itemId); @@ -74,10 +88,12 @@ namespace BaseGames.World private void OnDrawGizmos() { - Gizmos.color = new Color(0f, 1f, 0.5f, 0.6f); var col = GetComponent(); - if (col != null) - Gizmos.DrawWireCube(transform.position, col.bounds.size); + if (col == null) return; + Gizmos.color = _transitionType == TransitionType.Scene + ? new Color(1f, 0.6f, 0f, 0.6f) // 橙色:大区域切换 + : new Color(0f, 1f, 0.5f, 0.6f); // 绿色:房间切换 + Gizmos.DrawWireCube(transform.position, col.bounds.size); } } }