diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index df64bd6..a9604b2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,8 +13,9 @@ permissions:
security-events: write
jobs:
- test:
- name: Build and Test
+ # 代码质量检查 job(并行执行,不阻塞构建)
+ code-quality:
+ name: Code Quality & Security
runs-on: ubuntu-latest
steps:
@@ -23,9 +24,11 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
+
# 校验C#命名空间与源码目录是否符合命名规范
- name: Validate C# naming
run: bash scripts/validate-csharp-naming.sh
+
# 缓存MegaLinter
- name: Cache MegaLinter
uses: actions/cache@v5
@@ -34,8 +37,7 @@ jobs:
key: ${{ runner.os }}-megalinter-v9
restore-keys: |
${{ runner.os }}-megalinter-
-
-
+
# MegaLinter扫描步骤
# 执行代码质量检查和安全扫描,生成SARIF格式报告
- name: MegaLinter
@@ -44,11 +46,13 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FAIL_ON_ERROR: ${{ github.ref == 'refs/heads/main' }}
+
# 上传SARIF格式的安全和代码质量问题报告到GitHub安全中心
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: megalinter-reports/sarif
+
# 缓存TruffleHog
- name: Cache TruffleHog
uses: actions/cache@v5
@@ -68,6 +72,18 @@ jobs:
base: ${{ github.event.before }}
# 当前提交哈希,作为扫描的目标版本
head: ${{ github.sha }}
+
+ # 构建和测试 job(并行执行)
+ build-and-test:
+ name: Build and Test
+ runs-on: ubuntu-latest
+
+ steps:
+ # 检出源代码
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
# 安装和配置.NET SDK版本
- name: Setup .NET 8
@@ -113,29 +129,35 @@ jobs:
run: dotnet build -c Release --no-restore
# 运行单元测试,输出TRX格式结果到TestResults目录
- - name: Test - Core
+ # 在同一个 step 中并发执行所有测试以加快速度
+ - name: Test All Projects
run: |
dotnet test GFramework.Core.Tests \
-c Release \
--no-build \
--logger "trx;LogFileName=core-$RANDOM.trx" \
- --results-directory TestResults
-
- - name: Test - SourceGenerators
- run: |
+ --results-directory TestResults &
+
+ dotnet test GFramework.Game.Tests \
+ -c Release \
+ --no-build \
+ --logger "trx;LogFileName=game-$RANDOM.trx" \
+ --results-directory TestResults &
+
dotnet test GFramework.SourceGenerators.Tests \
-c Release \
--no-build \
--logger "trx;LogFileName=sg-$RANDOM.trx" \
- --results-directory TestResults
-
- - name: Test - GFramework.Ecs.Arch.Tests
- run: |
+ --results-directory TestResults &
+
dotnet test GFramework.Ecs.Arch.Tests \
-c Release \
--no-build \
--logger "trx;LogFileName=ecs-arch-$RANDOM.trx" \
- --results-directory TestResults
+ --results-directory TestResults &
+
+ # 等待所有后台测试完成
+ wait
- name: Generate CTRF report
run: |
diff --git a/GFramework.Game.Abstractions/Routing/IRoute.cs b/GFramework.Game.Abstractions/Routing/IRoute.cs
new file mode 100644
index 0000000..c626273
--- /dev/null
+++ b/GFramework.Game.Abstractions/Routing/IRoute.cs
@@ -0,0 +1,25 @@
+// 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.Routing;
+
+///
+/// 路由项接口,表示可路由的对象
+///
+public interface IRoute
+{
+ ///
+ /// 路由键值,用于唯一标识路由项
+ ///
+ string Key { get; }
+}
\ No newline at end of file
diff --git a/GFramework.Game.Abstractions/Routing/IRouteContext.cs b/GFramework.Game.Abstractions/Routing/IRouteContext.cs
new file mode 100644
index 0000000..d58aca4
--- /dev/null
+++ b/GFramework.Game.Abstractions/Routing/IRouteContext.cs
@@ -0,0 +1,26 @@
+// 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.Routing;
+
+///
+/// 路由上下文接口,表示路由进入时的参数
+///
+///
+/// 这是一个标记接口,用于类型约束。
+/// 具体的路由上下文类型应该实现此接口。
+///
+public interface IRouteContext
+{
+ // 标记接口,用于类型约束
+}
\ No newline at end of file
diff --git a/GFramework.Game.Abstractions/Routing/IRouteGuard.cs b/GFramework.Game.Abstractions/Routing/IRouteGuard.cs
new file mode 100644
index 0000000..7e8ce2f
--- /dev/null
+++ b/GFramework.Game.Abstractions/Routing/IRouteGuard.cs
@@ -0,0 +1,54 @@
+// 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.Routing;
+
+///
+/// 路由守卫接口,用于控制路由的进入和离开
+///
+/// 路由项类型
+public interface IRouteGuard where TRoute : IRoute
+{
+ ///
+ /// 守卫优先级,数值越小优先级越高
+ ///
+ ///
+ /// 守卫按优先级从小到大依次执行。
+ /// 建议使用 0-100 的范围,默认为 50。
+ ///
+ int Priority { get; }
+
+ ///
+ /// 是否可以中断后续守卫的执行
+ ///
+ ///
+ /// 如果为 true,当此守卫返回 true 或抛出异常时,将中断后续守卫的执行。
+ /// 如果为 false,将继续执行后续守卫。
+ ///
+ bool CanInterrupt { get; }
+
+ ///
+ /// 检查是否可以进入指定路由
+ ///
+ /// 路由键值
+ /// 路由上下文
+ /// 如果允许进入返回 true,否则返回 false
+ ValueTask CanEnterAsync(string routeKey, IRouteContext? context);
+
+ ///
+ /// 检查是否可以离开指定路由
+ ///
+ /// 路由键值
+ /// 如果允许离开返回 true,否则返回 false
+ ValueTask CanLeaveAsync(string routeKey);
+}
\ No newline at end of file
diff --git a/GFramework.Game.Abstractions/Scene/ISceneBehavior.cs b/GFramework.Game.Abstractions/Scene/ISceneBehavior.cs
index 4a817e9..2bb07ee 100644
--- a/GFramework.Game.Abstractions/Scene/ISceneBehavior.cs
+++ b/GFramework.Game.Abstractions/Scene/ISceneBehavior.cs
@@ -2,22 +2,24 @@
// 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.Routing;
+
namespace GFramework.Game.Abstractions.Scene;
///
/// 场景行为接口,定义了场景生命周期管理的标准方法。
/// 实现此接口的类需要处理场景的加载、激活、暂停、恢复和卸载等核心操作。
///
-public interface ISceneBehavior
+public interface ISceneBehavior : IRoute
{
///
/// 获取场景的唯一标识符。
diff --git a/GFramework.Game.Abstractions/Scene/ISceneEnterParam.cs b/GFramework.Game.Abstractions/Scene/ISceneEnterParam.cs
index d5b807c..4da6827 100644
--- a/GFramework.Game.Abstractions/Scene/ISceneEnterParam.cs
+++ b/GFramework.Game.Abstractions/Scene/ISceneEnterParam.cs
@@ -2,19 +2,21 @@
// 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.Routing;
+
namespace GFramework.Game.Abstractions.Scene;
///
-/// 场景进入参数接口
-/// 该接口用于定义场景跳转时传递的参数数据结构
+/// 场景进入参数接口
+/// 该接口用于定义场景跳转时传递的参数数据结构
///
-public interface ISceneEnterParam;
\ No newline at end of file
+public interface ISceneEnterParam : IRouteContext;
\ No newline at end of file
diff --git a/GFramework.Game.Abstractions/Scene/ISceneRouteGuard.cs b/GFramework.Game.Abstractions/Scene/ISceneRouteGuard.cs
index 5aca883..eb3e70e 100644
--- a/GFramework.Game.Abstractions/Scene/ISceneRouteGuard.cs
+++ b/GFramework.Game.Abstractions/Scene/ISceneRouteGuard.cs
@@ -11,40 +11,28 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+using GFramework.Game.Abstractions.Routing;
+
namespace GFramework.Game.Abstractions.Scene;
///
/// 场景路由守卫接口,用于在场景切换前进行权限检查和条件验证。
/// 实现此接口可以拦截场景的进入和离开操作。
///
-public interface ISceneRouteGuard
+public interface ISceneRouteGuard : IRouteGuard
{
- ///
- /// 获取守卫的执行优先级。
- /// 数值越小优先级越高,越先执行。
- /// 建议范围:-1000 到 1000。
- ///
- int Priority { get; }
-
- ///
- /// 获取守卫是否可以中断后续守卫的执行。
- /// true 表示当前守卫通过后,可以跳过后续守卫直接允许操作。
- /// false 表示即使当前守卫通过,仍需执行所有后续守卫。
- ///
- bool CanInterrupt { get; }
-
///
/// 异步检查是否允许进入指定场景。
///
/// 目标场景的唯一标识符。
/// 场景进入参数,可能包含初始化数据或上下文信息。
/// 如果允许进入则返回 true,否则返回 false。
- Task CanEnterAsync(string sceneKey, ISceneEnterParam? param);
+ ValueTask CanEnterAsync(string sceneKey, ISceneEnterParam? param);
///
/// 异步检查是否允许离开指定场景。
///
/// 当前场景的唯一标识符。
/// 如果允许离开则返回 true,否则返回 false。
- Task CanLeaveAsync(string sceneKey);
+ ValueTask CanLeaveAsync(string sceneKey);
}
\ No newline at end of file
diff --git a/GFramework.Game.Abstractions/UI/IUiPageBehavior.cs b/GFramework.Game.Abstractions/UI/IUiPageBehavior.cs
index efd0e53..8e84067 100644
--- a/GFramework.Game.Abstractions/UI/IUiPageBehavior.cs
+++ b/GFramework.Game.Abstractions/UI/IUiPageBehavior.cs
@@ -1,11 +1,12 @@
using GFramework.Game.Abstractions.Enums;
+using GFramework.Game.Abstractions.Routing;
namespace GFramework.Game.Abstractions.UI;
///
/// UI页面行为接口,定义了UI页面的生命周期方法和状态管理
///
-public interface IUiPageBehavior
+public interface IUiPageBehavior : IRoute
{
///
/// 获取或设置当前UI句柄。
diff --git a/GFramework.Game.Abstractions/UI/IUiPageEnterParam.cs b/GFramework.Game.Abstractions/UI/IUiPageEnterParam.cs
index 29bea63..4b40bc8 100644
--- a/GFramework.Game.Abstractions/UI/IUiPageEnterParam.cs
+++ b/GFramework.Game.Abstractions/UI/IUiPageEnterParam.cs
@@ -1,7 +1,9 @@
-namespace GFramework.Game.Abstractions.UI;
+using GFramework.Game.Abstractions.Routing;
+
+namespace GFramework.Game.Abstractions.UI;
///
-/// UI页面进入参数接口
-/// 该接口用于定义UI页面跳转时传递的参数数据结构
+/// UI页面进入参数接口
+/// 该接口用于定义UI页面跳转时传递的参数数据结构
///
-public interface IUiPageEnterParam;
\ No newline at end of file
+public interface IUiPageEnterParam : IRouteContext;
\ No newline at end of file
diff --git a/GFramework.Game.Abstractions/UI/IUiRouteGuard.cs b/GFramework.Game.Abstractions/UI/IUiRouteGuard.cs
index 8bfa0df..fff03c9 100644
--- a/GFramework.Game.Abstractions/UI/IUiRouteGuard.cs
+++ b/GFramework.Game.Abstractions/UI/IUiRouteGuard.cs
@@ -1,34 +1,25 @@
+using GFramework.Game.Abstractions.Routing;
+
namespace GFramework.Game.Abstractions.UI;
///
-/// UI路由守卫接口
-/// 用于拦截和处理UI路由切换,实现业务逻辑解耦
+/// UI路由守卫接口
+/// 用于拦截和处理UI路由切换,实现业务逻辑解耦
///
-public interface IUiRouteGuard
+public interface IUiRouteGuard : IRouteGuard
{
///
- /// 守卫优先级,数值越小越先执行
- ///
- int Priority { get; }
-
- ///
- /// 是否可中断后续守卫
- /// 如果返回 true,当该守卫返回 false 时,将停止执行后续守卫
- ///
- bool CanInterrupt { get; }
-
- ///
- /// 进入UI前的检查
+ /// 进入UI前的检查
///
/// 目标UI标识符
/// 进入参数
/// true表示允许进入,false表示拦截
- Task CanEnterAsync(string uiKey, IUiPageEnterParam? param);
+ ValueTask CanEnterAsync(string uiKey, IUiPageEnterParam? param);
///
- /// 离开UI前的检查
+ /// 离开UI前的检查
///
/// 当前UI标识符
/// true表示允许离开,false表示拦截
- Task CanLeaveAsync(string uiKey);
+ ValueTask CanLeaveAsync(string uiKey);
}
\ No newline at end of file
diff --git a/GFramework.Game.Abstractions/UI/IUiRouter.cs b/GFramework.Game.Abstractions/UI/IUiRouter.cs
index c5ee283..b6b1887 100644
--- a/GFramework.Game.Abstractions/UI/IUiRouter.cs
+++ b/GFramework.Game.Abstractions/UI/IUiRouter.cs
@@ -113,28 +113,6 @@ public interface IUiRouter : ISystem
///
bool Contains(string uiKey);
- #region 路由守卫
-
- ///
- /// 注册路由守卫
- ///
- /// 守卫实例
- void AddGuard(IUiRouteGuard guard);
-
- ///
- /// 注册路由守卫(泛型方法)
- ///
- /// 守卫类型,必须实现 IUiRouteGuard 且有无参构造函数
- void AddGuard() where T : IUiRouteGuard, new();
-
- ///
- /// 移除路由守卫
- ///
- /// 守卫实例
- void RemoveGuard(IUiRouteGuard guard);
-
- #endregion
-
#region Layer UI
///
diff --git a/GFramework.Game.Tests/GFramework.Game.Tests.csproj b/GFramework.Game.Tests/GFramework.Game.Tests.csproj
new file mode 100644
index 0000000..56fa021
--- /dev/null
+++ b/GFramework.Game.Tests/GFramework.Game.Tests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net8.0;net10.0
+ disable
+ enable
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GFramework.Game.Tests/GlobalUsings.cs b/GFramework.Game.Tests/GlobalUsings.cs
new file mode 100644
index 0000000..6b9d4d6
--- /dev/null
+++ b/GFramework.Game.Tests/GlobalUsings.cs
@@ -0,0 +1,19 @@
+// 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.
+
+global using NUnit.Framework;
+global using Moq;
+global using System;
+global using System.Collections.Generic;
+global using System.Linq;
+global using System.Threading.Tasks;
\ No newline at end of file
diff --git a/GFramework.Game.Tests/Routing/RouterBaseTests.cs b/GFramework.Game.Tests/Routing/RouterBaseTests.cs
new file mode 100644
index 0000000..54992a1
--- /dev/null
+++ b/GFramework.Game.Tests/Routing/RouterBaseTests.cs
@@ -0,0 +1,582 @@
+// 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.Routing;
+using GFramework.Game.Routing;
+
+namespace GFramework.Game.Tests.Routing;
+
+///
+/// RouterBase 单元测试
+///
+[TestFixture]
+public class RouterBaseTests
+{
+ ///
+ /// 测试用路由项
+ ///
+ private class TestRoute : IRoute
+ {
+ public string Key { get; set; } = string.Empty;
+ }
+
+ ///
+ /// 测试用路由上下文
+ ///
+ private class TestContext : IRouteContext
+ {
+ public string? Data { get; set; }
+ }
+
+ ///
+ /// 测试用路由守卫
+ ///
+ private class TestGuard : IRouteGuard
+ {
+ public Func>? EnterFunc { get; set; }
+ public Func>? LeaveFunc { get; set; }
+ public int Priority { get; set; }
+ public bool CanInterrupt { get; set; }
+
+ public ValueTask CanEnterAsync(string routeKey, IRouteContext? context)
+ {
+ return EnterFunc?.Invoke(routeKey, context) ?? ValueTask.FromResult(true);
+ }
+
+ public ValueTask CanLeaveAsync(string routeKey)
+ {
+ return LeaveFunc?.Invoke(routeKey) ?? ValueTask.FromResult(true);
+ }
+ }
+
+ ///
+ /// 测试用路由器实现
+ ///
+ private class TestRouter : RouterBase
+ {
+ public bool HandlersRegistered { get; private set; }
+
+ // 暴露 Stack 用于测试
+ public new Stack Stack => base.Stack;
+
+ protected override void OnInit()
+ {
+ // 测试用路由器不需要初始化逻辑
+ }
+
+ protected override void RegisterHandlers()
+ {
+ HandlersRegistered = true;
+ }
+
+ // 暴露 protected 方法用于测试
+ public new Task ExecuteEnterGuardsAsync(string routeKey, TestContext? context)
+ {
+ return base.ExecuteEnterGuardsAsync(routeKey, context);
+ }
+
+ public new Task ExecuteLeaveGuardsAsync(string routeKey)
+ {
+ return base.ExecuteLeaveGuardsAsync(routeKey);
+ }
+ }
+
+ [Test]
+ public void AddGuard_ShouldAddGuardToList()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var guard = new TestGuard { Priority = 10 };
+
+ // Act
+ router.AddGuard(guard);
+
+ // Assert - 通过尝试添加相同守卫来验证
+ Assert.DoesNotThrow(() => router.AddGuard(guard));
+ }
+
+ [Test]
+ public void AddGuard_ShouldSortByPriority()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var guard1 = new TestGuard { Priority = 20 };
+ var guard2 = new TestGuard { Priority = 10 };
+ var guard3 = new TestGuard { Priority = 30 };
+
+ // Act
+ router.AddGuard(guard1);
+ router.AddGuard(guard2);
+ router.AddGuard(guard3);
+
+ // Assert - 通过执行守卫来验证顺序
+ var executionOrder = new List();
+ guard1.EnterFunc = (_, _) =>
+ {
+ executionOrder.Add(1);
+ return ValueTask.FromResult(true);
+ };
+ guard2.EnterFunc = (_, _) =>
+ {
+ executionOrder.Add(2);
+ return ValueTask.FromResult(true);
+ };
+ guard3.EnterFunc = (_, _) =>
+ {
+ executionOrder.Add(3);
+ return ValueTask.FromResult(true);
+ };
+
+ router.ExecuteEnterGuardsAsync("test", null).Wait();
+
+ Assert.That(executionOrder, Is.EqualTo(new[] { 2, 1, 3 }));
+ }
+
+ [Test]
+ public void AddGuard_WithGeneric_ShouldCreateAndAddGuard()
+ {
+ // Arrange
+ var router = new TestRouter();
+
+ // Act & Assert
+ Assert.DoesNotThrow(() => router.AddGuard());
+ }
+
+ [Test]
+ public void AddGuard_WithNull_ShouldThrowArgumentNullException()
+ {
+ // Arrange
+ var router = new TestRouter();
+
+ // Act & Assert
+ Assert.Throws(() => router.AddGuard(null!));
+ }
+
+ [Test]
+ public void RemoveGuard_ShouldRemoveGuardFromList()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var guard = new TestGuard { Priority = 10 };
+ router.AddGuard(guard);
+
+ // Act
+ router.RemoveGuard(guard);
+
+ // Assert - 守卫应该被移除,不会再执行
+ var executed = false;
+ guard.EnterFunc = (_, _) =>
+ {
+ executed = true;
+ return ValueTask.FromResult(true);
+ };
+
+ router.ExecuteEnterGuardsAsync("test", null).Wait();
+
+ Assert.That(executed, Is.False);
+ }
+
+ [Test]
+ public void RemoveGuard_WithNull_ShouldThrowArgumentNullException()
+ {
+ // Arrange
+ var router = new TestRouter();
+
+ // Act & Assert
+ Assert.Throws(() => router.RemoveGuard(null!));
+ }
+
+ [Test]
+ public async Task ExecuteEnterGuardsAsync_WithNoGuards_ShouldReturnTrue()
+ {
+ // Arrange
+ var router = new TestRouter();
+
+ // Act
+ var result = await router.ExecuteEnterGuardsAsync("test", null);
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public async Task ExecuteEnterGuardsAsync_WithAllowingGuard_ShouldReturnTrue()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var guard = new TestGuard
+ {
+ Priority = 10,
+ EnterFunc = (_, _) => ValueTask.FromResult(true)
+ };
+ router.AddGuard(guard);
+
+ // Act
+ var result = await router.ExecuteEnterGuardsAsync("test", null);
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public async Task ExecuteEnterGuardsAsync_WithBlockingGuard_ShouldReturnFalse()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var guard = new TestGuard
+ {
+ Priority = 10,
+ EnterFunc = (_, _) => ValueTask.FromResult(false)
+ };
+ router.AddGuard(guard);
+
+ // Act
+ var result = await router.ExecuteEnterGuardsAsync("test", null);
+
+ // Assert
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public async Task ExecuteEnterGuardsAsync_WithInterruptingGuard_ShouldStopExecution()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var guard1 = new TestGuard
+ {
+ Priority = 10,
+ CanInterrupt = true,
+ EnterFunc = (_, _) => ValueTask.FromResult(true)
+ };
+ var guard2Executed = false;
+ var guard2 = new TestGuard
+ {
+ Priority = 20,
+ EnterFunc = (_, _) =>
+ {
+ guard2Executed = true;
+ return ValueTask.FromResult(true);
+ }
+ };
+ router.AddGuard(guard1);
+ router.AddGuard(guard2);
+
+ // Act
+ var result = await router.ExecuteEnterGuardsAsync("test", null);
+
+ // Assert
+ Assert.That(result, Is.True);
+ Assert.That(guard2Executed, Is.False);
+ }
+
+ [Test]
+ public async Task ExecuteEnterGuardsAsync_WithThrowingGuard_ShouldContinueIfNotInterrupting()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var guard1 = new TestGuard
+ {
+ Priority = 10,
+ CanInterrupt = false,
+ EnterFunc = (_, _) => throw new InvalidOperationException("Test exception")
+ };
+ var guard2Executed = false;
+ var guard2 = new TestGuard
+ {
+ Priority = 20,
+ EnterFunc = (_, _) =>
+ {
+ guard2Executed = true;
+ return ValueTask.FromResult(true);
+ }
+ };
+ router.AddGuard(guard1);
+ router.AddGuard(guard2);
+
+ // Act
+ var result = await router.ExecuteEnterGuardsAsync("test", null);
+
+ // Assert
+ Assert.That(result, Is.True);
+ Assert.That(guard2Executed, Is.True);
+ }
+
+ [Test]
+ public async Task ExecuteEnterGuardsAsync_WithThrowingInterruptingGuard_ShouldReturnFalse()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var guard = new TestGuard
+ {
+ Priority = 10,
+ CanInterrupt = true,
+ EnterFunc = (_, _) => throw new InvalidOperationException("Test exception")
+ };
+ router.AddGuard(guard);
+
+ // Act
+ var result = await router.ExecuteEnterGuardsAsync("test", null);
+
+ // Assert
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public async Task ExecuteLeaveGuardsAsync_WithNoGuards_ShouldReturnTrue()
+ {
+ // Arrange
+ var router = new TestRouter();
+
+ // Act
+ var result = await router.ExecuteLeaveGuardsAsync("test");
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public async Task ExecuteLeaveGuardsAsync_WithAllowingGuard_ShouldReturnTrue()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var guard = new TestGuard
+ {
+ Priority = 10,
+ LeaveFunc = _ => ValueTask.FromResult(true)
+ };
+ router.AddGuard(guard);
+
+ // Act
+ var result = await router.ExecuteLeaveGuardsAsync("test");
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public async Task ExecuteLeaveGuardsAsync_WithBlockingGuard_ShouldReturnFalse()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var guard = new TestGuard
+ {
+ Priority = 10,
+ LeaveFunc = _ => ValueTask.FromResult(false)
+ };
+ router.AddGuard(guard);
+
+ // Act
+ var result = await router.ExecuteLeaveGuardsAsync("test");
+
+ // Assert
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public void Contains_WithEmptyStack_ShouldReturnFalse()
+ {
+ // Arrange
+ var router = new TestRouter();
+
+ // Act
+ var result = router.Contains("test");
+
+ // Assert
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public void Contains_WithMatchingRoute_ShouldReturnTrue()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var route = new TestRoute { Key = "test" };
+ router.Stack.Push(route);
+
+ // Act
+ var result = router.Contains("test");
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void Contains_WithNonMatchingRoute_ShouldReturnFalse()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var route = new TestRoute { Key = "test1" };
+ router.Stack.Push(route);
+
+ // Act
+ var result = router.Contains("test2");
+
+ // Assert
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public void PeekKey_WithEmptyStack_ShouldReturnEmptyString()
+ {
+ // Arrange
+ var router = new TestRouter();
+
+ // Act
+ var result = router.PeekKey();
+
+ // Assert
+ Assert.That(result, Is.EqualTo(string.Empty));
+ }
+
+ [Test]
+ public void PeekKey_WithRoute_ShouldReturnRouteKey()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var route = new TestRoute { Key = "test" };
+ router.Stack.Push(route);
+
+ // Act
+ var result = router.PeekKey();
+
+ // Assert
+ Assert.That(result, Is.EqualTo("test"));
+ }
+
+ [Test]
+ public void IsTop_WithEmptyStack_ShouldReturnFalse()
+ {
+ // Arrange
+ var router = new TestRouter();
+
+ // Act
+ var result = router.IsTop("test");
+
+ // Assert
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public void IsTop_WithMatchingRoute_ShouldReturnTrue()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var route = new TestRoute { Key = "test" };
+ router.Stack.Push(route);
+
+ // Act
+ var result = router.IsTop("test");
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void IsTop_WithNonMatchingRoute_ShouldReturnFalse()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var route = new TestRoute { Key = "test1" };
+ router.Stack.Push(route);
+
+ // Act
+ var result = router.IsTop("test2");
+
+ // Assert
+ Assert.That(result, Is.False);
+ }
+
+ [Test]
+ public void Current_WithEmptyStack_ShouldReturnNull()
+ {
+ // Arrange
+ var router = new TestRouter();
+
+ // Act
+ var result = router.Current;
+
+ // Assert
+ Assert.That(result, Is.Null);
+ }
+
+ [Test]
+ public void Current_WithRoute_ShouldReturnTopRoute()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var route = new TestRoute { Key = "test" };
+ router.Stack.Push(route);
+
+ // Act
+ var result = router.Current;
+
+ // Assert
+ Assert.That(result, Is.EqualTo(route));
+ }
+
+ [Test]
+ public void CurrentKey_WithEmptyStack_ShouldReturnNull()
+ {
+ // Arrange
+ var router = new TestRouter();
+
+ // Act
+ var result = router.CurrentKey;
+
+ // Assert
+ Assert.That(result, Is.Null);
+ }
+
+ [Test]
+ public void CurrentKey_WithRoute_ShouldReturnRouteKey()
+ {
+ // Arrange
+ var router = new TestRouter();
+ var route = new TestRoute { Key = "test" };
+ router.Stack.Push(route);
+
+ // Act
+ var result = router.CurrentKey;
+
+ // Assert
+ Assert.That(result, Is.EqualTo("test"));
+ }
+
+ [Test]
+ public void Count_WithEmptyStack_ShouldReturnZero()
+ {
+ // Arrange
+ var router = new TestRouter();
+
+ // Act
+ var result = router.Count;
+
+ // Assert
+ Assert.That(result, Is.EqualTo(0));
+ }
+
+ [Test]
+ public void Count_WithRoutes_ShouldReturnCorrectCount()
+ {
+ // Arrange
+ var router = new TestRouter();
+ router.Stack.Push(new TestRoute { Key = "test1" });
+ router.Stack.Push(new TestRoute { Key = "test2" });
+
+ // Act
+ var result = router.Count;
+
+ // Assert
+ Assert.That(result, Is.EqualTo(2));
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Game/Routing/RouterBase.cs b/GFramework.Game/Routing/RouterBase.cs
new file mode 100644
index 0000000..1f73150
--- /dev/null
+++ b/GFramework.Game/Routing/RouterBase.cs
@@ -0,0 +1,244 @@
+// 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.Core.Systems;
+using GFramework.Game.Abstractions.Routing;
+
+namespace GFramework.Game.Routing;
+
+///
+/// 路由器基类,提供通用的路由管理功能
+///
+/// 路由项类型,必须实现 IRoute 接口
+/// 路由上下文类型,必须实现 IRouteContext 接口
+///
+/// 此基类提供了以下通用功能:
+/// - 路由守卫管理 (AddGuard/RemoveGuard)
+/// - 守卫执行逻辑 (ExecuteEnterGuardsAsync/ExecuteLeaveGuardsAsync)
+/// - 路由栈管理 (Stack/Current/CurrentKey)
+/// - 栈操作方法 (Contains/PeekKey/IsTop)
+///
+public abstract class RouterBase : AbstractSystem
+ where TRoute : IRoute
+ where TContext : IRouteContext
+{
+ private static readonly ILogger Log =
+ LoggerFactoryResolver.Provider.CreateLogger(nameof(RouterBase));
+
+ ///
+ /// 路由守卫列表,按优先级排序
+ ///
+ private readonly List> _guards = new();
+
+ ///
+ /// 路由栈,用于管理路由的显示顺序和导航历史
+ ///
+ protected readonly Stack Stack = new();
+
+ ///
+ /// 获取当前路由 (栈顶元素)
+ ///
+ public TRoute? Current => Stack.Count > 0 ? Stack.Peek() : default;
+
+ ///
+ /// 获取当前路由的键值
+ ///
+ public string? CurrentKey => Current?.Key;
+
+ ///
+ /// 获取栈深度
+ ///
+ public int Count => Stack.Count;
+
+ #region Abstract Methods
+
+ ///
+ /// 注册过渡处理器 (由子类实现)
+ ///
+ ///
+ /// 子类应该在此方法中注册所有需要的过渡处理器。
+ /// 此方法在 OnInit 中被调用。
+ ///
+ protected abstract void RegisterHandlers();
+
+ #endregion
+
+ #region Guard Management
+
+ ///
+ /// 添加路由守卫
+ ///
+ /// 路由守卫实例
+ /// 当守卫实例为 null 时抛出
+ public void AddGuard(IRouteGuard guard)
+ {
+ ArgumentNullException.ThrowIfNull(guard);
+
+ if (_guards.Contains(guard))
+ {
+ Log.Debug("Guard already registered: {0}", guard.GetType().Name);
+ return;
+ }
+
+ _guards.Add(guard);
+ _guards.Sort((a, b) => a.Priority.CompareTo(b.Priority));
+ Log.Debug("Guard registered: {0}, Priority={1}", guard.GetType().Name, guard.Priority);
+ }
+
+ ///
+ /// 添加路由守卫 (泛型版本)
+ ///
+ /// 守卫类型,必须实现 IRouteGuard 接口且有无参构造函数
+ public void AddGuard() where T : IRouteGuard, new()
+ {
+ AddGuard(new T());
+ }
+
+ ///
+ /// 移除路由守卫
+ ///
+ /// 要移除的路由守卫实例
+ /// 当守卫实例为 null 时抛出
+ public void RemoveGuard(IRouteGuard guard)
+ {
+ ArgumentNullException.ThrowIfNull(guard);
+ if (_guards.Remove(guard))
+ Log.Debug("Guard removed: {0}", guard.GetType().Name);
+ }
+
+ #endregion
+
+ #region Guard Execution
+
+ ///
+ /// 执行进入守卫检查
+ ///
+ /// 路由键值
+ /// 路由上下文
+ /// 如果所有守卫都允许进入返回 true,否则返回 false
+ ///
+ /// 守卫按优先级从小到大依次执行。
+ /// 如果某个守卫返回 false 且 CanInterrupt 为 true,则中断后续守卫的执行。
+ /// 如果某个守卫抛出异常且 CanInterrupt 为 true,则中断后续守卫的执行。
+ ///
+ protected async Task ExecuteEnterGuardsAsync(string routeKey, TContext? context)
+ {
+ foreach (var guard in _guards)
+ {
+ try
+ {
+ Log.Debug("Executing enter guard: {0} for {1}", guard.GetType().Name, routeKey);
+ var canEnter = await guard.CanEnterAsync(routeKey, context);
+
+ 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;
+ }
+
+ ///
+ /// 执行离开守卫检查
+ ///
+ /// 路由键值
+ /// 如果所有守卫都允许离开返回 true,否则返回 false
+ ///
+ /// 守卫按优先级从小到大依次执行。
+ /// 如果某个守卫返回 false 且 CanInterrupt 为 true,则中断后续守卫的执行。
+ /// 如果某个守卫抛出异常且 CanInterrupt 为 true,则中断后续守卫的执行。
+ ///
+ protected async Task ExecuteLeaveGuardsAsync(string routeKey)
+ {
+ foreach (var guard in _guards)
+ {
+ try
+ {
+ Log.Debug("Executing leave guard: {0} for {1}", guard.GetType().Name, routeKey);
+ var canLeave = await guard.CanLeaveAsync(routeKey);
+
+ 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
+
+ #region Stack Operations
+
+ ///
+ /// 检查栈中是否包含指定路由
+ ///
+ /// 路由键值
+ /// 如果栈中包含指定路由返回 true,否则返回 false
+ public bool Contains(string routeKey)
+ {
+ return Stack.Any(r => r.Key == routeKey);
+ }
+
+ ///
+ /// 获取栈顶路由的键值
+ ///
+ /// 栈顶路由的键值,如果栈为空则返回空字符串
+ public string PeekKey()
+ {
+ return Stack.Count == 0 ? string.Empty : Stack.Peek().Key;
+ }
+
+ ///
+ /// 判断栈顶是否为指定路由
+ ///
+ /// 路由键值
+ /// 如果栈顶是指定路由返回 true,否则返回 false
+ public bool IsTop(string routeKey)
+ {
+ return Stack.Count != 0 && Stack.Peek().Key.Equals(routeKey);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/GFramework.Game/Scene/SceneRouterBase.cs b/GFramework.Game/Scene/SceneRouterBase.cs
index 2b3ccec..812644c 100644
--- a/GFramework.Game/Scene/SceneRouterBase.cs
+++ b/GFramework.Game/Scene/SceneRouterBase.cs
@@ -14,9 +14,9 @@
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Extensions;
using GFramework.Core.Logging;
-using GFramework.Core.Systems;
using GFramework.Game.Abstractions.Enums;
using GFramework.Game.Abstractions.Scene;
+using GFramework.Game.Routing;
namespace GFramework.Game.Scene;
@@ -25,15 +25,13 @@ namespace GFramework.Game.Scene;
/// 实现了 接口,用于管理场景的加载、替换和卸载操作。
///
public abstract class SceneRouterBase
- : AbstractSystem, ISceneRouter
+ : RouterBase, ISceneRouter
{
private static readonly ILogger Log =
LoggerFactoryResolver.Provider.CreateLogger(nameof(SceneRouterBase));
- private readonly List _guards = new();
private readonly SceneTransitionPipeline _pipeline = new();
- private readonly Stack _stack = new();
private readonly SemaphoreSlim _transitionLock = new(1, 1);
private ISceneFactory _factory = null!;
@@ -45,17 +43,17 @@ public abstract class SceneRouterBase
///
/// 获取当前场景行为对象。
///
- public ISceneBehavior? Current => _stack.Count > 0 ? _stack.Peek() : null;
+ public new ISceneBehavior? Current => Stack.Count > 0 ? Stack.Peek() : null;
///
/// 获取当前场景的键名。
///
- public string? CurrentKey => Current?.Key;
+ public new string? CurrentKey => Current?.Key;
///
/// 获取场景栈的只读视图,按压入顺序排列(从栈底到栈顶)。
///
- public IEnumerable Stack => _stack.Reverse();
+ IEnumerable ISceneRouter.Stack => base.Stack.Reverse();
///
/// 获取是否正在进行场景转换。
@@ -115,9 +113,9 @@ public abstract class SceneRouterBase
///
/// 场景键名。
/// 如果场景在栈中返回true,否则返回false。
- public bool Contains(string sceneKey)
+ public new bool Contains(string sceneKey)
{
- return _stack.Any(s => s.Key == sceneKey);
+ return Stack.Any(s => s.Key == sceneKey);
}
#endregion
@@ -163,46 +161,10 @@ public abstract class SceneRouterBase
_pipeline.UnregisterAroundHandler(handler);
}
- ///
- /// 添加场景路由守卫。
- ///
- /// 守卫实例。
- 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);
- }
- }
-
- ///
- /// 添加场景路由守卫(泛型版本)。
- ///
- /// 守卫类型。
- public void AddGuard() where T : ISceneRouteGuard, new()
- {
- AddGuard(new T());
- }
-
- ///
- /// 移除场景路由守卫。
- ///
- /// 守卫实例。
- public void RemoveGuard(ISceneRouteGuard guard)
- {
- if (_guards.Remove(guard))
- {
- Log.Debug("Guard removed: {0}", guard.GetType().Name);
- }
- }
-
///
/// 注册场景过渡处理器的抽象方法,由子类实现。
///
- protected abstract void RegisterHandlers();
+ protected override abstract void RegisterHandlers();
///
/// 系统初始化方法,获取场景工厂并注册处理器。
@@ -281,20 +243,20 @@ public abstract class SceneRouterBase
await scene.OnLoadAsync(param);
// 暂停当前场景
- if (_stack.Count > 0)
+ if (Stack.Count > 0)
{
- var current = _stack.Peek();
+ var current = Stack.Peek();
await current.OnPauseAsync();
}
// 压入栈
- _stack.Push(scene);
+ Stack.Push(scene);
// 进入场景
await scene.OnEnterAsync();
Log.Debug("Push Scene: {0}, stackCount={1}",
- sceneKey, _stack.Count);
+ sceneKey, Stack.Count);
}
#endregion
@@ -335,10 +297,10 @@ public abstract class SceneRouterBase
/// 异步任务。
private async ValueTask PopInternalAsync()
{
- if (_stack.Count == 0)
+ if (Stack.Count == 0)
return;
- var top = _stack.Peek();
+ var top = Stack.Peek();
// 守卫检查
if (!await ExecuteLeaveGuardsAsync(top.Key))
@@ -347,7 +309,7 @@ public abstract class SceneRouterBase
return;
}
- _stack.Pop();
+ Stack.Pop();
// 退出场景
await top.OnExitAsync();
@@ -359,13 +321,13 @@ public abstract class SceneRouterBase
Root!.RemoveScene(top);
// 恢复下一个场景
- if (_stack.Count > 0)
+ if (Stack.Count > 0)
{
- var next = _stack.Peek();
+ var next = Stack.Peek();
await next.OnResumeAsync();
}
- Log.Debug("Pop Scene, stackCount={0}", _stack.Count);
+ Log.Debug("Pop Scene, stackCount={0}", Stack.Count);
}
#endregion
@@ -406,7 +368,7 @@ public abstract class SceneRouterBase
/// 异步任务。
private async ValueTask ClearInternalAsync()
{
- while (_stack.Count > 0)
+ while (Stack.Count > 0)
{
await PopInternalAsync();
}
@@ -460,82 +422,5 @@ public abstract class SceneRouterBase
Log.Debug("AfterChange phases completed: {0}", @event.TransitionType);
}
- ///
- /// 执行进入场景的守卫检查。
- /// 按优先级顺序执行所有守卫的CanEnterAsync方法。
- ///
- /// 场景键名。
- /// 进入参数。
- /// 如果所有守卫都允许进入返回true,否则返回false。
- private async Task 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;
- }
-
- ///
- /// 执行离开场景的守卫检查。
- /// 按优先级顺序执行所有守卫的CanLeaveAsync方法。
- ///
- /// 场景键名。
- /// 如果所有守卫都允许离开返回true,否则返回false。
- private async Task 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
}
\ No newline at end of file
diff --git a/GFramework.Game/UI/UiRouterBase.cs b/GFramework.Game/UI/UiRouterBase.cs
index 529c2cb..f105cbb 100644
--- a/GFramework.Game/UI/UiRouterBase.cs
+++ b/GFramework.Game/UI/UiRouterBase.cs
@@ -1,9 +1,9 @@
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Extensions;
using GFramework.Core.Logging;
-using GFramework.Core.Systems;
using GFramework.Game.Abstractions.Enums;
using GFramework.Game.Abstractions.UI;
+using GFramework.Game.Routing;
namespace GFramework.Game.UI;
@@ -11,15 +11,10 @@ namespace GFramework.Game.UI;
/// UI路由基类,提供页面栈管理和层级UI管理功能
/// 负责UI页面的导航、显示、隐藏以及生命周期管理
///
-public abstract class UiRouterBase : AbstractSystem, IUiRouter
+public abstract class UiRouterBase : RouterBase, IUiRouter
{
private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger(nameof(UiRouterBase));
- ///
- /// 路由守卫列表,用于控制UI页面的进入和离开
- ///
- private readonly List _guards = new();
-
///
/// 层级管理字典(非栈层级),用于管理Overlay、Modal、Toast等浮层UI
/// Key: UiLayer枚举值, Value: InstanceId到PageBehavior的映射字典
@@ -31,11 +26,6 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
///
private readonly UiTransitionPipeline _pipeline = new();
- ///
- /// 页面栈,用于管理UI页面的显示顺序和导航历史
- ///
- private readonly Stack _stack = new();
-
///
/// UI工厂实例,用于创建UI页面和相关对象
///
@@ -98,7 +88,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
}
var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param);
- Log.Debug("Push UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, _stack.Count);
+ Log.Debug("Push UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, Stack.Count);
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
@@ -126,7 +116,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
}
var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param);
- Log.Debug("Push existing UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, _stack.Count);
+ Log.Debug("Push existing UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, Stack.Count);
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
@@ -142,13 +132,13 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// 页面弹出策略
public async ValueTask PopAsync(UiPopPolicy policy = UiPopPolicy.Destroy)
{
- if (_stack.Count == 0)
+ if (Stack.Count == 0)
{
Log.Debug("Pop ignored: stack is empty");
return;
}
- var leavingUiKey = _stack.Peek().Key;
+ var leavingUiKey = Stack.Peek().Key;
if (!await ExecuteLeaveGuardsAsync(leavingUiKey))
{
@@ -156,7 +146,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
return;
}
- var nextUiKey = _stack.Count > 1 ? _stack.ElementAt(1).Key : null;
+ var nextUiKey = Stack.Count > 1 ? Stack.ElementAt(1).Key : null;
var @event = CreateEvent(nextUiKey, UiTransitionType.Pop);
await _pipeline.ExecuteAroundAsync(@event, async () =>
@@ -226,7 +216,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
public async ValueTask ClearAsync()
{
var @event = CreateEvent(string.Empty, UiTransitionType.Clear);
- Log.Debug("Clear UI Stack, stackCount={0}", _stack.Count);
+ Log.Debug("Clear UI Stack, stackCount={0}", Stack.Count);
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
@@ -240,9 +230,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// 获取栈顶元素的键值
///
/// 栈顶UI页面的键值,如果栈为空则返回空字符串
- public string PeekKey()
+ public new string PeekKey()
{
- return _stack.Count == 0 ? string.Empty : _stack.Peek().Key;
+ return Stack.Count == 0 ? string.Empty : Stack.Peek().Key;
}
///
@@ -251,7 +241,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// 栈顶UI页面行为实例,如果栈为空则返回null
public IUiPageBehavior? Peek()
{
- return _stack.Count == 0 ? null : _stack.Peek();
+ return Stack.Count == 0 ? null : Stack.Peek();
}
///
@@ -259,9 +249,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
///
/// 要检查的UI页面键值
/// 如果栈顶是指定UI则返回true,否则返回false
- public bool IsTop(string uiKey)
+ public new bool IsTop(string uiKey)
{
- return _stack.Count != 0 && _stack.Peek().Key.Equals(uiKey);
+ return Stack.Count != 0 && Stack.Peek().Key.Equals(uiKey);
}
///
@@ -269,15 +259,15 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
///
/// 要检查的UI页面键值
/// 如果栈中包含指定UI则返回true,否则返回false
- public bool Contains(string uiKey)
+ public new bool Contains(string uiKey)
{
- return _stack.Any(p => p.Key.Equals(uiKey));
+ return Stack.Any(p => p.Key.Equals(uiKey));
}
///
/// 获取栈深度
///
- public int Count => _stack.Count;
+ public new int Count => Stack.Count;
#endregion
@@ -458,51 +448,6 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
#endregion
- #region Route Guards
-
- ///
- /// 注册路由守卫
- ///
- /// 路由守卫实例
- /// 当守卫实例为null时抛出
- public void AddGuard(IUiRouteGuard guard)
- {
- ArgumentNullException.ThrowIfNull(guard);
-
- if (_guards.Contains(guard))
- {
- Log.Debug("Guard already registered: {0}", guard.GetType().Name);
- return;
- }
-
- _guards.Add(guard);
- _guards.Sort((a, b) => a.Priority.CompareTo(b.Priority));
- Log.Debug("Guard registered: {0}, Priority={1}", guard.GetType().Name, guard.Priority);
- }
-
- ///
- /// 注册路由守卫(泛型)
- ///
- /// 路由守卫类型,必须实现IUiRouteGuard接口且有无参构造函数
- public void AddGuard() where T : IUiRouteGuard, new()
- {
- AddGuard(new T());
- }
-
- ///
- /// 移除路由守卫
- ///
- /// 要移除的路由守卫实例
- /// 当守卫实例为null时抛出
- public void RemoveGuard(IUiRouteGuard guard)
- {
- ArgumentNullException.ThrowIfNull(guard);
- if (_guards.Remove(guard))
- Log.Debug("Guard removed: {0}", guard.GetType().Name);
- }
-
- #endregion
-
#region Initialization
///
@@ -525,7 +470,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// 抽象方法,用于注册具体的处理程序。
/// 子类必须实现此方法以完成特定的处理逻辑注册。
///
- protected abstract void RegisterHandlers();
+ protected override abstract void RegisterHandlers();
#endregion
@@ -655,9 +600,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// 过渡策略
private void DoPushPageInternal(IUiPageBehavior page, IUiPageEnterParam? param, UiTransitionPolicy policy)
{
- if (_stack.Count > 0)
+ if (Stack.Count > 0)
{
- var current = _stack.Peek();
+ var current = Stack.Peek();
Log.Debug("Pause current page: {0}", current.View.GetType().Name);
current.OnPause();
@@ -671,9 +616,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
Log.Debug("Add page to UiRoot: {0}", page.View.GetType().Name);
_uiRoot.AddUiPage(page);
- _stack.Push(page);
+ Stack.Push(page);
- Log.Debug("Enter & Show page: {0}, stackAfter={1}", page.View.GetType().Name, _stack.Count);
+ Log.Debug("Enter & Show page: {0}, stackAfter={1}", page.View.GetType().Name, Stack.Count);
page.OnEnter(param);
page.OnShow();
}
@@ -684,12 +629,12 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// 页面弹出策略
private void DoPopInternal(UiPopPolicy policy)
{
- if (_stack.Count == 0)
+ if (Stack.Count == 0)
return;
- var top = _stack.Pop();
+ var top = Stack.Pop();
Log.Debug("Pop UI Page internal: {0}, policy={1}, stackAfterPop={2}",
- top.GetType().Name, policy, _stack.Count);
+ top.GetType().Name, policy, Stack.Count);
if (policy == UiPopPolicy.Destroy)
{
@@ -701,9 +646,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
top.OnHide();
}
- if (_stack.Count > 0)
+ if (Stack.Count > 0)
{
- var next = _stack.Peek();
+ var next = Stack.Peek();
next.OnResume();
next.OnShow();
}
@@ -715,85 +660,10 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// 页面弹出策略
private void DoClearInternal(UiPopPolicy policy)
{
- Log.Debug("Clear UI Stack internal, count={0}", _stack.Count);
- while (_stack.Count > 0)
+ Log.Debug("Clear UI Stack internal, count={0}", Stack.Count);
+ while (Stack.Count > 0)
DoPopInternal(policy);
}
- ///
- /// 执行进入守卫检查
- ///
- /// UI页面键值
- /// 页面进入参数
- /// 如果允许进入则返回true,否则返回false
- private async Task ExecuteEnterGuardsAsync(string uiKey, IUiPageEnterParam? param)
- {
- foreach (var guard in _guards)
- {
- try
- {
- Log.Debug("Executing enter guard: {0} for {1}", guard.GetType().Name, uiKey);
- var canEnter = await guard.CanEnterAsync(uiKey, 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;
- }
-
- ///
- /// 执行离开守卫检查
- ///
- /// UI页面键值
- /// 如果允许离开则返回true,否则返回false
- private async Task ExecuteLeaveGuardsAsync(string uiKey)
- {
- foreach (var guard in _guards)
- {
- try
- {
- Log.Debug("Executing leave guard: {0} for {1}", guard.GetType().Name, uiKey);
- var canLeave = await guard.CanLeaveAsync(uiKey);
-
- 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
}
\ No newline at end of file
diff --git a/GFramework.csproj b/GFramework.csproj
index aa36eaf..20f2339 100644
--- a/GFramework.csproj
+++ b/GFramework.csproj
@@ -55,6 +55,7 @@
+
@@ -93,6 +94,7 @@
+
@@ -117,6 +119,7 @@
+
diff --git a/GFramework.sln b/GFramework.sln
index 04b1d8f..7076c6a 100644
--- a/GFramework.sln
+++ b/GFramework.sln
@@ -32,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Ecs.Arch", "GFra
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Ecs.Arch.Tests", "GFramework.Ecs.Arch.Tests\GFramework.Ecs.Arch.Tests.csproj", "{112CF413-4596-4AA3-B3FE-65532802FDD6}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Game.Tests", "GFramework.Game.Tests\GFramework.Game.Tests.csproj", "{738DC58A-0387-4D75-AA96-1C1D8C29D350}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -234,6 +236,18 @@ Global
{112CF413-4596-4AA3-B3FE-65532802FDD6}.Release|x64.Build.0 = Release|Any CPU
{112CF413-4596-4AA3-B3FE-65532802FDD6}.Release|x86.ActiveCfg = Release|Any CPU
{112CF413-4596-4AA3-B3FE-65532802FDD6}.Release|x86.Build.0 = Release|Any CPU
+ {738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|x64.Build.0 = Debug|Any CPU
+ {738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|x86.Build.0 = Debug|Any CPU
+ {738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|Any CPU.Build.0 = Release|Any CPU
+ {738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x64.ActiveCfg = Release|Any CPU
+ {738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x64.Build.0 = Release|Any CPU
+ {738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x86.ActiveCfg = Release|Any CPU
+ {738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE