feat: 添加场景过渡类型支持,优化场景加载逻辑

This commit is contained in:
2026-05-19 15:18:13 +08:00
parent be96b326de
commit ee0f659c97
12 changed files with 259 additions and 20 deletions

View File

@@ -56,6 +56,7 @@ namespace BaseGames.Core
{
SceneName = sm?.LastCheckpointScene,
EntryTransitionId = sm?.LastCheckpointSpawnId,
TransitionType = TransitionType.Scene,
ShowLoadingScreen = true,
IsRespawn = true,
});

View File

@@ -12,7 +12,10 @@ namespace BaseGames.Core.Events
public string EntryId;
/// <summary>玩家出生点 Transition ID具体过渡门 ID可为 null</summary>
public string EntryTransitionId;
/// <summary>是否显示加载画面</summary>
/// <summary>过渡类型,决定 <see cref="BaseGames.Core.SceneService"/> 的演出行为(淡出时长、加载画面等)。
/// 默认 <see cref="TransitionType.Room"/>,向后兼容旧请求。</summary>
public TransitionType TransitionType;
/// <summary>是否显示加载画面(由 TransitionType 自动推导,通常无需手动设置)。</summary>
public bool ShowLoadingScreen;
/// <summary>死亡复活时为 true不执行正常过渡动画</summary>
public bool IsRespawn;

View File

@@ -0,0 +1,16 @@
namespace BaseGames.Core.Events
{
/// <summary>
/// 场景过渡类型,决定 <see cref="BaseGames.Core.SceneService"/> 的演出行为。
/// </summary>
public enum TransitionType
{
/// <summary>同区域相邻房间切换。极短淡出≈0.05 s无加载画面相机硬切。
/// 适用于走廊边界、隐藏通道等玩家感知连续的场景边界。</summary>
Room,
/// <summary>跨大区域切换。完整淡出,显示加载画面。
/// 适用于地图间传送、返回标题、大区域入口等有明显空间跳跃感的切换。</summary>
Scene,
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7a9a71d44467e124ea9e7cda40603f30
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -19,7 +19,11 @@ namespace BaseGames.Core
}
/// <summary>
/// 场景管理服务。
/// 场景管理服务。根据 <see cref="SceneLoadRequest.TransitionType"/> 执行不同演出:
/// <list type="bullet">
/// <item><b>Room</b>:极短淡出(<see cref="_roomFadeDuration"/>),无加载画面。</item>
/// <item><b>Scene</b>:完整淡出(<see cref="_sceneFadeDuration"/>),显示加载画面。</item>
/// </list>
/// </summary>
[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
});

View File

@@ -30,6 +30,7 @@ namespace BaseGames.Challenge
{
SceneName = _challengeSceneName,
EntryTransitionId = string.Empty,
TransitionType = TransitionType.Scene,
ShowLoadingScreen = false,
IsRespawn = false,
});

View File

@@ -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,
});
}

View File

@@ -51,7 +51,8 @@ namespace BaseGames.UI
_uiManager.CloseTopPanel();
_onSceneLoadRequest?.Raise(new SceneLoadRequest
{
SceneName = "MainMenu",
SceneName = "MainMenu",
TransitionType = TransitionType.Scene,
ShowLoadingScreen = true
});
}

View File

@@ -0,0 +1,142 @@
using System.Collections;
using Animancer;
using BaseGames.Core.Events;
using UnityEngine;
namespace BaseGames.World
{
/// <summary>
/// 门过渡组件。挂载在有物理门对象的 GameObject 上。
/// <para>
/// 触发流程:
/// <list type="number">
/// <item>玩家进入触发器或按交互键 → 播放 <see cref="_openClip"/> 开门动画</item>
/// <item>动画完成后发出 <see cref="SceneLoadRequest"/>,类型由 <see cref="_transitionType"/> 决定</item>
/// <item>目标场景出生点侧的 <see cref="DoorTransition"/> 被 <see cref="PlayerSpawnPoint"/> 引用,
/// 由外部调用 <see cref="PlayEnterAnimation"/> 播放玩家进入门的过渡动画</item>
/// </list>
/// </para>
/// <para>
/// 若不需要门动画,直接使用 <see cref="RoomTransition"/> 即可。
/// </para>
/// </summary>
[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,
});
}
/// <summary>
/// 玩家从门内侧走出时的动画(由出生点侧的 <see cref="PlayerSpawnPoint"/> 在玩家出生后调用)。
/// 若未配置 <see cref="_enterClip"/> 则静默跳过。
/// </summary>
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<Collider2D>();
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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8a0d07af1ccd0284889188cf32516e1b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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;
/// <summary>玩家出生时的朝向(+1 右,-1 左)。</summary>
public int FacingDirection => _facingDirection;
public int FacingDirection => _facingDirection;
/// <summary>此出生点关联的出口门(可为 null。</summary>
public DoorTransition ExitDoor => _exitDoor;
/// <summary>
/// 触发出口门的进入动画(玩家从门内侧走出)。
/// 无关联门时静默跳过。由负责放置玩家的系统在玩家落点后调用。
/// </summary>
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);
}
}
}
}

View File

@@ -4,8 +4,15 @@ using UnityEngine;
namespace BaseGames.World
{
/// <summary>
/// 房间传送点。玩家进入触发器或按交互键时,广播 <see cref="SceneLoadRequest"/>
/// 由 SceneLoader 监听并执行 Additive 场景加载/卸载。
/// 房间/场景过渡触发器。玩家进入触发器或按交互键时,广播 <see cref="SceneLoadRequest"/>
/// <para>
/// 通过 <see cref="_transitionType"/> 控制过渡演出:
/// <list type="bullet">
/// <item><b>Room</b>:极短淡出,无加载画面,适合相邻房间边界。</item>
/// <item><b>Scene</b>:完整淡出 + 加载画面,适合大区域切换。</item>
/// </list>
/// 如需门动画,请改用 <see cref="DoorTransition"/> 组件,它会在动画完成后内部触发本组件。
/// </para>
/// </summary>
[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()
/// <summary>触发过渡。可由触发器、交互系统或 <see cref="DoorTransition"/> 调用。</summary>
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<Collider2D>();
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);
}
}
}