feat(scene): 实现场景路由守卫和过渡处理器管道

- 添加场景路由守卫机制,支持进入和离开场景的权限检查
- 实现场景过渡处理器管道,支持BeforeChange和AfterChange阶段处理
- 新增LoadingProgressHandler和LoggingTransitionHandler处理器
- 添加SceneTransitionPhases和SceneTransitionType枚举定义
- 实现ISceneRouteGuard、ISceneTransitionHandler等核心接口
- 在SceneRouterBase中集成守卫检查和处理器管道功能
- 重构场景切换逻辑,添加事件驱动的过渡处理机制
This commit is contained in:
GeWuYou 2026-02-15 14:29:33 +08:00 committed by gewuyou
parent 65d56d0696
commit b054ee1c4a
12 changed files with 1058 additions and 5 deletions

View File

@ -0,0 +1,42 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace GFramework.Game.Abstractions.enums;
/// <summary>
/// 场景过渡阶段枚举,定义了场景切换过程中的不同执行阶段。
/// 使用 Flags 特性支持组合多个阶段。
/// </summary>
[Flags]
public enum SceneTransitionPhases
{
/// <summary>
/// 场景切换前阶段(阻塞执行)。
/// 用于执行需要在场景切换前完成的操作,如显示加载动画、弹出确认对话框等。
/// 此阶段的处理器会阻塞场景切换流程,直到所有处理器执行完成。
/// </summary>
BeforeChange = 1,
/// <summary>
/// 场景切换后阶段(非阻塞执行)。
/// 用于执行场景切换后的后续操作,如播放音效、记录日志、发送统计数据等。
/// 此阶段的处理器异步执行,不会阻塞场景切换流程。
/// </summary>
AfterChange = 2,
/// <summary>
/// 所有阶段的组合标志。
/// 表示处理器适用于场景切换的所有阶段。
/// </summary>
All = BeforeChange | AfterChange
}

View File

@ -0,0 +1,44 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace GFramework.Game.Abstractions.enums;
/// <summary>
/// 场景过渡类型枚举,定义了场景切换的不同操作模式。
/// </summary>
public enum SceneTransitionType
{
/// <summary>
/// 压入新场景到场景栈顶部。
/// 当前场景会被暂停,新场景成为活动场景。
/// </summary>
Push,
/// <summary>
/// 弹出当前场景并恢复下一个场景。
/// 当前场景会被卸载,栈中的下一个场景变为活动场景。
/// </summary>
Pop,
/// <summary>
/// 替换所有场景,清空整个场景栈并加载新场景。
/// 此操作会卸载所有现有场景,然后加载指定的新场景。
/// </summary>
Replace,
/// <summary>
/// 清空所有已加载的场景。
/// 卸载场景栈中的所有场景,使系统回到无场景状态。
/// </summary>
Clear
}

View File

@ -0,0 +1,45 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace GFramework.Game.Abstractions.scene;
/// <summary>
/// 场景加载进度接口,用于跟踪和报告场景资源的加载状态。
/// 实现此接口的类可以提供场景加载的实时进度信息。
/// </summary>
public interface ISceneLoadingProgress
{
/// <summary>
/// 获取正在加载的场景的唯一标识符。
/// </summary>
string SceneKey { get; }
/// <summary>
/// 获取当前加载进度,范围为 0.0 到 1.0。
/// 0.0 表示刚开始加载1.0 表示加载完成。
/// </summary>
float Progress { get; }
/// <summary>
/// 获取当前加载阶段的描述信息。
/// 例如:"加载纹理资源"、"初始化场景对象"等。
/// 如果没有具体信息则返回 null。
/// </summary>
string? Message { get; }
/// <summary>
/// 获取加载是否已完成的状态。
/// true 表示场景资源已全部加载完成false 表示仍在加载中。
/// </summary>
bool IsCompleted { get; }
}

View File

@ -0,0 +1,50 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace GFramework.Game.Abstractions.scene;
/// <summary>
/// 场景路由守卫接口,用于在场景切换前进行权限检查和条件验证。
/// 实现此接口可以拦截场景的进入和离开操作。
/// </summary>
public interface ISceneRouteGuard
{
/// <summary>
/// 获取守卫的执行优先级。
/// 数值越小优先级越高,越先执行。
/// 建议范围:-1000 到 1000。
/// </summary>
int Priority { get; }
/// <summary>
/// 获取守卫是否可以中断后续守卫的执行。
/// true 表示当前守卫通过后,可以跳过后续守卫直接允许操作。
/// false 表示即使当前守卫通过,仍需执行所有后续守卫。
/// </summary>
bool CanInterrupt { get; }
/// <summary>
/// 异步检查是否允许进入指定场景。
/// </summary>
/// <param name="sceneKey">目标场景的唯一标识符。</param>
/// <param name="param">场景进入参数,可能包含初始化数据或上下文信息。</param>
/// <returns>如果允许进入则返回 true否则返回 false。</returns>
Task<bool> CanEnterAsync(string sceneKey, ISceneEnterParam? param);
/// <summary>
/// 异步检查是否允许离开指定场景。
/// </summary>
/// <param name="sceneKey">当前场景的唯一标识符。</param>
/// <returns>如果允许离开则返回 true否则返回 false。</returns>
Task<bool> CanLeaveAsync(string sceneKey);
}

View File

@ -0,0 +1,52 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using GFramework.Game.Abstractions.enums;
namespace GFramework.Game.Abstractions.scene;
/// <summary>
/// 场景过渡处理器接口,定义了场景切换过程中的扩展点。
/// 实现此接口可以在场景切换的不同阶段执行自定义逻辑。
/// </summary>
public interface ISceneTransitionHandler
{
/// <summary>
/// 获取处理器的执行优先级。
/// 数值越小优先级越高,越先执行。
/// 建议范围:-1000 到 1000。
/// </summary>
int Priority { get; }
/// <summary>
/// 获取处理器适用的场景过渡阶段。
/// 可以使用 Flags 组合多个阶段(如 BeforeChange | AfterChange
/// </summary>
SceneTransitionPhases Phases { get; }
/// <summary>
/// 判断处理器是否应该处理当前场景过渡事件。
/// </summary>
/// <param name="event">场景过渡事件。</param>
/// <param name="phases">当前执行的阶段。</param>
/// <returns>如果应该处理则返回 true否则返回 false。</returns>
bool ShouldHandle(SceneTransitionEvent @event, SceneTransitionPhases phases);
/// <summary>
/// 异步处理场景过渡事件。
/// </summary>
/// <param name="event">场景过渡事件,包含切换的上下文信息。</param>
/// <param name="cancellationToken">取消令牌,用于支持操作取消。</param>
/// <returns>表示处理操作完成的异步任务。</returns>
Task HandleAsync(SceneTransitionEvent @event, CancellationToken cancellationToken);
}

View File

@ -0,0 +1,115 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using GFramework.Game.Abstractions.enums;
namespace GFramework.Game.Abstractions.scene;
/// <summary>
/// 场景过渡事件,封装了场景切换过程中的上下文信息。
/// 提供了场景切换的元数据和自定义上下文数据的存储能力。
/// </summary>
public sealed class SceneTransitionEvent
{
private readonly Dictionary<string, object> _context = new();
/// <summary>
/// 获取或初始化源场景的唯一标识符。
/// 表示切换前的场景,如果是首次加载场景则为 null。
/// </summary>
public string? FromSceneKey { get; init; }
/// <summary>
/// 获取或初始化目标场景的唯一标识符。
/// 表示切换后的场景,如果是清空操作则为 null。
/// </summary>
public string? ToSceneKey { get; init; }
/// <summary>
/// 获取或初始化场景过渡类型。
/// 指示当前执行的是哪种场景切换操作Push/Pop/Replace/Clear
/// </summary>
public SceneTransitionType TransitionType { get; init; }
/// <summary>
/// 获取或初始化场景进入参数。
/// 包含传递给新场景的初始化数据或上下文信息。
/// </summary>
public ISceneEnterParam? EnterParam { get; init; }
/// <summary>
/// 从上下文中获取指定键的值。
/// </summary>
/// <typeparam name="T">值的类型。</typeparam>
/// <param name="key">上下文键。</param>
/// <param name="defaultValue">如果键不存在时返回的默认值。</param>
/// <returns>上下文中存储的值,如果键不存在则返回默认值。</returns>
public T Get<T>(string key, T defaultValue = default!)
{
if (_context.TryGetValue(key, out var value) && value is T typedValue)
return typedValue;
return defaultValue;
}
/// <summary>
/// 设置上下文中指定键的值。
/// </summary>
/// <typeparam name="T">值的类型。</typeparam>
/// <param name="key">上下文键。</param>
/// <param name="value">要存储的值。</param>
public void Set<T>(string key, T value)
{
if (key == null)
throw new ArgumentNullException(nameof(key));
_context[key] = value!;
}
/// <summary>
/// 尝试从上下文中获取指定键的值。
/// </summary>
/// <typeparam name="T">值的类型。</typeparam>
/// <param name="key">上下文键。</param>
/// <param name="value">输出参数,如果键存在则包含对应的值。</param>
/// <returns>如果键存在且类型匹配则返回 true否则返回 false。</returns>
public bool TryGet<T>(string key, out T value)
{
if (_context.TryGetValue(key, out var obj) && obj is T typedValue)
{
value = typedValue;
return true;
}
value = default!;
return false;
}
/// <summary>
/// 检查上下文中是否存在指定的键。
/// </summary>
/// <param name="key">上下文键。</param>
/// <returns>如果键存在则返回 true否则返回 false。</returns>
public bool Has(string key)
{
return _context.ContainsKey(key);
}
/// <summary>
/// 从上下文中移除指定的键。
/// </summary>
/// <param name="key">上下文键。</param>
/// <returns>如果键存在并成功移除则返回 true否则返回 false。</returns>
public bool Remove(string key)
{
return _context.Remove(key);
}
}

View File

@ -0,0 +1,32 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace GFramework.Game.Abstractions.scene;
/// <summary>
/// 场景过渡处理器选项,定义了处理器执行时的配置参数。
/// </summary>
/// <param name="TimeoutMs">
/// 处理器执行的超时时间(毫秒)。
/// 设置为 0 表示无超时限制。
/// 如果处理器执行超过此时间,将触发超时异常。
/// </param>
/// <param name="ContinueOnError">
/// 当处理器执行失败时是否继续执行后续处理器。
/// true 表示即使当前处理器失败,管道仍会继续执行后续处理器。
/// false 表示当前处理器失败时,管道会立即停止并抛出异常。
/// </param>
public record SceneTransitionHandlerOptions(
int TimeoutMs = 0,
bool ContinueOnError = true
);

View File

@ -12,8 +12,10 @@
// limitations under the License.
using GFramework.Core.Abstractions.logging;
using GFramework.Core.extensions;
using GFramework.Core.logging;
using GFramework.Core.system;
using GFramework.Game.Abstractions.enums;
using GFramework.Game.Abstractions.scene;
namespace GFramework.Game.scene;
@ -28,20 +30,43 @@ public abstract class SceneRouterBase
private static readonly ILogger Log =
LoggerFactoryResolver.Provider.CreateLogger(nameof(SceneRouterBase));
private readonly List<ISceneRouteGuard> _guards = new();
private readonly SceneTransitionPipeline _pipeline = new();
private readonly Stack<ISceneBehavior> _stack = new();
private readonly SemaphoreSlim _transitionLock = new(1, 1);
private ISceneFactory _factory = null!;
/// <summary>
/// 场景根节点
/// </summary>
protected ISceneRoot? Root;
/// <summary>
/// 获取当前场景行为对象。
/// </summary>
public ISceneBehavior? Current => _stack.Count > 0 ? _stack.Peek() : null;
/// <summary>
/// 获取当前场景的键名。
/// </summary>
public string? CurrentKey => Current?.Key;
/// <summary>
/// 获取场景栈的只读列表,按压入顺序排列。
/// </summary>
public IReadOnlyList<ISceneBehavior> Stack =>
_stack.Reverse().ToList();
/// <summary>
/// 获取是否正在进行场景转换。
/// </summary>
public bool IsTransitioning { get; private set; }
/// <summary>
/// 绑定场景根节点。
/// </summary>
/// <param name="root">场景根节点实例。</param>
public void BindRoot(ISceneRoot root)
{
Root = root;
@ -50,6 +75,12 @@ public abstract class SceneRouterBase
#region Replace
/// <summary>
/// 替换当前场景为指定场景。
/// </summary>
/// <param name="sceneKey">目标场景键名。</param>
/// <param name="param">场景进入参数。</param>
/// <returns>异步任务。</returns>
public async ValueTask ReplaceAsync(
string sceneKey,
ISceneEnterParam? param = null)
@ -59,10 +90,12 @@ public abstract class SceneRouterBase
{
IsTransitioning = true;
Log.Debug("Replace Scene: {0}", sceneKey);
var @event = CreateEvent(sceneKey, SceneTransitionType.Replace, param);
await BeforeChangeAsync(@event);
await ClearInternalAsync();
await PushInternalAsync(sceneKey, param);
AfterChange(@event);
}
finally
{
@ -75,6 +108,11 @@ public abstract class SceneRouterBase
#region Query
/// <summary>
/// 检查指定场景是否在栈中。
/// </summary>
/// <param name="sceneKey">场景键名。</param>
/// <returns>如果场景在栈中返回true否则返回false。</returns>
public bool Contains(string sceneKey)
{
return _stack.Any(s => s.Key == sceneKey);
@ -82,8 +120,84 @@ public abstract class SceneRouterBase
#endregion
/// <summary>
/// 注册场景过渡处理器。
/// </summary>
/// <param name="handler">处理器实例。</param>
/// <param name="options">执行选项。</param>
public void RegisterHandler(ISceneTransitionHandler handler, SceneTransitionHandlerOptions? options = null)
{
_pipeline.RegisterHandler(handler, options);
}
/// <summary>
/// 注销场景过渡处理器。
/// </summary>
/// <param name="handler">处理器实例。</param>
public void UnregisterHandler(ISceneTransitionHandler handler)
{
_pipeline.UnregisterHandler(handler);
}
/// <summary>
/// 添加场景路由守卫。
/// </summary>
/// <param name="guard">守卫实例。</param>
public void AddGuard(ISceneRouteGuard guard)
{
ArgumentNullException.ThrowIfNull(guard);
if (!_guards.Contains(guard))
{
_guards.Add(guard);
_guards.Sort((a, b) => a.Priority.CompareTo(b.Priority));
Log.Debug("Guard added: {0}, Priority={1}", guard.GetType().Name, guard.Priority);
}
}
/// <summary>
/// 添加场景路由守卫(泛型版本)。
/// </summary>
/// <typeparam name="T">守卫类型。</typeparam>
public void AddGuard<T>() where T : ISceneRouteGuard, new()
{
AddGuard(new T());
}
/// <summary>
/// 移除场景路由守卫。
/// </summary>
/// <param name="guard">守卫实例。</param>
public void RemoveGuard(ISceneRouteGuard guard)
{
if (_guards.Remove(guard))
{
Log.Debug("Guard removed: {0}", guard.GetType().Name);
}
}
/// <summary>
/// 注册场景过渡处理器的抽象方法,由子类实现。
/// </summary>
protected abstract void RegisterHandlers();
/// <summary>
/// 系统初始化方法,获取场景工厂并注册处理器。
/// </summary>
protected override void OnInit()
{
_factory = this.GetUtility<ISceneFactory>()!;
Log.Debug("SceneRouterBase initialized. Factory={0}", _factory.GetType().Name);
RegisterHandlers();
}
#region Push
/// <summary>
/// 将指定场景推入栈顶。
/// </summary>
/// <param name="sceneKey">目标场景键名。</param>
/// <param name="param">场景进入参数。</param>
/// <returns>异步任务。</returns>
public async ValueTask PushAsync(
string sceneKey,
ISceneEnterParam? param = null)
@ -92,7 +206,12 @@ public abstract class SceneRouterBase
try
{
IsTransitioning = true;
var @event = CreateEvent(sceneKey, SceneTransitionType.Push, param);
await BeforeChangeAsync(@event);
await PushInternalAsync(sceneKey, param);
AfterChange(@event);
}
finally
{
@ -101,6 +220,13 @@ public abstract class SceneRouterBase
}
}
/// <summary>
/// 内部推送场景实现方法。
/// 执行守卫检查、场景加载、暂停当前场景、压入栈等操作。
/// </summary>
/// <param name="sceneKey">场景键名。</param>
/// <param name="param">场景进入参数。</param>
/// <returns>异步任务。</returns>
private async ValueTask PushInternalAsync(
string sceneKey,
ISceneEnterParam? param)
@ -111,18 +237,31 @@ public abstract class SceneRouterBase
return;
}
// 守卫检查
if (!await ExecuteEnterGuardsAsync(sceneKey, param))
{
Log.Warn("Push blocked by guard: {0}", sceneKey);
return;
}
// 通过 Root 加载场景Root.LoadAsync 返回 ISceneBehavior
var scene = await Root!.LoadAsync(sceneKey);
// 加载资源
await scene.OnLoadAsync(param);
// 暂停当前场景
if (_stack.Count > 0)
{
var current = _stack.Peek();
await current.OnPauseAsync();
}
// 压入栈
_stack.Push(scene);
await scene.OnEnterAsync(param);
await scene.OnShowAsync();
// 进入场景
await scene.OnEnterAsync();
Log.Debug("Push Scene: {0}, stackCount={1}",
sceneKey, _stack.Count);
@ -132,13 +271,22 @@ public abstract class SceneRouterBase
#region Pop
/// <summary>
/// 弹出栈顶场景。
/// </summary>
/// <returns>异步任务。</returns>
public async ValueTask PopAsync()
{
await _transitionLock.WaitAsync();
try
{
IsTransitioning = true;
var @event = CreateEvent(null, SceneTransitionType.Pop);
await BeforeChangeAsync(@event);
await PopInternalAsync();
AfterChange(@event);
}
finally
{
@ -147,21 +295,41 @@ public abstract class SceneRouterBase
}
}
/// <summary>
/// 内部弹出场景实现方法。
/// 执行守卫检查、退出场景、卸载资源、恢复下一个场景等操作。
/// </summary>
/// <returns>异步任务。</returns>
private async ValueTask PopInternalAsync()
{
if (_stack.Count == 0)
return;
var top = _stack.Pop();
var top = _stack.Peek();
// 守卫检查
if (!await ExecuteLeaveGuardsAsync(top.Key))
{
Log.Warn("Pop blocked by guard: {0}", top.Key);
return;
}
_stack.Pop();
// 退出场景
await top.OnExitAsync();
// 卸载资源
await top.OnUnloadAsync();
// 从场景树卸载
await Root!.UnloadAsync(top);
// 恢复下一个场景
if (_stack.Count > 0)
{
var next = _stack.Peek();
await next.OnResumeAsync();
await next.OnShowAsync();
}
Log.Debug("Pop Scene, stackCount={0}", _stack.Count);
@ -171,13 +339,22 @@ public abstract class SceneRouterBase
#region Clear
/// <summary>
/// 清空所有场景栈。
/// </summary>
/// <returns>异步任务。</returns>
public async ValueTask ClearAsync()
{
await _transitionLock.WaitAsync();
try
{
IsTransitioning = true;
var @event = CreateEvent(null, SceneTransitionType.Clear);
await BeforeChangeAsync(@event);
await ClearInternalAsync();
AfterChange(@event);
}
finally
{
@ -186,6 +363,11 @@ public abstract class SceneRouterBase
}
}
/// <summary>
/// 内部清空场景栈实现方法。
/// 循环调用弹出操作直到栈为空。
/// </summary>
/// <returns>异步任务。</returns>
private async ValueTask ClearInternalAsync()
{
while (_stack.Count > 0)
@ -195,4 +377,141 @@ public abstract class SceneRouterBase
}
#endregion
#region Helper Methods
/// <summary>
/// 创建场景转换事件对象。
/// </summary>
/// <param name="toSceneKey">目标场景键名。</param>
/// <param name="type">转换类型。</param>
/// <param name="param">进入参数。</param>
/// <returns>场景转换事件实例。</returns>
private SceneTransitionEvent CreateEvent(
string? toSceneKey,
SceneTransitionType type,
ISceneEnterParam? param = null)
{
return new SceneTransitionEvent
{
FromSceneKey = CurrentKey,
ToSceneKey = toSceneKey,
TransitionType = type,
EnterParam = param
};
}
/// <summary>
/// 执行转换前阶段的处理逻辑。
/// </summary>
/// <param name="event">场景转换事件。</param>
/// <returns>异步任务。</returns>
private async Task BeforeChangeAsync(SceneTransitionEvent @event)
{
Log.Debug("BeforeChange phases started: {0}", @event.TransitionType);
await _pipeline.ExecuteAsync(@event, SceneTransitionPhases.BeforeChange);
Log.Debug("BeforeChange phases completed: {0}", @event.TransitionType);
}
/// <summary>
/// 执行转换后阶段的处理逻辑。
/// 在后台线程中异步执行,避免阻塞主线程。
/// </summary>
/// <param name="event">场景转换事件。</param>
private void AfterChange(SceneTransitionEvent @event)
{
Log.Debug("AfterChange phases started: {0}", @event.TransitionType);
_ = Task.Run(async () =>
{
try
{
await _pipeline.ExecuteAsync(@event, SceneTransitionPhases.AfterChange);
Log.Debug("AfterChange phases completed: {0}", @event.TransitionType);
}
catch (Exception ex)
{
Log.Error("AfterChange phases failed: {0}, Error: {1}",
@event.TransitionType, ex.Message);
}
});
}
/// <summary>
/// 执行进入场景的守卫检查。
/// 按优先级顺序执行所有守卫的CanEnterAsync方法。
/// </summary>
/// <param name="sceneKey">场景键名。</param>
/// <param name="param">进入参数。</param>
/// <returns>如果所有守卫都允许进入返回true否则返回false。</returns>
private async Task<bool> ExecuteEnterGuardsAsync(string sceneKey, ISceneEnterParam? param)
{
foreach (var guard in _guards)
{
try
{
Log.Debug("Executing enter guard: {0} for {1}", guard.GetType().Name, sceneKey);
var canEnter = await guard.CanEnterAsync(sceneKey, param);
if (!canEnter)
{
Log.Debug("Enter guard blocked: {0}", guard.GetType().Name);
return false;
}
if (guard.CanInterrupt)
{
Log.Debug("Enter guard {0} passed, can interrupt = true", guard.GetType().Name);
return true;
}
}
catch (Exception ex)
{
Log.Error("Enter guard {0} failed: {1}", guard.GetType().Name, ex.Message);
if (guard.CanInterrupt)
return false;
}
}
return true;
}
/// <summary>
/// 执行离开场景的守卫检查。
/// 按优先级顺序执行所有守卫的CanLeaveAsync方法。
/// </summary>
/// <param name="sceneKey">场景键名。</param>
/// <returns>如果所有守卫都允许离开返回true否则返回false。</returns>
private async Task<bool> ExecuteLeaveGuardsAsync(string sceneKey)
{
foreach (var guard in _guards)
{
try
{
Log.Debug("Executing leave guard: {0} for {1}", guard.GetType().Name, sceneKey);
var canLeave = await guard.CanLeaveAsync(sceneKey);
if (!canLeave)
{
Log.Debug("Leave guard blocked: {0}", guard.GetType().Name);
return false;
}
if (guard.CanInterrupt)
{
Log.Debug("Leave guard {0} passed, can interrupt = true", guard.GetType().Name);
return true;
}
}
catch (Exception ex)
{
Log.Error("Leave guard {0} failed: {1}", guard.GetType().Name, ex.Message);
if (guard.CanInterrupt)
return false;
}
}
return true;
}
#endregion
}

View File

@ -0,0 +1,182 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging;
using GFramework.Game.Abstractions.enums;
using GFramework.Game.Abstractions.scene;
namespace GFramework.Game.scene;
/// <summary>
/// 场景过渡处理器管道,负责管理和执行场景切换扩展点。
/// 提供了处理器的注册、注销和按优先级顺序执行的功能。
/// </summary>
public class SceneTransitionPipeline
{
private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger("SceneTransitionPipeline");
private readonly List<ISceneTransitionHandler> _handlers = [];
private readonly Dictionary<ISceneTransitionHandler, SceneTransitionHandlerOptions> _options = new();
/// <summary>
/// 注册场景过渡处理器。
/// </summary>
/// <param name="handler">处理器实例。</param>
/// <param name="options">执行选项,如果为 null 则使用默认选项。</param>
public void RegisterHandler(ISceneTransitionHandler handler, SceneTransitionHandlerOptions? options = null)
{
ArgumentNullException.ThrowIfNull(handler);
if (_handlers.Contains(handler))
{
Log.Debug("Handler already registered: {0}", handler.GetType().Name);
return;
}
_handlers.Add(handler);
_options[handler] = options ?? new SceneTransitionHandlerOptions();
Log.Debug(
"Handler registered: {0}, Priority={1}, Phases={2}, TimeoutMs={3}",
handler.GetType().Name,
handler.Priority,
handler.Phases,
_options[handler].TimeoutMs
);
}
/// <summary>
/// 注销场景过渡处理器。
/// </summary>
/// <param name="handler">处理器实例。</param>
public void UnregisterHandler(ISceneTransitionHandler handler)
{
ArgumentNullException.ThrowIfNull(handler);
if (!_handlers.Remove(handler)) return;
_options.Remove(handler);
Log.Debug("Handler unregistered: {0}", handler.GetType().Name);
}
/// <summary>
/// 执行指定阶段的所有处理器。
/// </summary>
/// <param name="event">场景过渡事件。</param>
/// <param name="phases">执行阶段。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>异步任务。</returns>
public async Task ExecuteAsync(
SceneTransitionEvent @event,
SceneTransitionPhases phases,
CancellationToken cancellationToken = default
)
{
@event.Set("Phases", phases.ToString());
Log.Debug(
"Execute pipeline: Phases={0}, From={1}, To={2}, Type={3}, HandlerCount={4}",
phases,
@event.FromSceneKey,
@event.ToSceneKey ?? "None",
@event.TransitionType,
_handlers.Count
);
var sortedHandlers = FilterAndSortHandlers(@event, phases);
if (sortedHandlers.Count == 0)
{
Log.Debug("No handlers to execute for phases: {0}", phases);
return;
}
Log.Debug(
"Executing {0} handlers for phases {1}",
sortedHandlers.Count,
phases
);
foreach (var handler in sortedHandlers)
{
var options = _options[handler];
await ExecuteSingleHandlerAsync(handler, options, @event, cancellationToken);
}
Log.Debug("Pipeline execution completed for phases: {0}", phases);
}
private List<ISceneTransitionHandler> FilterAndSortHandlers(
SceneTransitionEvent @event,
SceneTransitionPhases phases)
{
return _handlers
.Where(h => h.Phases.HasFlag(phases) && h.ShouldHandle(@event, phases))
.OrderBy(h => h.Priority)
.ToList();
}
private static async Task ExecuteSingleHandlerAsync(
ISceneTransitionHandler handler,
SceneTransitionHandlerOptions options,
SceneTransitionEvent @event,
CancellationToken cancellationToken)
{
Log.Debug(
"Executing handler: {0}, Priority={1}",
handler.GetType().Name,
handler.Priority
);
try
{
using var timeoutCts = options.TimeoutMs > 0
? new CancellationTokenSource(options.TimeoutMs)
: null;
using var linkedCts = timeoutCts != null && cancellationToken.CanBeCanceled
? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token)
: null;
await handler.HandleAsync(
@event,
linkedCts?.Token ?? cancellationToken
).ConfigureAwait(false);
Log.Debug("Handler completed: {0}", handler.GetType().Name);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
Log.Error(
"Handler timeout: {0}, TimeoutMs={1}",
handler.GetType().Name,
options.TimeoutMs
);
if (options.ContinueOnError) return;
Log.Error("Stopping pipeline due to timeout and ContinueOnError=false");
throw;
}
catch (OperationCanceledException)
{
Log.Debug("Handler cancelled: {0}", handler.GetType().Name);
throw;
}
catch (Exception ex)
{
Log.Error("Handler failed: {0}, Error: {1}", handler.GetType().Name, ex.Message);
if (options.ContinueOnError) return;
Log.Error("Stopping pipeline due to error and ContinueOnError=false");
throw;
}
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging;
using GFramework.Game.Abstractions.enums;
using GFramework.Game.Abstractions.scene;
namespace GFramework.Game.scene.handler;
/// <summary>
/// 加载进度处理器,用于在场景加载前显示加载界面和进度反馈。
/// </summary>
public sealed class LoadingProgressHandler : SceneTransitionHandlerBase
{
private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger(nameof(LoadingProgressHandler));
/// <summary>
/// 获取处理器优先级,数值越小优先级越高(优先执行)。
/// </summary>
public override int Priority => -100;
/// <summary>
/// 获取处理器处理的场景切换阶段,只处理 BeforeChange 阶段。
/// </summary>
public override SceneTransitionPhases Phases => SceneTransitionPhases.BeforeChange;
/// <summary>
/// 处理场景切换事件的异步方法。
/// </summary>
/// <param name="event">场景切换事件对象,包含切换的相关信息。</param>
/// <param name="cancellationToken">取消令牌,用于控制异步操作的取消。</param>
/// <returns>表示异步操作的任务。</returns>
public override async Task HandleAsync(SceneTransitionEvent @event, CancellationToken cancellationToken)
{
// 只处理 Push 和 Replace 操作(需要加载场景)
if (@event.TransitionType != SceneTransitionType.Push &&
@event.TransitionType != SceneTransitionType.Replace)
return;
// 显示加载界面
// TODO: 调用 UI 系统显示加载界面
Log.Info("Loading scene: {0}", @event.ToSceneKey);
// 监听加载进度(如果需要)
// 这里可以通过事件上下文传递进度信息
// 例如:@event.Set("LoadingProgress", progressValue);
await Task.CompletedTask;
}
}

View File

@ -0,0 +1,56 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using GFramework.Core.Abstractions.logging;
using GFramework.Core.logging;
using GFramework.Game.Abstractions.enums;
using GFramework.Game.Abstractions.scene;
namespace GFramework.Game.scene.handler;
/// <summary>
/// 日志场景切换处理器,用于记录场景切换的详细信息。
/// </summary>
public sealed class LoggingTransitionHandler : SceneTransitionHandlerBase
{
private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger(nameof(LoggingTransitionHandler));
/// <summary>
/// 获取处理器优先级,数值越大优先级越低(最后执行)。
/// </summary>
public override int Priority => 999;
/// <summary>
/// 获取处理器处理的场景切换阶段,处理所有阶段。
/// </summary>
public override SceneTransitionPhases Phases => SceneTransitionPhases.All;
/// <summary>
/// 处理场景切换事件的异步方法。
/// </summary>
/// <param name="event">场景切换事件对象,包含切换的相关信息。</param>
/// <param name="cancellationToken">取消令牌,用于控制异步操作的取消。</param>
/// <returns>表示异步操作的任务。</returns>
public override Task HandleAsync(SceneTransitionEvent @event, CancellationToken cancellationToken)
{
Log.Info(
"Scene Transition: Phases={0}, Type={1}, From={2}, To={3}",
@event.Get<string>("Phases", "Unknown"),
@event.TransitionType,
@event.FromSceneKey ?? "None",
@event.ToSceneKey ?? "None"
);
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,55 @@
// Copyright (c) 2026 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using GFramework.Game.Abstractions.enums;
using GFramework.Game.Abstractions.scene;
namespace GFramework.Game.scene.handler;
/// <summary>
/// 场景过渡处理器抽象基类,提供了 ISceneTransitionHandler 接口的默认实现。
/// 派生类只需重写必要的方法即可快速实现自定义处理器。
/// </summary>
public abstract class SceneTransitionHandlerBase : ISceneTransitionHandler
{
/// <summary>
/// 获取处理器适用的场景过渡阶段。
/// 默认为所有阶段BeforeChange 和 AfterChange
/// </summary>
public virtual SceneTransitionPhases Phases => SceneTransitionPhases.All;
/// <summary>
/// 获取处理器的执行优先级。
/// 派生类必须实现此属性以指定优先级。
/// </summary>
public abstract int Priority { get; }
/// <summary>
/// 判断处理器是否应该处理当前场景过渡事件。
/// 默认实现总是返回 true派生类可以重写此方法以添加自定义过滤逻辑。
/// </summary>
/// <param name="event">场景过渡事件。</param>
/// <param name="phases">当前执行的阶段。</param>
/// <returns>默认返回 true表示总是处理。</returns>
public virtual bool ShouldHandle(SceneTransitionEvent @event, SceneTransitionPhases phases)
=> true;
/// <summary>
/// 异步处理场景过渡事件。
/// 派生类必须实现此方法以定义具体的处理逻辑。
/// </summary>
/// <param name="event">场景过渡事件,包含切换的上下文信息。</param>
/// <param name="cancellationToken">取消令牌,用于支持操作取消。</param>
/// <returns>表示处理操作完成的异步任务。</returns>
public abstract Task HandleAsync(SceneTransitionEvent @event, CancellationToken cancellationToken);
}