mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
Merge pull request #84 from GeWuYou/docs/technical-documentation-update
Docs/technical documentation update
This commit is contained in:
commit
519e3a480b
5
.claude/settings.json
Normal file
5
.claude/settings.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"enabledPlugins": {
|
||||
"oh-my-claudecode@omc": true
|
||||
}
|
||||
}
|
||||
205
.claude/skills/_shared/DOCUMENTATION_STANDARDS.md
Normal file
205
.claude/skills/_shared/DOCUMENTATION_STANDARDS.md
Normal file
@ -0,0 +1,205 @@
|
||||
# GFramework 文档编写规范
|
||||
|
||||
## Markdown 语法规范
|
||||
|
||||
### 1. 泛型标记转义
|
||||
|
||||
在 Markdown 文档中,所有泛型标记必须转义,否则会被 VitePress 误认为 HTML 标签。
|
||||
|
||||
**错误示例**:
|
||||
```markdown
|
||||
`Option<T>` 是一个泛型类型
|
||||
`Result<TValue, TError>` 表示结果
|
||||
public class Repository<TData> { }
|
||||
```
|
||||
|
||||
**正确示例**:
|
||||
```markdown
|
||||
`Option<T>` 是一个泛型类型
|
||||
`Result<TValue, TError>` 表示结果
|
||||
public class Repository<TData> { }
|
||||
```
|
||||
|
||||
**常见泛型标记**:
|
||||
- `<T>` → `<T>`
|
||||
- `<TResult>` → `<TResult>`
|
||||
- `<TValue>` → `<TValue>`
|
||||
- `<TError>` → `<TError>`
|
||||
- `<TSaveData>` → `<TSaveData>`
|
||||
- `<TData>` → `<TData>`
|
||||
- `<TNode>` → `<TNode>`
|
||||
|
||||
### 2. HTML 标签转义
|
||||
|
||||
如果需要在文档中显示 HTML 标签,必须转义:
|
||||
- `<summary>` → `<summary>`
|
||||
- `<param>` → `<param>`
|
||||
- `<returns>` → `<returns>`
|
||||
|
||||
### 3. 链接验证
|
||||
|
||||
**内部链接规则**:
|
||||
- 使用相对路径: `/zh-CN/core/events`
|
||||
- 确保目标文件存在
|
||||
- 不要链接到尚未创建的页面
|
||||
|
||||
**已存在的文档路径**:
|
||||
|
||||
**Core 模块**:
|
||||
- `/zh-CN/core/architecture` - 架构系统
|
||||
- `/zh-CN/core/ioc` - IoC 容器
|
||||
- `/zh-CN/core/events` - 事件系统
|
||||
- `/zh-CN/core/command` - 命令系统
|
||||
- `/zh-CN/core/query` - 查询系统
|
||||
- `/zh-CN/core/model` - Model 系统
|
||||
- `/zh-CN/core/system` - System 系统
|
||||
- `/zh-CN/core/utility` - Utility 系统
|
||||
- `/zh-CN/core/controller` - Controller 系统
|
||||
- `/zh-CN/core/logging` - 日志系统
|
||||
- `/zh-CN/core/pool` - 对象池
|
||||
- `/zh-CN/core/property` - 可绑定属性
|
||||
- `/zh-CN/core/lifecycle` - 生命周期管理
|
||||
- `/zh-CN/core/coroutine` - 协程系统
|
||||
- `/zh-CN/core/resource` - 资源管理
|
||||
- `/zh-CN/core/state-machine` - 状态机
|
||||
- `/zh-CN/core/cqrs` - CQRS 与 Mediator
|
||||
- `/zh-CN/core/functional` - 函数式编程
|
||||
- `/zh-CN/core/pause` - 暂停管理
|
||||
- `/zh-CN/core/configuration` - 配置管理
|
||||
- `/zh-CN/core/ecs` - ECS 系统集成
|
||||
- `/zh-CN/core/extensions` - 扩展方法
|
||||
- `/zh-CN/core/rule` - 规则系统
|
||||
- `/zh-CN/core/environment` - 环境系统
|
||||
- `/zh-CN/core/context` - 上下文系统
|
||||
- `/zh-CN/core/async-initialization` - 异步初始化
|
||||
|
||||
**Game 模块**:
|
||||
- `/zh-CN/game/scene` - 场景系统
|
||||
- `/zh-CN/game/ui` - UI 系统
|
||||
- `/zh-CN/game/data` - 数据与存档
|
||||
- `/zh-CN/game/storage` - 存储系统
|
||||
- `/zh-CN/game/serialization` - 序列化系统
|
||||
- `/zh-CN/game/setting` - 设置系统
|
||||
|
||||
**Godot 模块**:
|
||||
- `/zh-CN/godot/architecture` - Godot 架构集成
|
||||
- `/zh-CN/godot/scene` - Godot 场景系统
|
||||
- `/zh-CN/godot/ui` - Godot UI 系统
|
||||
- `/zh-CN/godot/pool` - Godot 节点池
|
||||
- `/zh-CN/godot/resource` - Godot 资源仓储
|
||||
- `/zh-CN/godot/logging` - Godot 日志系统
|
||||
- `/zh-CN/godot/pause` - Godot 暂停处理
|
||||
- `/zh-CN/godot/extensions` - Godot 扩展
|
||||
- `/zh-CN/godot/coroutine` - Godot 协程
|
||||
- `/zh-CN/godot/signal` - Godot 信号
|
||||
- `/zh-CN/godot/storage` - Godot 存储
|
||||
|
||||
**教程**:
|
||||
- `/zh-CN/tutorials/coroutine-tutorial` - 协程系统教程
|
||||
- `/zh-CN/tutorials/state-machine-tutorial` - 状态机教程
|
||||
- `/zh-CN/tutorials/resource-management` - 资源管理教程
|
||||
- `/zh-CN/tutorials/save-system` - 存档系统教程
|
||||
- `/zh-CN/tutorials/godot-complete-project` - Godot 完整项目
|
||||
- `/zh-CN/tutorials/functional-programming` - 函数式编程实践
|
||||
- `/zh-CN/tutorials/pause-system` - 暂停系统实现
|
||||
- `/zh-CN/tutorials/data-migration` - 数据迁移实践
|
||||
- `/zh-CN/tutorials/godot-integration` - Godot 集成
|
||||
- `/zh-CN/tutorials/advanced-patterns` - 高级模式
|
||||
|
||||
**其他**:
|
||||
- `/zh-CN/getting-started/quick-start` - 快速开始
|
||||
- `/zh-CN/getting-started/installation` - 安装指南
|
||||
- `/zh-CN/best-practices/architecture-patterns` - 架构模式
|
||||
|
||||
**不存在的路径** (不要链接):
|
||||
- `/zh-CN/best-practices/performance` - 尚未创建
|
||||
- `/zh-CN/core/serializer` - 错误路径,应使用 `/zh-CN/game/serialization`
|
||||
|
||||
## 代码块规范
|
||||
|
||||
### 1. 代码块语言标识
|
||||
|
||||
始终指定代码块的语言:
|
||||
|
||||
```markdown
|
||||
\`\`\`csharp
|
||||
public class Example { }
|
||||
\`\`\`
|
||||
|
||||
\`\`\`bash
|
||||
npm install
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
### 2. 代码注释
|
||||
|
||||
代码示例应包含中文注释:
|
||||
|
||||
```csharp
|
||||
// 创建玩家实体
|
||||
var player = new Player
|
||||
{
|
||||
Name = "玩家1", // 玩家名称
|
||||
Level = 1 // 初始等级
|
||||
};
|
||||
```
|
||||
|
||||
## Frontmatter 规范
|
||||
|
||||
每个文档必须包含正确的 frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: 文档标题
|
||||
description: 简短描述(1-2 句话)
|
||||
---
|
||||
```
|
||||
|
||||
## 文档结构规范
|
||||
|
||||
### 指南文档结构
|
||||
|
||||
1. 概述
|
||||
2. 核心概念
|
||||
3. 基本用法
|
||||
4. 高级用法
|
||||
5. 最佳实践
|
||||
6. 常见问题
|
||||
7. 相关文档
|
||||
|
||||
### 教程文档结构
|
||||
|
||||
1. 学习目标
|
||||
2. 前置条件
|
||||
3. 步骤 1-N (3-7 步)
|
||||
4. 完整代码
|
||||
5. 运行结果
|
||||
6. 下一步
|
||||
7. 相关文档
|
||||
|
||||
## 验证清单
|
||||
|
||||
生成文档后,必须检查:
|
||||
|
||||
- [ ] 所有泛型标记已转义 (`<T>` → `<T>`)
|
||||
- [ ] 所有内部链接指向存在的页面
|
||||
- [ ] Frontmatter 格式正确
|
||||
- [ ] 代码块指定了语言
|
||||
- [ ] 代码包含中文注释
|
||||
- [ ] 文档结构完整
|
||||
- [ ] 没有 HTML 标签错误
|
||||
|
||||
## 自动修复脚本
|
||||
|
||||
如果文档已生成,可以使用以下脚本修复常见问题:
|
||||
|
||||
```bash
|
||||
# 修复泛型标记
|
||||
sed -i 's/<T>/\<T\>/g' file.md
|
||||
sed -i 's/<TResult>/\<TResult\>/g' file.md
|
||||
sed -i 's/<TValue>/\<TValue\>/g' file.md
|
||||
sed -i 's/<TError>/\<TError\>/g' file.md
|
||||
|
||||
# 验证构建
|
||||
cd docs && bun run build
|
||||
```
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -5,5 +5,9 @@ riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
GFramework.sln.DotSettings.user
|
||||
.idea/
|
||||
# ai
|
||||
opencode.json
|
||||
.claude/settings.local.json
|
||||
.omc/
|
||||
docs/.omc/
|
||||
docs/.vitepress/cache/
|
||||
31
docs/.vitepress/cache/deps/_metadata.json
vendored
31
docs/.vitepress/cache/deps/_metadata.json
vendored
@ -1,31 +0,0 @@
|
||||
{
|
||||
"hash": "2500e5b1",
|
||||
"configHash": "3af8c6da",
|
||||
"lockfileHash": "42b6a898",
|
||||
"browserHash": "36f3405c",
|
||||
"optimized": {
|
||||
"vue": {
|
||||
"src": "../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "d870a771",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vue/devtools-api": {
|
||||
"src": "../../../node_modules/@vue/devtools-api/dist/index.js",
|
||||
"file": "vitepress___@vue_devtools-api.js",
|
||||
"fileHash": "d4c69f44",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vueuse/core": {
|
||||
"src": "../../../node_modules/@vueuse/core/dist/index.js",
|
||||
"file": "vitepress___@vueuse_core.js",
|
||||
"fileHash": "05ae35d2",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"chunk-FOJXB67H": {
|
||||
"file": "chunk-FOJXB67H.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
12828
docs/.vitepress/cache/deps/chunk-FOJXB67H.js
vendored
12828
docs/.vitepress/cache/deps/chunk-FOJXB67H.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
3
docs/.vitepress/cache/deps/package.json
vendored
3
docs/.vitepress/cache/deps/package.json
vendored
@ -1,3 +0,0 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
8737
docs/.vitepress/cache/deps/vitepress___@vueuse_core.js
vendored
8737
docs/.vitepress/cache/deps/vitepress___@vueuse_core.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
346
docs/.vitepress/cache/deps/vue.js
vendored
346
docs/.vitepress/cache/deps/vue.js
vendored
@ -1,346 +0,0 @@
|
||||
import {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
Comment,
|
||||
DeprecationTypes,
|
||||
EffectScope,
|
||||
ErrorCodes,
|
||||
ErrorTypeStrings,
|
||||
Fragment,
|
||||
KeepAlive,
|
||||
ReactiveEffect,
|
||||
Static,
|
||||
Suspense,
|
||||
Teleport,
|
||||
Text,
|
||||
TrackOpTypes,
|
||||
Transition,
|
||||
TransitionGroup,
|
||||
TriggerOpTypes,
|
||||
VueElement,
|
||||
assertNumber,
|
||||
callWithAsyncErrorHandling,
|
||||
callWithErrorHandling,
|
||||
camelize,
|
||||
capitalize,
|
||||
cloneVNode,
|
||||
compatUtils,
|
||||
compile,
|
||||
computed,
|
||||
createApp,
|
||||
createBaseVNode,
|
||||
createBlock,
|
||||
createCommentVNode,
|
||||
createElementBlock,
|
||||
createHydrationRenderer,
|
||||
createPropsRestProxy,
|
||||
createRenderer,
|
||||
createSSRApp,
|
||||
createSlots,
|
||||
createStaticVNode,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
customRef,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
defineModel,
|
||||
defineOptions,
|
||||
defineProps,
|
||||
defineSSRCustomElement,
|
||||
defineSlots,
|
||||
devtools,
|
||||
effect,
|
||||
effectScope,
|
||||
getCurrentInstance,
|
||||
getCurrentScope,
|
||||
getCurrentWatcher,
|
||||
getTransitionRawChildren,
|
||||
guardReactiveProps,
|
||||
h,
|
||||
handleError,
|
||||
hasInjectionContext,
|
||||
hydrate,
|
||||
hydrateOnIdle,
|
||||
hydrateOnInteraction,
|
||||
hydrateOnMediaQuery,
|
||||
hydrateOnVisible,
|
||||
initCustomFormatter,
|
||||
initDirectivesForSSR,
|
||||
inject,
|
||||
isMemoSame,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isRef,
|
||||
isRuntimeOnly,
|
||||
isShallow,
|
||||
isVNode,
|
||||
markRaw,
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
mergeProps,
|
||||
nextTick,
|
||||
nodeOps,
|
||||
normalizeClass,
|
||||
normalizeProps,
|
||||
normalizeStyle,
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onDeactivated,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onScopeDispose,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
onWatcherCleanup,
|
||||
openBlock,
|
||||
patchProp,
|
||||
popScopeId,
|
||||
provide,
|
||||
proxyRefs,
|
||||
pushScopeId,
|
||||
queuePostFlushCb,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
registerRuntimeCompiler,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent,
|
||||
resolveFilter,
|
||||
resolveTransitionHooks,
|
||||
setBlockTracking,
|
||||
setDevtoolsHook,
|
||||
setTransitionHooks,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
ssrContextKey,
|
||||
ssrUtils,
|
||||
stop,
|
||||
toDisplayString,
|
||||
toHandlerKey,
|
||||
toHandlers,
|
||||
toRaw,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
transformVNodeArgs,
|
||||
triggerRef,
|
||||
unref,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
useHost,
|
||||
useId,
|
||||
useModel,
|
||||
useSSRContext,
|
||||
useShadowRoot,
|
||||
useSlots,
|
||||
useTemplateRef,
|
||||
useTransitionState,
|
||||
vModelCheckbox,
|
||||
vModelDynamic,
|
||||
vModelRadio,
|
||||
vModelSelect,
|
||||
vModelText,
|
||||
vShow,
|
||||
version,
|
||||
warn,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
withAsyncContext,
|
||||
withCtx,
|
||||
withDefaults,
|
||||
withDirectives,
|
||||
withKeys,
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
} from "./chunk-FOJXB67H.js";
|
||||
export {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
Comment,
|
||||
DeprecationTypes,
|
||||
EffectScope,
|
||||
ErrorCodes,
|
||||
ErrorTypeStrings,
|
||||
Fragment,
|
||||
KeepAlive,
|
||||
ReactiveEffect,
|
||||
Static,
|
||||
Suspense,
|
||||
Teleport,
|
||||
Text,
|
||||
TrackOpTypes,
|
||||
Transition,
|
||||
TransitionGroup,
|
||||
TriggerOpTypes,
|
||||
VueElement,
|
||||
assertNumber,
|
||||
callWithAsyncErrorHandling,
|
||||
callWithErrorHandling,
|
||||
camelize,
|
||||
capitalize,
|
||||
cloneVNode,
|
||||
compatUtils,
|
||||
compile,
|
||||
computed,
|
||||
createApp,
|
||||
createBlock,
|
||||
createCommentVNode,
|
||||
createElementBlock,
|
||||
createBaseVNode as createElementVNode,
|
||||
createHydrationRenderer,
|
||||
createPropsRestProxy,
|
||||
createRenderer,
|
||||
createSSRApp,
|
||||
createSlots,
|
||||
createStaticVNode,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
customRef,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
defineModel,
|
||||
defineOptions,
|
||||
defineProps,
|
||||
defineSSRCustomElement,
|
||||
defineSlots,
|
||||
devtools,
|
||||
effect,
|
||||
effectScope,
|
||||
getCurrentInstance,
|
||||
getCurrentScope,
|
||||
getCurrentWatcher,
|
||||
getTransitionRawChildren,
|
||||
guardReactiveProps,
|
||||
h,
|
||||
handleError,
|
||||
hasInjectionContext,
|
||||
hydrate,
|
||||
hydrateOnIdle,
|
||||
hydrateOnInteraction,
|
||||
hydrateOnMediaQuery,
|
||||
hydrateOnVisible,
|
||||
initCustomFormatter,
|
||||
initDirectivesForSSR,
|
||||
inject,
|
||||
isMemoSame,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isRef,
|
||||
isRuntimeOnly,
|
||||
isShallow,
|
||||
isVNode,
|
||||
markRaw,
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
mergeProps,
|
||||
nextTick,
|
||||
nodeOps,
|
||||
normalizeClass,
|
||||
normalizeProps,
|
||||
normalizeStyle,
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onDeactivated,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onScopeDispose,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
onWatcherCleanup,
|
||||
openBlock,
|
||||
patchProp,
|
||||
popScopeId,
|
||||
provide,
|
||||
proxyRefs,
|
||||
pushScopeId,
|
||||
queuePostFlushCb,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
registerRuntimeCompiler,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent,
|
||||
resolveFilter,
|
||||
resolveTransitionHooks,
|
||||
setBlockTracking,
|
||||
setDevtoolsHook,
|
||||
setTransitionHooks,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
ssrContextKey,
|
||||
ssrUtils,
|
||||
stop,
|
||||
toDisplayString,
|
||||
toHandlerKey,
|
||||
toHandlers,
|
||||
toRaw,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
transformVNodeArgs,
|
||||
triggerRef,
|
||||
unref,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
useHost,
|
||||
useId,
|
||||
useModel,
|
||||
useSSRContext,
|
||||
useShadowRoot,
|
||||
useSlots,
|
||||
useTemplateRef,
|
||||
useTransitionState,
|
||||
vModelCheckbox,
|
||||
vModelDynamic,
|
||||
vModelRadio,
|
||||
vModelSelect,
|
||||
vModelText,
|
||||
vShow,
|
||||
version,
|
||||
warn,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
withAsyncContext,
|
||||
withCtx,
|
||||
withDefaults,
|
||||
withDirectives,
|
||||
withKeys,
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
};
|
||||
7
docs/.vitepress/cache/deps/vue.js.map
vendored
7
docs/.vitepress/cache/deps/vue.js.map
vendored
@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
@ -78,8 +78,10 @@ export default defineConfig({
|
||||
{ text: '事件系统', link: '/zh-CN/core/events' },
|
||||
{ text: '属性系统', link: '/zh-CN/core/property' },
|
||||
{ text: 'IoC容器', link: '/zh-CN/core/ioc' },
|
||||
{ text: 'ECS 系统集成', link: '/zh-CN/core/ecs' },
|
||||
{ text: '对象池', link: '/zh-CN/core/pool' },
|
||||
{ text: '日志系统', link: '/zh-CN/core/logging' },
|
||||
{ text: '函数式编程', link: '/zh-CN/core/functional' },
|
||||
{ text: '扩展方法', link: '/zh-CN/core/extensions' },
|
||||
{ text: '工具类', link: '/zh-CN/core/utility' },
|
||||
{ text: '模型层', link: '/zh-CN/core/model' },
|
||||
@ -105,6 +107,10 @@ export default defineConfig({
|
||||
text: 'Godot 集成',
|
||||
items: [
|
||||
{ text: '概览', link: '/zh-CN/godot/' },
|
||||
{ text: '架构集成', link: '/zh-CN/godot/architecture' },
|
||||
{ text: '场景系统', link: '/zh-CN/godot/scene' },
|
||||
{ text: 'UI 系统', link: '/zh-CN/godot/ui' },
|
||||
{ text: '资源仓储', link: '/zh-CN/godot/resource' },
|
||||
{ text: '协程系统', link: '/zh-CN/godot/coroutine' },
|
||||
{ text: '节点扩展', link: '/zh-CN/godot/extensions' },
|
||||
{ text: '信号系统', link: '/zh-CN/godot/signal' },
|
||||
@ -156,7 +162,13 @@ export default defineConfig({
|
||||
{ text: '7. 总结与最佳实践', link: '/zh-CN/tutorials/basic/07-summary' }
|
||||
]
|
||||
},
|
||||
{ text: '使用协程系统', link: '/zh-CN/tutorials/coroutine-tutorial' },
|
||||
{ text: '实现状态机', link: '/zh-CN/tutorials/state-machine-tutorial' },
|
||||
{ text: '函数式编程实践', link: '/zh-CN/tutorials/functional-programming' },
|
||||
{ text: '资源管理最佳实践', link: '/zh-CN/tutorials/resource-management' },
|
||||
{ text: '实现存档系统', link: '/zh-CN/tutorials/save-system' },
|
||||
{ text: 'Godot 集成', link: '/zh-CN/tutorials/godot-integration' },
|
||||
{ text: 'Godot 完整项目', link: '/zh-CN/tutorials/godot-complete-project' },
|
||||
{ text: '高级模式', link: '/zh-CN/tutorials/advanced-patterns' }
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1303
docs/zh-CN/best-practices/error-handling.md
Normal file
1303
docs/zh-CN/best-practices/error-handling.md
Normal file
File diff suppressed because it is too large
Load Diff
384
docs/zh-CN/best-practices/mobile-optimization.md
Normal file
384
docs/zh-CN/best-practices/mobile-optimization.md
Normal file
@ -0,0 +1,384 @@
|
||||
---
|
||||
title: 移动平台优化指南
|
||||
description: 针对移动平台的性能优化、内存管理和电池优化最佳实践
|
||||
---
|
||||
|
||||
# 移动平台优化指南
|
||||
|
||||
## 概述
|
||||
|
||||
移动平台游戏开发面临着独特的挑战:有限的内存、较弱的处理器、电池续航限制、触摸输入、多样的屏幕尺寸等。本指南将帮助你使用
|
||||
GFramework 开发高性能的移动游戏,提供针对性的优化策略和最佳实践。
|
||||
|
||||
**移动平台的主要限制**:
|
||||
|
||||
- **内存限制**:移动设备内存通常在 2-8GB,远低于 PC
|
||||
- **CPU 性能**:移动 CPU 性能较弱,且受热量限制
|
||||
- **GPU 性能**:移动 GPU 功能有限,填充率和带宽受限
|
||||
- **电池续航**:高性能运行会快速消耗电池
|
||||
- **存储空间**:应用包大小受限,用户存储空间有限
|
||||
- **网络环境**:移动网络不稳定,延迟较高
|
||||
|
||||
**优化目标**:
|
||||
|
||||
- 减少内存占用(目标:<200MB)
|
||||
- 降低 CPU 使用率(目标:<30%)
|
||||
- 优化 GPU 渲染(目标:60 FPS)
|
||||
- 延长电池续航(目标:3+ 小时)
|
||||
- 减小包体大小(目标:<100MB)
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 1. 内存管理
|
||||
|
||||
移动设备内存有限,需要精细管理:
|
||||
|
||||
```csharp
|
||||
// 监控内存使用
|
||||
public class MemoryMonitor : AbstractSystem
|
||||
{
|
||||
private const long MemoryWarningThreshold = 150 * 1024 * 1024; // 150MB
|
||||
private const long MemoryCriticalThreshold = 200 * 1024 * 1024; // 200MB
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
this.RegisterEvent<GameUpdateEvent>(OnUpdate);
|
||||
}
|
||||
|
||||
private void OnUpdate(GameUpdateEvent e)
|
||||
{
|
||||
// 每 5 秒检查一次内存
|
||||
if (e.TotalTime % 5.0 < e.DeltaTime)
|
||||
{
|
||||
CheckMemoryUsage();
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckMemoryUsage()
|
||||
{
|
||||
var memoryUsage = GC.GetTotalMemory(false);
|
||||
|
||||
if (memoryUsage > MemoryCriticalThreshold)
|
||||
{
|
||||
// 内存严重不足,强制清理
|
||||
SendEvent(new MemoryCriticalEvent());
|
||||
ForceMemoryCleanup();
|
||||
}
|
||||
else if (memoryUsage > MemoryWarningThreshold)
|
||||
{
|
||||
// 内存警告,温和清理
|
||||
SendEvent(new MemoryWarningEvent());
|
||||
SoftMemoryCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
private void ForceMemoryCleanup()
|
||||
{
|
||||
// 卸载不必要的资源
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
resourceManager.UnloadUnusedResources();
|
||||
|
||||
// 清理对象池
|
||||
var poolSystem = this.GetSystem<ObjectPoolSystem>();
|
||||
poolSystem.TrimPools();
|
||||
|
||||
// 强制 GC
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
private void SoftMemoryCleanup()
|
||||
{
|
||||
// 温和清理:只清理明确不需要的资源
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
resourceManager.UnloadUnusedResources();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 性能分析
|
||||
|
||||
使用性能分析工具识别瓶颈:
|
||||
|
||||
```csharp
|
||||
public class PerformanceProfiler : AbstractSystem
|
||||
{
|
||||
private readonly Dictionary<string, PerformanceMetrics> _metrics = new();
|
||||
|
||||
public IDisposable Profile(string name)
|
||||
{
|
||||
return new ProfileScope(name, this);
|
||||
}
|
||||
|
||||
private void RecordMetric(string name, double duration)
|
||||
{
|
||||
if (!_metrics.TryGetValue(name, out var metrics))
|
||||
{
|
||||
metrics = new PerformanceMetrics();
|
||||
_metrics[name] = metrics;
|
||||
}
|
||||
|
||||
metrics.AddSample(duration);
|
||||
}
|
||||
|
||||
public void PrintReport()
|
||||
{
|
||||
Console.WriteLine("\n=== 性能报告 ===");
|
||||
foreach (var (name, metrics) in _metrics.OrderByDescending(x => x.Value.AverageMs))
|
||||
{
|
||||
Console.WriteLine($"{name}:");
|
||||
Console.WriteLine($" 平均: {metrics.AverageMs:F2}ms");
|
||||
Console.WriteLine($" 最大: {metrics.MaxMs:F2}ms");
|
||||
Console.WriteLine($" 最小: {metrics.MinMs:F2}ms");
|
||||
Console.WriteLine($" 调用次数: {metrics.SampleCount}");
|
||||
}
|
||||
}
|
||||
|
||||
private class ProfileScope : IDisposable
|
||||
{
|
||||
private readonly string _name;
|
||||
private readonly PerformanceProfiler _profiler;
|
||||
private readonly Stopwatch _stopwatch;
|
||||
|
||||
public ProfileScope(string name, PerformanceProfiler profiler)
|
||||
{
|
||||
_name = name;
|
||||
_profiler = profiler;
|
||||
_stopwatch = Stopwatch.StartNew();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_stopwatch.Stop();
|
||||
_profiler.RecordMetric(_name, _stopwatch.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
public class GameSystem : AbstractSystem
|
||||
{
|
||||
private PerformanceProfiler _profiler;
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_profiler = this.GetSystem<PerformanceProfiler>();
|
||||
}
|
||||
|
||||
private void UpdateGame()
|
||||
{
|
||||
using (_profiler.Profile("GameUpdate"))
|
||||
{
|
||||
// 游戏更新逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 电池优化
|
||||
|
||||
减少不必要的计算和渲染:
|
||||
|
||||
```csharp
|
||||
public class PowerSavingSystem : AbstractSystem
|
||||
{
|
||||
private bool _isPowerSavingMode;
|
||||
private int _targetFrameRate = 60;
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
this.RegisterEvent<BatteryLowEvent>(OnBatteryLow);
|
||||
this.RegisterEvent<BatteryNormalEvent>(OnBatteryNormal);
|
||||
}
|
||||
|
||||
private void OnBatteryLow(BatteryLowEvent e)
|
||||
{
|
||||
EnablePowerSavingMode();
|
||||
}
|
||||
|
||||
private void OnBatteryNormal(BatteryNormalEvent e)
|
||||
{
|
||||
DisablePowerSavingMode();
|
||||
}
|
||||
|
||||
private void EnablePowerSavingMode()
|
||||
{
|
||||
_isPowerSavingMode = true;
|
||||
|
||||
// 降低帧率
|
||||
_targetFrameRate = 30;
|
||||
Application.targetFrameRate = _targetFrameRate;
|
||||
|
||||
// 降低渲染质量
|
||||
QualitySettings.SetQualityLevel(0);
|
||||
|
||||
// 减少粒子效果
|
||||
SendEvent(new ReduceEffectsEvent());
|
||||
|
||||
// 暂停非关键系统
|
||||
PauseNonCriticalSystems();
|
||||
|
||||
Console.WriteLine("省电模式已启用");
|
||||
}
|
||||
|
||||
private void DisablePowerSavingMode()
|
||||
{
|
||||
_isPowerSavingMode = false;
|
||||
|
||||
// 恢复帧率
|
||||
_targetFrameRate = 60;
|
||||
Application.targetFrameRate = _targetFrameRate;
|
||||
|
||||
// 恢复渲染质量
|
||||
QualitySettings.SetQualityLevel(2);
|
||||
|
||||
// 恢复粒子效果
|
||||
SendEvent(new RestoreEffectsEvent());
|
||||
|
||||
// 恢复非关键系统
|
||||
ResumeNonCriticalSystems();
|
||||
|
||||
Console.WriteLine("省电模式已禁用");
|
||||
}
|
||||
|
||||
private void PauseNonCriticalSystems()
|
||||
{
|
||||
// 暂停动画系统
|
||||
var animationSystem = this.GetSystem<AnimationSystem>();
|
||||
animationSystem?.Pause();
|
||||
|
||||
// 暂停音效系统(保留音乐)
|
||||
var audioSystem = this.GetSystem<AudioSystem>();
|
||||
audioSystem?.PauseSoundEffects();
|
||||
}
|
||||
|
||||
private void ResumeNonCriticalSystems()
|
||||
{
|
||||
var animationSystem = this.GetSystem<AnimationSystem>();
|
||||
animationSystem?.Resume();
|
||||
|
||||
var audioSystem = this.GetSystem<AudioSystem>();
|
||||
audioSystem?.ResumeSoundEffects();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 内存优化
|
||||
|
||||
### 1. 资源管理策略
|
||||
|
||||
实现智能资源加载和卸载:
|
||||
|
||||
```csharp
|
||||
public class MobileResourceManager : AbstractSystem
|
||||
{
|
||||
private readonly IResourceManager _resourceManager;
|
||||
private readonly Dictionary<string, ResourcePriority> _resourcePriorities = new();
|
||||
private readonly HashSet<string> _loadedResources = new();
|
||||
|
||||
public MobileResourceManager(IResourceManager resourceManager)
|
||||
{
|
||||
_resourceManager = resourceManager;
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 配置资源优先级
|
||||
ConfigureResourcePriorities();
|
||||
|
||||
// 监听场景切换事件
|
||||
this.RegisterEvent<SceneChangedEvent>(OnSceneChanged);
|
||||
|
||||
// 监听内存警告
|
||||
this.RegisterEvent<MemoryWarningEvent>(OnMemoryWarning);
|
||||
}
|
||||
|
||||
private void ConfigureResourcePriorities()
|
||||
{
|
||||
// 高优先级:UI、玩家资源
|
||||
_resourcePriorities["ui/"] = ResourcePriority.High;
|
||||
_resourcePriorities["player/"] = ResourcePriority.High;
|
||||
|
||||
// 中优先级:敌人、道具
|
||||
_resourcePriorities["enemy/"] = ResourcePriority.Medium;
|
||||
_resourcePriorities["item/"] = ResourcePriority.Medium;
|
||||
|
||||
// 低优先级:特效、装饰
|
||||
_resourcePriorities["effect/"] = ResourcePriority.Low;
|
||||
_resourcePriorities["decoration/"] = ResourcePriority.Low;
|
||||
}
|
||||
|
||||
public async Task<T> LoadResourceAsync<T>(string path) where T : class
|
||||
{
|
||||
// 检查内存
|
||||
if (IsMemoryLow())
|
||||
{
|
||||
// 内存不足,先清理低优先级资源
|
||||
UnloadLowPriorityResources();
|
||||
}
|
||||
|
||||
var resource = await _resourceManager.LoadAsync<T>(path);
|
||||
_loadedResources.Add(path);
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private void OnSceneChanged(SceneChangedEvent e)
|
||||
{
|
||||
// 场景切换时,卸载旧场景资源
|
||||
UnloadSceneResources(e.PreviousScene);
|
||||
|
||||
// 预加载新场景资源
|
||||
PreloadSceneResources(e.NewScene);
|
||||
}
|
||||
|
||||
private void OnMemoryWarning(MemoryWarningEvent e)
|
||||
{
|
||||
// 内存警告,卸载低优先级资源
|
||||
UnloadLowPriorityResources();
|
||||
}
|
||||
|
||||
private void UnloadLowPriorityResources()
|
||||
{
|
||||
var resourcesToUnload = _loadedResources
|
||||
.Where(path => GetResourcePriority(path) == ResourcePriority.Low)
|
||||
.ToList();
|
||||
|
||||
foreach (var path in resourcesToUnload)
|
||||
{
|
||||
_resourceManager.Unload(path);
|
||||
_loadedResources.Remove(path);
|
||||
}
|
||||
|
||||
Console.WriteLine($"卸载了 {resourcesToUnload.Count} 个低优先级资源");
|
||||
}
|
||||
|
||||
private ResourcePriority GetResourcePriority(string path)
|
||||
{
|
||||
foreach (var (prefix, priority) in _resourcePriorities)
|
||||
{
|
||||
if (path.StartsWith(prefix))
|
||||
return priority;
|
||||
}
|
||||
|
||||
return ResourcePriority.Medium;
|
||||
}
|
||||
|
||||
private bool IsMemoryLow()
|
||||
{
|
||||
var memoryUsage = GC.GetTotalMemory(false);
|
||||
return memoryUsage > 150 * 1024 * 1024; // 150MB
|
||||
}
|
||||
}
|
||||
|
||||
public enum ResourcePriority
|
||||
{
|
||||
Low,
|
||||
Medium,
|
||||
High
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 纹理压缩和优化
|
||||
|
||||
546
docs/zh-CN/best-practices/multiplayer.md
Normal file
546
docs/zh-CN/best-practices/multiplayer.md
Normal file
@ -0,0 +1,546 @@
|
||||
# 多人游戏架构指南
|
||||
|
||||
> 基于 GFramework 架构设计高性能、可扩展的多人游戏系统。
|
||||
|
||||
## 📋 目录
|
||||
|
||||
- [概述](#概述)
|
||||
- [核心概念](#核心概念)
|
||||
- [架构设计](#架构设计)
|
||||
- [状态管理](#状态管理)
|
||||
- [命令模式与输入处理](#命令模式与输入处理)
|
||||
- [事件同步](#事件同步)
|
||||
- [网络优化](#网络优化)
|
||||
- [安全考虑](#安全考虑)
|
||||
- [最佳实践](#最佳实践)
|
||||
- [常见问题](#常见问题)
|
||||
|
||||
## 概述
|
||||
|
||||
多人游戏开发面临着单机游戏所没有的独特挑战:
|
||||
|
||||
### 主要挑战
|
||||
|
||||
1. **网络延迟** - 玩家操作和服务器响应之间存在不可避免的延迟
|
||||
2. **状态同步** - 确保所有客户端看到一致的游戏状态
|
||||
3. **带宽限制** - 需要高效传输游戏数据,避免网络拥塞
|
||||
4. **作弊防护** - 防止客户端篡改游戏逻辑和数据
|
||||
5. **并发处理** - 同时处理多个玩家的输入和状态更新
|
||||
6. **断线重连** - 优雅处理网络中断和玩家重新连接
|
||||
|
||||
### GFramework 的优势
|
||||
|
||||
GFramework 的架构设计天然适合多人游戏开发:
|
||||
|
||||
- **分层架构** - 清晰分离客户端逻辑、网络层和服务器逻辑
|
||||
- **事件系统** - 松耦合的事件驱动架构便于状态同步
|
||||
- **命令模式** - 统一的输入处理和验证机制
|
||||
- **Model-System 分离** - 数据和逻辑分离便于状态管理
|
||||
- **模块化设计** - 网络功能可以作为独立模块集成
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 1. 客户端-服务器架构
|
||||
|
||||
```csharp
|
||||
// 服务器架构
|
||||
public class ServerArchitecture : Architecture
|
||||
{
|
||||
protected override void Init()
|
||||
{
|
||||
// 注册服务器专用的 Model
|
||||
RegisterModel(new ServerGameStateModel());
|
||||
RegisterModel(new PlayerConnectionModel());
|
||||
|
||||
// 注册服务器专用的 System
|
||||
RegisterSystem(new ServerNetworkSystem());
|
||||
RegisterSystem(new AuthorityGameLogicSystem());
|
||||
RegisterSystem(new StateReplicationSystem());
|
||||
RegisterSystem(new AntiCheatSystem());
|
||||
|
||||
// 注册工具
|
||||
RegisterUtility(new NetworkUtility());
|
||||
RegisterUtility(new ValidationUtility());
|
||||
}
|
||||
}
|
||||
|
||||
// 客户端架构
|
||||
public class ClientArchitecture : Architecture
|
||||
{
|
||||
protected override void Init()
|
||||
{
|
||||
// 注册客户端专用的 Model
|
||||
RegisterModel(new ClientGameStateModel());
|
||||
RegisterModel(new PredictionModel());
|
||||
|
||||
// 注册客户端专用的 System
|
||||
RegisterSystem(new ClientNetworkSystem());
|
||||
RegisterSystem(new PredictionSystem());
|
||||
RegisterSystem(new InterpolationSystem());
|
||||
RegisterSystem(new ClientInputSystem());
|
||||
|
||||
// 注册工具
|
||||
RegisterUtility(new NetworkUtility());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 状态同步策略
|
||||
|
||||
#### 状态同步 (State Synchronization)
|
||||
|
||||
服务器定期向客户端发送完整的游戏状态。
|
||||
|
||||
```csharp
|
||||
// 游戏状态快照
|
||||
public struct GameStateSnapshot
|
||||
{
|
||||
public uint Tick { get; set; }
|
||||
public long Timestamp { get; set; }
|
||||
public PlayerState[] Players { get; set; }
|
||||
public EntityState[] Entities { get; set; }
|
||||
}
|
||||
|
||||
public struct PlayerState
|
||||
{
|
||||
public string PlayerId { get; set; }
|
||||
public Vector3 Position { get; set; }
|
||||
public Quaternion Rotation { get; set; }
|
||||
public int Health { get; set; }
|
||||
public PlayerAnimationState AnimationState { get; set; }
|
||||
}
|
||||
|
||||
// 状态复制系统
|
||||
public class StateReplicationSystem : AbstractSystem
|
||||
{
|
||||
private ServerGameStateModel _gameState;
|
||||
private PlayerConnectionModel _connections;
|
||||
private uint _currentTick;
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_gameState = this.GetModel<ServerGameStateModel>();
|
||||
_connections = this.GetModel<PlayerConnectionModel>();
|
||||
|
||||
// 每个 tick 复制状态
|
||||
this.RegisterEvent<ServerTickEvent>(OnServerTick);
|
||||
}
|
||||
|
||||
private void OnServerTick(ServerTickEvent e)
|
||||
{
|
||||
_currentTick++;
|
||||
|
||||
// 创建状态快照
|
||||
var snapshot = CreateSnapshot();
|
||||
|
||||
// 发送给所有连接的客户端
|
||||
foreach (var connection in _connections.ActiveConnections)
|
||||
{
|
||||
SendSnapshotToClient(connection, snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
private GameStateSnapshot CreateSnapshot()
|
||||
{
|
||||
return new GameStateSnapshot
|
||||
{
|
||||
Tick = _currentTick,
|
||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
Players = _gameState.Players.Select(p => new PlayerState
|
||||
{
|
||||
PlayerId = p.Id,
|
||||
Position = p.Position,
|
||||
Rotation = p.Rotation,
|
||||
Health = p.Health,
|
||||
AnimationState = p.AnimationState
|
||||
}).ToArray(),
|
||||
Entities = _gameState.Entities.Select(e => CreateEntityState(e)).ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 增量同步 (Delta Synchronization)
|
||||
|
||||
只发送状态变化,减少带宽消耗。
|
||||
|
||||
```csharp
|
||||
// 增量状态
|
||||
public struct DeltaState
|
||||
{
|
||||
public uint Tick { get; set; }
|
||||
public uint BaseTick { get; set; }
|
||||
public PlayerDelta[] PlayerDeltas { get; set; }
|
||||
public EntityDelta[] EntityDeltas { get; set; }
|
||||
}
|
||||
|
||||
public struct PlayerDelta
|
||||
{
|
||||
public string PlayerId { get; set; }
|
||||
public DeltaFlags Flags { get; set; }
|
||||
public Vector3? Position { get; set; }
|
||||
public Quaternion? Rotation { get; set; }
|
||||
public int? Health { get; set; }
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum DeltaFlags
|
||||
{
|
||||
None = 0,
|
||||
Position = 1 << 0,
|
||||
Rotation = 1 << 1,
|
||||
Health = 1 << 2,
|
||||
Animation = 1 << 3
|
||||
}
|
||||
|
||||
// 增量复制系统
|
||||
public class DeltaReplicationSystem : AbstractSystem
|
||||
{
|
||||
private readonly Dictionary<uint, GameStateSnapshot> _snapshotHistory = new();
|
||||
private const int MaxHistorySize = 60; // 保留 60 个快照
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
this.RegisterEvent<ServerTickEvent>(OnServerTick);
|
||||
}
|
||||
|
||||
private void OnServerTick(ServerTickEvent e)
|
||||
{
|
||||
var currentSnapshot = CreateSnapshot();
|
||||
_snapshotHistory[e.Tick] = currentSnapshot;
|
||||
|
||||
// 清理旧快照
|
||||
CleanupOldSnapshots(e.Tick);
|
||||
|
||||
// 为每个客户端生成增量
|
||||
foreach (var connection in GetConnections())
|
||||
{
|
||||
var lastAckedTick = connection.LastAcknowledgedTick;
|
||||
|
||||
if (_snapshotHistory.TryGetValue(lastAckedTick, out var baseSnapshot))
|
||||
{
|
||||
var delta = CreateDelta(baseSnapshot, currentSnapshot);
|
||||
SendDeltaToClient(connection, delta);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 客户端太落后,发送完整快照
|
||||
SendSnapshotToClient(connection, currentSnapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DeltaState CreateDelta(GameStateSnapshot baseSnapshot, GameStateSnapshot currentSnapshot)
|
||||
{
|
||||
var delta = new DeltaState
|
||||
{
|
||||
Tick = currentSnapshot.Tick,
|
||||
BaseTick = baseSnapshot.Tick,
|
||||
PlayerDeltas = new List<PlayerDelta>()
|
||||
};
|
||||
|
||||
// 比较玩家状态
|
||||
foreach (var currentPlayer in currentSnapshot.Players)
|
||||
{
|
||||
var basePlayer = baseSnapshot.Players.FirstOrDefault(p => p.PlayerId == currentPlayer.PlayerId);
|
||||
|
||||
if (basePlayer.PlayerId == null)
|
||||
{
|
||||
// 新玩家,发送完整状态
|
||||
delta.PlayerDeltas.Add(CreateFullPlayerDelta(currentPlayer));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 计算差异
|
||||
var playerDelta = CreatePlayerDelta(basePlayer, currentPlayer);
|
||||
if (playerDelta.Flags != DeltaFlags.None)
|
||||
{
|
||||
delta.PlayerDeltas.Add(playerDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return delta;
|
||||
}
|
||||
|
||||
private PlayerDelta CreatePlayerDelta(PlayerState baseState, PlayerState currentState)
|
||||
{
|
||||
var delta = new PlayerDelta { PlayerId = currentState.PlayerId };
|
||||
|
||||
if (Vector3.Distance(baseState.Position, currentState.Position) > 0.01f)
|
||||
{
|
||||
delta.Flags |= DeltaFlags.Position;
|
||||
delta.Position = currentState.Position;
|
||||
}
|
||||
|
||||
if (Quaternion.Angle(baseState.Rotation, currentState.Rotation) > 0.1f)
|
||||
{
|
||||
delta.Flags |= DeltaFlags.Rotation;
|
||||
delta.Rotation = currentState.Rotation;
|
||||
}
|
||||
|
||||
if (baseState.Health != currentState.Health)
|
||||
{
|
||||
delta.Flags |= DeltaFlags.Health;
|
||||
delta.Health = currentState.Health;
|
||||
}
|
||||
|
||||
return delta;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 客户端预测与回滚
|
||||
|
||||
客户端立即响应玩家输入,然后在收到服务器确认后进行校正。
|
||||
|
||||
```csharp
|
||||
// 输入命令
|
||||
public struct PlayerInputCommand
|
||||
{
|
||||
public uint Tick { get; set; }
|
||||
public long Timestamp { get; set; }
|
||||
public Vector2 MoveDirection { get; set; }
|
||||
public Vector2 LookDirection { get; set; }
|
||||
public InputFlags Flags { get; set; }
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum InputFlags
|
||||
{
|
||||
None = 0,
|
||||
Jump = 1 << 0,
|
||||
Attack = 1 << 1,
|
||||
Interact = 1 << 2,
|
||||
Reload = 1 << 3
|
||||
}
|
||||
|
||||
// 客户端预测系统
|
||||
public class ClientPredictionSystem : AbstractSystem
|
||||
{
|
||||
private PredictionModel _prediction;
|
||||
private ClientGameStateModel _gameState;
|
||||
private readonly Queue<PlayerInputCommand> _pendingInputs = new();
|
||||
private uint _lastProcessedServerTick;
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_prediction = this.GetModel<PredictionModel>();
|
||||
_gameState = this.GetModel<ClientGameStateModel>();
|
||||
|
||||
this.RegisterEvent<PlayerInputEvent>(OnPlayerInput);
|
||||
this.RegisterEvent<ServerStateReceivedEvent>(OnServerStateReceived);
|
||||
}
|
||||
|
||||
private void OnPlayerInput(PlayerInputEvent e)
|
||||
{
|
||||
var input = new PlayerInputCommand
|
||||
{
|
||||
Tick = _prediction.CurrentTick,
|
||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
MoveDirection = e.MoveDirection,
|
||||
LookDirection = e.LookDirection,
|
||||
Flags = e.Flags
|
||||
};
|
||||
|
||||
// 保存输入用于重放
|
||||
_pendingInputs.Enqueue(input);
|
||||
|
||||
// 立即应用预测
|
||||
ApplyInput(input);
|
||||
|
||||
// 发送到服务器
|
||||
SendInputToServer(input);
|
||||
}
|
||||
|
||||
private void OnServerStateReceived(ServerStateReceivedEvent e)
|
||||
{
|
||||
_lastProcessedServerTick = e.Snapshot.Tick;
|
||||
|
||||
// 应用服务器状态
|
||||
ApplyServerState(e.Snapshot);
|
||||
|
||||
// 移除已确认的输入
|
||||
while (_pendingInputs.Count > 0 && _pendingInputs.Peek().Tick <= e.Snapshot.Tick)
|
||||
{
|
||||
_pendingInputs.Dequeue();
|
||||
}
|
||||
|
||||
// 重放未确认的输入
|
||||
ReplayPendingInputs();
|
||||
}
|
||||
|
||||
private void ApplyInput(PlayerInputCommand input)
|
||||
{
|
||||
var player = _gameState.LocalPlayer;
|
||||
|
||||
// 应用移动
|
||||
var movement = input.MoveDirection * player.Speed * Time.DeltaTime;
|
||||
player.Position += new Vector3(movement.X, 0, movement.Y);
|
||||
|
||||
// 应用旋转
|
||||
if (input.LookDirection != Vector2.Zero)
|
||||
{
|
||||
player.Rotation = Quaternion.LookRotation(
|
||||
new Vector3(input.LookDirection.X, 0, input.LookDirection.Y)
|
||||
);
|
||||
}
|
||||
|
||||
// 应用动作
|
||||
if ((input.Flags & InputFlags.Jump) != 0 && player.IsGrounded)
|
||||
{
|
||||
player.Velocity = new Vector3(player.Velocity.X, player.JumpForce, player.Velocity.Z);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReplayPendingInputs()
|
||||
{
|
||||
// 从服务器状态开始重放所有未确认的输入
|
||||
var savedState = SavePlayerState();
|
||||
|
||||
foreach (var input in _pendingInputs)
|
||||
{
|
||||
ApplyInput(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 1. 分离逻辑层
|
||||
|
||||
```csharp
|
||||
// 共享游戏逻辑 (客户端和服务器都使用)
|
||||
public class SharedGameLogic
|
||||
{
|
||||
public static void ProcessMovement(PlayerState player, Vector2 moveDirection, float deltaTime)
|
||||
{
|
||||
var movement = moveDirection.Normalized() * player.Speed * deltaTime;
|
||||
player.Position += new Vector3(movement.X, 0, movement.Y);
|
||||
}
|
||||
|
||||
public static bool CanJump(PlayerState player)
|
||||
{
|
||||
return player.IsGrounded && !player.IsStunned;
|
||||
}
|
||||
|
||||
public static int CalculateDamage(int attackPower, int defense, float criticalChance)
|
||||
{
|
||||
var baseDamage = Math.Max(1, attackPower - defense);
|
||||
var isCritical = Random.Shared.NextDouble() < criticalChance;
|
||||
return isCritical ? baseDamage * 2 : baseDamage;
|
||||
}
|
||||
}
|
||||
|
||||
// 服务器权威逻辑
|
||||
public class ServerGameLogicSystem : AbstractSystem
|
||||
{
|
||||
private ServerGameStateModel _gameState;
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_gameState = this.GetModel<ServerGameStateModel>();
|
||||
|
||||
this.RegisterEvent<PlayerInputReceivedEvent>(OnPlayerInputReceived);
|
||||
this.RegisterEvent<AttackRequestEvent>(OnAttackRequest);
|
||||
}
|
||||
|
||||
private void OnPlayerInputReceived(PlayerInputReceivedEvent e)
|
||||
{
|
||||
var player = _gameState.GetPlayer(e.PlayerId);
|
||||
|
||||
// 验证输入
|
||||
if (!ValidateInput(e.Input))
|
||||
{
|
||||
SendInputRejection(e.PlayerId, "Invalid input");
|
||||
return;
|
||||
}
|
||||
|
||||
// 应用共享逻辑
|
||||
SharedGameLogic.ProcessMovement(player, e.Input.MoveDirection, Time.DeltaTime);
|
||||
|
||||
// 服务器端验证
|
||||
if ((e.Input.Flags & InputFlags.Jump) != 0)
|
||||
{
|
||||
if (SharedGameLogic.CanJump(player))
|
||||
{
|
||||
player.Velocity = new Vector3(player.Velocity.X, player.JumpForce, player.Velocity.Z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAttackRequest(AttackRequestEvent e)
|
||||
{
|
||||
var attacker = _gameState.GetPlayer(e.AttackerId);
|
||||
var target = _gameState.GetPlayer(e.TargetId);
|
||||
|
||||
// 服务器端验证
|
||||
if (!CanAttack(attacker, target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算伤害
|
||||
var damage = SharedGameLogic.CalculateDamage(
|
||||
attacker.AttackPower,
|
||||
target.Defense,
|
||||
attacker.CriticalChance
|
||||
);
|
||||
|
||||
// 应用伤害
|
||||
target.Health = Math.Max(0, target.Health - damage);
|
||||
|
||||
// 广播事件
|
||||
this.SendEvent(new PlayerDamagedEvent
|
||||
{
|
||||
AttackerId = e.AttackerId,
|
||||
TargetId = e.TargetId,
|
||||
Damage = damage,
|
||||
RemainingHealth = target.Health
|
||||
});
|
||||
|
||||
if (target.Health == 0)
|
||||
{
|
||||
this.SendEvent(new PlayerDiedEvent
|
||||
{
|
||||
PlayerId = e.TargetId,
|
||||
KillerId = e.AttackerId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 客户端表现逻辑
|
||||
public class ClientPresentationSystem : AbstractSystem
|
||||
{
|
||||
protected override void OnInit()
|
||||
{
|
||||
this.RegisterEvent<PlayerDamagedEvent>(OnPlayerDamaged);
|
||||
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
|
||||
}
|
||||
|
||||
private void OnPlayerDamaged(PlayerDamagedEvent e)
|
||||
{
|
||||
// 播放受击特效
|
||||
PlayDamageEffect(e.TargetId, e.Damage);
|
||||
|
||||
// 播放受击音效
|
||||
PlayDamageSound(e.TargetId);
|
||||
|
||||
// 更新 UI
|
||||
UpdateHealthBar(e.TargetId, e.RemainingHealth);
|
||||
}
|
||||
|
||||
private void OnPlayerDied(PlayerDiedEvent e)
|
||||
{
|
||||
// 播放死亡动画
|
||||
PlayDeathAnimation(e.PlayerId);
|
||||
|
||||
// 播放死亡音效
|
||||
PlayDeathSound(e.PlayerId);
|
||||
|
||||
// 显示击杀提示
|
||||
ShowKillFeed(e.KillerId, e.PlayerId);
|
||||
}
|
||||
}
|
||||
1359
docs/zh-CN/best-practices/performance.md
Normal file
1359
docs/zh-CN/best-practices/performance.md
Normal file
File diff suppressed because it is too large
Load Diff
839
docs/zh-CN/contributing.md
Normal file
839
docs/zh-CN/contributing.md
Normal file
@ -0,0 +1,839 @@
|
||||
# 贡献指南
|
||||
|
||||
欢迎为 GFramework 贡献代码!本指南将帮助你了解如何参与项目开发。
|
||||
|
||||
## 概述
|
||||
|
||||
GFramework 是一个开源的游戏开发框架,我们欢迎所有形式的贡献:
|
||||
|
||||
- 报告 Bug 和提出功能建议
|
||||
- 提交代码修复和新功能
|
||||
- 改进文档和示例
|
||||
- 参与讨论和代码审查
|
||||
|
||||
## 行为准则
|
||||
|
||||
### 社区规范
|
||||
|
||||
我们致力于为所有贡献者提供友好、安全和包容的环境。参与本项目时,请遵守以下准则:
|
||||
|
||||
- **尊重他人**:尊重不同的观点和经验
|
||||
- **建设性沟通**:提供有建设性的反馈,避免人身攻击
|
||||
- **协作精神**:帮助新贡献者融入社区
|
||||
- **专业态度**:保持专业和礼貌的交流方式
|
||||
|
||||
### 不可接受的行为
|
||||
|
||||
- 使用性别化语言或图像
|
||||
- 人身攻击或侮辱性评论
|
||||
- 骚扰行为(公开或私下)
|
||||
- 未经许可发布他人的私人信息
|
||||
- 其他不道德或不专业的行为
|
||||
|
||||
## 如何贡献
|
||||
|
||||
### 报告问题
|
||||
|
||||
发现 Bug 或有功能建议时,请通过 GitHub Issues 提交:
|
||||
|
||||
1. **搜索现有 Issue**:避免重复提交
|
||||
2. **使用清晰的标题**:简洁描述问题
|
||||
3. **提供详细信息**:
|
||||
- Bug 报告:复现步骤、预期行为、实际行为、环境信息
|
||||
- 功能建议:使用场景、预期效果、可能的实现方案
|
||||
|
||||
**Bug 报告模板**:
|
||||
|
||||
```markdown
|
||||
**描述**
|
||||
简要描述 Bug
|
||||
|
||||
**复现步骤**
|
||||
1. 执行操作 A
|
||||
2. 执行操作 B
|
||||
3. 观察到错误
|
||||
|
||||
**预期行为**
|
||||
应该发生什么
|
||||
|
||||
**实际行为**
|
||||
实际发生了什么
|
||||
|
||||
**环境信息**
|
||||
- GFramework 版本:
|
||||
- .NET 版本:
|
||||
- 操作系统:
|
||||
- Godot 版本(如适用):
|
||||
|
||||
**附加信息**
|
||||
日志、截图等
|
||||
```
|
||||
|
||||
### 提交 Pull Request
|
||||
|
||||
#### 基本流程
|
||||
|
||||
1. **Fork 仓库**:在 GitHub 上 Fork 本项目
|
||||
2. **克隆到本地**:
|
||||
```bash
|
||||
git clone https://github.com/your-username/GFramework.git
|
||||
cd GFramework
|
||||
```
|
||||
3. **创建特性分支**:
|
||||
```bash
|
||||
git checkout -b feature/your-feature-name
|
||||
# 或
|
||||
git checkout -b fix/your-bug-fix
|
||||
```
|
||||
4. **进行开发**:编写代码、添加测试、更新文档
|
||||
5. **提交更改**:遵循提交规范(见下文)
|
||||
6. **推送分支**:
|
||||
```bash
|
||||
git push origin feature/your-feature-name
|
||||
```
|
||||
7. **创建 PR**:在 GitHub 上创建 Pull Request
|
||||
|
||||
#### PR 要求
|
||||
|
||||
- **清晰的标题**:简洁描述变更内容
|
||||
- **详细的描述**:
|
||||
- 变更的背景和动机
|
||||
- 实现方案说明
|
||||
- 测试验证结果
|
||||
- 相关 Issue 链接(如有)
|
||||
- **代码质量**:通过所有 CI 检查
|
||||
- **测试覆盖**:为新功能添加测试
|
||||
- **文档更新**:更新相关文档
|
||||
|
||||
### 改进文档
|
||||
|
||||
文档改进同样重要:
|
||||
|
||||
- **修正错误**:拼写、语法、技术错误
|
||||
- **补充示例**:添加代码示例和使用场景
|
||||
- **完善说明**:改进不清晰的描述
|
||||
- **翻译工作**:帮助翻译文档(如需要)
|
||||
|
||||
文档位于 `docs/` 目录,使用 Markdown 格式编写。
|
||||
|
||||
## 开发环境设置
|
||||
|
||||
### 前置要求
|
||||
|
||||
- **.NET SDK**:8.0、9.0 或 10.0
|
||||
- **Git**:版本控制工具
|
||||
- **IDE**(推荐):
|
||||
- Visual Studio 2022+
|
||||
- JetBrains Rider
|
||||
- Visual Studio Code + C# Dev Kit
|
||||
|
||||
### 克隆仓库
|
||||
|
||||
```bash
|
||||
# 克隆你 Fork 的仓库
|
||||
git clone https://github.com/your-username/GFramework.git
|
||||
cd GFramework
|
||||
|
||||
# 添加上游仓库
|
||||
git remote add upstream https://github.com/GeWuYou/GFramework.git
|
||||
```
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
# 恢复 NuGet 包
|
||||
dotnet restore
|
||||
|
||||
# 恢复 .NET 本地工具
|
||||
dotnet tool restore
|
||||
```
|
||||
|
||||
### 构建项目
|
||||
|
||||
```bash
|
||||
# 构建所有项目
|
||||
dotnet build
|
||||
|
||||
# 构建特定配置
|
||||
dotnet build -c Release
|
||||
```
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
dotnet test
|
||||
|
||||
# 运行特定测试项目
|
||||
dotnet test GFramework.Core.Tests
|
||||
dotnet test GFramework.SourceGenerators.Tests
|
||||
|
||||
# 生成测试覆盖率报告
|
||||
dotnet test --collect:"XPlat Code Coverage"
|
||||
```
|
||||
|
||||
### 验证代码质量
|
||||
|
||||
项目使用 MegaLinter 进行代码质量检查:
|
||||
|
||||
```bash
|
||||
# 本地运行 MegaLinter(需要 Docker)
|
||||
docker run --rm -v $(pwd):/tmp/lint oxsecurity/megalinter:v9
|
||||
|
||||
# 或使用 CI 流程验证
|
||||
git push origin your-branch
|
||||
```
|
||||
|
||||
## 代码规范
|
||||
|
||||
### 命名规范
|
||||
|
||||
遵循 C# 标准命名约定:
|
||||
|
||||
- **类、接口、方法**:PascalCase
|
||||
```csharp
|
||||
public class PlayerController { }
|
||||
public interface IEventBus { }
|
||||
public void ProcessInput() { }
|
||||
```
|
||||
|
||||
- **私有字段**:_camelCase(下划线前缀)
|
||||
```csharp
|
||||
private int _health;
|
||||
private readonly ILogger _logger;
|
||||
```
|
||||
|
||||
- **参数、局部变量**:camelCase
|
||||
```csharp
|
||||
public void SetHealth(int newHealth)
|
||||
{
|
||||
var oldHealth = _health;
|
||||
_health = newHealth;
|
||||
}
|
||||
```
|
||||
|
||||
- **常量**:PascalCase
|
||||
```csharp
|
||||
public const int MaxPlayers = 4;
|
||||
private const string DefaultName = "Player";
|
||||
```
|
||||
|
||||
- **接口**:I 前缀
|
||||
```csharp
|
||||
public interface IArchitecture { }
|
||||
public interface ICommand<TInput> { }
|
||||
```
|
||||
|
||||
### 代码风格
|
||||
|
||||
- **缩进**:4 个空格(不使用 Tab)
|
||||
- **大括号**:Allman 风格(独占一行)
|
||||
```csharp
|
||||
if (condition)
|
||||
{
|
||||
DoSomething();
|
||||
}
|
||||
```
|
||||
|
||||
- **using 指令**:文件顶部,按字母顺序排列
|
||||
```csharp
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GFramework.Core.Abstractions;
|
||||
```
|
||||
|
||||
- **空行**:
|
||||
- 命名空间后空一行
|
||||
- 类成员之间空一行
|
||||
- 逻辑块之间适当空行
|
||||
|
||||
- **行长度**:建议不超过 120 字符
|
||||
|
||||
### 注释规范
|
||||
|
||||
#### XML 文档注释
|
||||
|
||||
所有公共 API 必须包含 XML 文档注释:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 架构基类,提供系统、模型、工具等组件的注册与管理功能。
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">模型类型</typeparam>
|
||||
/// <param name="configuration">架构配置</param>
|
||||
/// <returns>注册的模型实例</returns>
|
||||
/// <exception cref="ArgumentNullException">当 model 为 null 时抛出</exception>
|
||||
public TModel RegisterModel<TModel>(TModel model) where TModel : IModel
|
||||
{
|
||||
// 实现代码
|
||||
}
|
||||
```
|
||||
|
||||
#### 代码注释
|
||||
|
||||
- **何时添加注释**:
|
||||
- 复杂的算法逻辑
|
||||
- 非显而易见的设计决策
|
||||
- 临时解决方案(使用 TODO 或 HACK 标记)
|
||||
- 性能关键代码的优化说明
|
||||
|
||||
- **注释风格**:
|
||||
```csharp
|
||||
// 单行注释使用双斜杠
|
||||
|
||||
// 多行注释可以使用多个单行注释
|
||||
// 每行都以双斜杠开始
|
||||
|
||||
/* 或使用块注释
|
||||
* 适用于较长的说明
|
||||
*/
|
||||
```
|
||||
|
||||
- **避免无用注释**:
|
||||
```csharp
|
||||
// 不好:注释重复代码内容
|
||||
// 设置健康值为 100
|
||||
health = 100;
|
||||
|
||||
// 好:解释为什么这样做
|
||||
// 初始化时设置满血,避免首次战斗时的边界情况
|
||||
health = MaxHealth;
|
||||
```
|
||||
|
||||
### 设计原则
|
||||
|
||||
- **SOLID 原则**:遵循面向对象设计原则
|
||||
- **依赖注入**:优先使用构造函数注入
|
||||
- **接口隔离**:定义小而专注的接口
|
||||
- **不可变性**:优先使用 `readonly` 和不可变类型
|
||||
- **异步编程**:I/O 操作使用 `async`/`await`
|
||||
|
||||
## 提交规范
|
||||
|
||||
### Commit 消息格式
|
||||
|
||||
使用 Conventional Commits 规范:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
#### Type(类型)
|
||||
|
||||
- **feat**:新功能
|
||||
- **fix**:Bug 修复
|
||||
- **docs**:文档更新
|
||||
- **style**:代码格式调整(不影响功能)
|
||||
- **refactor**:重构(不是新功能也不是修复)
|
||||
- **perf**:性能优化
|
||||
- **test**:添加或修改测试
|
||||
- **chore**:构建过程或辅助工具的变动
|
||||
- **ci**:CI 配置文件和脚本的变动
|
||||
|
||||
#### Scope(范围)
|
||||
|
||||
指明变更影响的模块:
|
||||
|
||||
- `core`:GFramework.Core
|
||||
- `game`:GFramework.Game
|
||||
- `godot`:GFramework.Godot
|
||||
- `generators`:源码生成器
|
||||
- `docs`:文档
|
||||
- `tests`:测试
|
||||
|
||||
#### Subject(主题)
|
||||
|
||||
- 使用祈使句,现在时态:"add" 而不是 "added" 或 "adds"
|
||||
- 首字母小写
|
||||
- 结尾不加句号
|
||||
- 限制在 50 字符以内
|
||||
|
||||
#### Body(正文)
|
||||
|
||||
- 详细描述变更的动机和实现细节
|
||||
- 与主题空一行
|
||||
- 每行不超过 72 字符
|
||||
|
||||
#### Footer(页脚)
|
||||
|
||||
- 关联 Issue:`Closes #123`
|
||||
- 破坏性变更:`BREAKING CHANGE: 描述`
|
||||
|
||||
#### 示例
|
||||
|
||||
```bash
|
||||
# 简单提交
|
||||
git commit -m "feat(core): add event priority support"
|
||||
|
||||
# 详细提交
|
||||
git commit -m "fix(godot): resolve scene loading race condition
|
||||
|
||||
修复了在快速切换场景时可能出现的资源加载竞态条件。
|
||||
通过引入场景加载锁机制,确保同一时间只有一个场景在加载。
|
||||
|
||||
Closes #456"
|
||||
|
||||
# 破坏性变更
|
||||
git commit -m "refactor(core): change IArchitecture interface
|
||||
|
||||
BREAKING CHANGE: IArchitecture.Init() 现在返回 Task 而不是 void。
|
||||
所有继承 Architecture 的类需要更新为异步初始化。
|
||||
|
||||
Migration guide: 将 Init() 改为 async Task Init()
|
||||
"
|
||||
```
|
||||
|
||||
### 分支策略
|
||||
|
||||
- **main**:主分支,保持稳定
|
||||
- **feature/***:新功能分支
|
||||
- `feature/event-priority`
|
||||
- `feature/godot-ui-system`
|
||||
- **fix/***:Bug 修复分支
|
||||
- `fix/memory-leak`
|
||||
- `fix/null-reference`
|
||||
- **docs/***:文档更新分支
|
||||
- `docs/api-reference`
|
||||
- `docs/tutorial-update`
|
||||
- **refactor/***:重构分支
|
||||
- `refactor/logging-system`
|
||||
|
||||
#### 分支命名规范
|
||||
|
||||
- 使用小写字母和连字符
|
||||
- 简洁描述分支目的
|
||||
- 避免使用个人名称
|
||||
|
||||
## 测试要求
|
||||
|
||||
### 单元测试
|
||||
|
||||
所有新功能和 Bug 修复都应包含单元测试:
|
||||
|
||||
```csharp
|
||||
using Xunit;
|
||||
|
||||
namespace GFramework.Core.Tests.events;
|
||||
|
||||
public class EventBusTests
|
||||
{
|
||||
[Fact]
|
||||
public void Subscribe_ShouldReceiveEvent()
|
||||
{
|
||||
// Arrange
|
||||
var eventBus = new EventBus();
|
||||
var received = false;
|
||||
|
||||
// Act
|
||||
eventBus.Subscribe<TestEvent>(e => received = true);
|
||||
eventBus.Publish(new TestEvent());
|
||||
|
||||
// Assert
|
||||
Assert.True(received);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(10)]
|
||||
[InlineData(100)]
|
||||
public void Subscribe_MultipleEvents_ShouldReceiveAll(int count)
|
||||
{
|
||||
// 测试实现
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 测试组织
|
||||
|
||||
- **测试项目**:`*.Tests` 后缀
|
||||
- **测试类**:`*Tests` 后缀,与被测试类对应
|
||||
- **测试方法**:`MethodName_Scenario_ExpectedResult` 格式
|
||||
- **测试数据**:使用 `[Theory]` 和 `[InlineData]` 进行参数化测试
|
||||
|
||||
### 测试覆盖率
|
||||
|
||||
- **目标**:新代码覆盖率 > 80%
|
||||
- **关键路径**:核心功能覆盖率 > 90%
|
||||
- **边界情况**:测试异常情况和边界值
|
||||
|
||||
### 集成测试
|
||||
|
||||
对于涉及多个组件交互的功能,添加集成测试:
|
||||
|
||||
```csharp
|
||||
public class ArchitectureIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Architecture_FullLifecycle_ShouldWork()
|
||||
{
|
||||
// Arrange
|
||||
var architecture = new TestArchitecture();
|
||||
|
||||
// Act
|
||||
await architecture.InitAsync();
|
||||
var result = architecture.GetModel<TestModel>();
|
||||
await architecture.DestroyAsync();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 性能测试
|
||||
|
||||
对性能敏感的代码,添加基准测试:
|
||||
|
||||
```csharp
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
[MemoryDiagnoser]
|
||||
public class EventBusBenchmarks
|
||||
{
|
||||
private EventBus _eventBus;
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
_eventBus = new EventBus();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void Publish_1000Events()
|
||||
{
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
_eventBus.Publish(new TestEvent());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 文档要求
|
||||
|
||||
### XML 注释
|
||||
|
||||
所有公共 API 必须包含完整的 XML 文档注释:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 事件总线接口,提供事件的发布和订阅功能。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 事件总线使用观察者模式实现,支持类型安全的事件分发。
|
||||
/// 所有订阅都是弱引用,避免内存泄漏。
|
||||
/// </remarks>
|
||||
public interface IEventBus
|
||||
{
|
||||
/// <summary>
|
||||
/// 订阅指定类型的事件。
|
||||
/// </summary>
|
||||
/// <typeparam name="TEvent">事件类型</typeparam>
|
||||
/// <param name="handler">事件处理器</param>
|
||||
/// <returns>取消订阅的句柄</returns>
|
||||
/// <exception cref="ArgumentNullException">当 handler 为 null 时抛出</exception>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var unregister = eventBus.Subscribe&lt;PlayerDiedEvent&gt;(e =&gt;
|
||||
/// {
|
||||
/// Console.WriteLine($"Player {e.PlayerId} died");
|
||||
/// });
|
||||
///
|
||||
/// // 取消订阅
|
||||
/// unregister.Dispose();
|
||||
/// </code>
|
||||
/// </example>
|
||||
IUnRegister Subscribe<TEvent>(Action<TEvent> handler) where TEvent : IEvent;
|
||||
}
|
||||
```
|
||||
|
||||
### Markdown 文档
|
||||
|
||||
#### 文档结构
|
||||
|
||||
```markdown
|
||||
# 标题
|
||||
|
||||
简要介绍模块功能和用途。
|
||||
|
||||
## 核心概念
|
||||
|
||||
解释关键概念和术语。
|
||||
|
||||
## 快速开始
|
||||
|
||||
提供最简单的使用示例。
|
||||
|
||||
## 详细用法
|
||||
|
||||
### 功能 A
|
||||
|
||||
详细说明和代码示例。
|
||||
|
||||
### 功能 B
|
||||
|
||||
详细说明和代码示例。
|
||||
|
||||
## 最佳实践
|
||||
|
||||
推荐的使用模式和注意事项。
|
||||
|
||||
## 常见问题
|
||||
|
||||
FAQ 列表。
|
||||
|
||||
## 相关资源
|
||||
|
||||
链接到相关文档和示例。
|
||||
```
|
||||
|
||||
#### 代码示例
|
||||
|
||||
- **完整性**:示例代码应该可以直接运行
|
||||
- **注释**:关键步骤添加注释说明
|
||||
- **格式化**:使用正确的语法高亮
|
||||
|
||||
```csharp
|
||||
// 创建架构实例
|
||||
var architecture = new GameArchitecture();
|
||||
|
||||
// 初始化架构
|
||||
await architecture.InitAsync();
|
||||
|
||||
// 注册模型
|
||||
var playerModel = architecture.GetModel<PlayerModel>();
|
||||
|
||||
// 发送命令
|
||||
await architecture.SendCommandAsync(new AttackCommand
|
||||
{
|
||||
TargetId = enemyId
|
||||
});
|
||||
```
|
||||
|
||||
#### 图表
|
||||
|
||||
使用 Mermaid 或 ASCII 图表说明复杂概念:
|
||||
|
||||
```markdown
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Controller] --> B[Command]
|
||||
B --> C[System]
|
||||
C --> D[Model]
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
## PR 流程
|
||||
|
||||
### 创建 PR
|
||||
|
||||
1. **确保分支最新**:
|
||||
```bash
|
||||
git fetch upstream
|
||||
git rebase upstream/main
|
||||
```
|
||||
|
||||
2. **推送到 Fork**:
|
||||
```bash
|
||||
git push origin feature/your-feature
|
||||
```
|
||||
|
||||
3. **创建 PR**:
|
||||
- 在 GitHub 上点击 "New Pull Request"
|
||||
- 选择 base: `main` ← compare: `your-branch`
|
||||
- 填写 PR 模板
|
||||
|
||||
### PR 模板
|
||||
|
||||
```markdown
|
||||
## 变更说明
|
||||
|
||||
简要描述本 PR 的变更内容。
|
||||
|
||||
## 变更类型
|
||||
|
||||
- [ ] Bug 修复
|
||||
- [ ] 新功能
|
||||
- [ ] 破坏性变更
|
||||
- [ ] 文档更新
|
||||
- [ ] 性能优化
|
||||
- [ ] 代码重构
|
||||
|
||||
## 相关 Issue
|
||||
|
||||
Closes #123
|
||||
|
||||
## 测试
|
||||
|
||||
描述如何测试这些变更:
|
||||
|
||||
- [ ] 添加了单元测试
|
||||
- [ ] 添加了集成测试
|
||||
- [ ] 手动测试通过
|
||||
|
||||
## 检查清单
|
||||
|
||||
- [ ] 代码遵循项目规范
|
||||
- [ ] 添加了必要的注释
|
||||
- [ ] 更新了相关文档
|
||||
- [ ] 所有测试通过
|
||||
- [ ] 没有引入新的警告
|
||||
|
||||
## 截图(如适用)
|
||||
|
||||
添加截图或 GIF 展示变更效果。
|
||||
|
||||
## 附加说明
|
||||
|
||||
其他需要说明的内容。
|
||||
```
|
||||
|
||||
### 代码审查
|
||||
|
||||
PR 提交后,维护者会进行代码审查:
|
||||
|
||||
- **响应反馈**:及时回复审查意见
|
||||
- **修改代码**:根据建议进行调整
|
||||
- **讨论方案**:对有争议的地方进行讨论
|
||||
- **保持耐心**:审查可能需要时间
|
||||
|
||||
#### 审查关注点
|
||||
|
||||
- **功能正确性**:代码是否实现了预期功能
|
||||
- **代码质量**:是否遵循项目规范
|
||||
- **测试覆盖**:是否有足够的测试
|
||||
- **性能影响**:是否有性能问题
|
||||
- **向后兼容**:是否破坏现有 API
|
||||
|
||||
### 合并流程
|
||||
|
||||
1. **通过 CI 检查**:所有自动化测试通过
|
||||
2. **代码审查通过**:至少一位维护者批准
|
||||
3. **解决冲突**:如有冲突需先解决
|
||||
4. **合并方式**:
|
||||
- 功能分支:Squash and merge
|
||||
- 修复分支:Merge commit
|
||||
- 文档更新:Squash and merge
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 如何同步上游更新?
|
||||
|
||||
```bash
|
||||
# 获取上游更新
|
||||
git fetch upstream
|
||||
|
||||
# 合并到本地 main
|
||||
git checkout main
|
||||
git merge upstream/main
|
||||
|
||||
# 更新你的 Fork
|
||||
git push origin main
|
||||
|
||||
# 更新特性分支
|
||||
git checkout feature/your-feature
|
||||
git rebase main
|
||||
```
|
||||
|
||||
### 如何解决合并冲突?
|
||||
|
||||
```bash
|
||||
# 拉取最新代码
|
||||
git fetch upstream
|
||||
git rebase upstream/main
|
||||
|
||||
# 如果有冲突,手动解决后
|
||||
git add .
|
||||
git rebase --continue
|
||||
|
||||
# 强制推送(因为 rebase 改变了历史)
|
||||
git push origin feature/your-feature --force-with-lease
|
||||
```
|
||||
|
||||
### 提交了错误的代码怎么办?
|
||||
|
||||
```bash
|
||||
# 修改最后一次提交
|
||||
git add .
|
||||
git commit --amend
|
||||
|
||||
# 或者撤销最后一次提交
|
||||
git reset --soft HEAD~1
|
||||
# 修改后重新提交
|
||||
git add .
|
||||
git commit -m "fix: correct implementation"
|
||||
```
|
||||
|
||||
### 如何运行特定的测试?
|
||||
|
||||
```bash
|
||||
# 运行单个测试类
|
||||
dotnet test --filter "FullyQualifiedName~EventBusTests"
|
||||
|
||||
# 运行单个测试方法
|
||||
dotnet test --filter "FullyQualifiedName~EventBusTests.Subscribe_ShouldReceiveEvent"
|
||||
|
||||
# 运行特定类别的测试
|
||||
dotnet test --filter "Category=Integration"
|
||||
```
|
||||
|
||||
### 如何生成文档?
|
||||
|
||||
```bash
|
||||
# 安装 VitePress(如果还没安装)
|
||||
cd docs
|
||||
npm install
|
||||
|
||||
# 本地预览文档
|
||||
npm run docs:dev
|
||||
|
||||
# 构建文档
|
||||
npm run docs:build
|
||||
```
|
||||
|
||||
### 代码审查需要多长时间?
|
||||
|
||||
- **简单修复**:通常 1-3 天
|
||||
- **新功能**:可能需要 1-2 周
|
||||
- **大型重构**:可能需要更长时间
|
||||
|
||||
请耐心等待,维护者会尽快审查。
|
||||
|
||||
### 我的 PR 被拒绝了怎么办?
|
||||
|
||||
不要气馁!被拒绝的原因可能是:
|
||||
|
||||
- 不符合项目方向
|
||||
- 需要更多讨论
|
||||
- 实现方式需要调整
|
||||
|
||||
你可以:
|
||||
|
||||
- 在 Issue 中讨论方案
|
||||
- 根据反馈调整实现
|
||||
- 寻求维护者的建议
|
||||
|
||||
### 如何成为维护者?
|
||||
|
||||
持续贡献高质量的代码和文档,积极参与社区讨论,帮助其他贡献者。维护者会邀请活跃且负责任的贡献者加入维护团队。
|
||||
|
||||
## 获取帮助
|
||||
|
||||
如果你在贡献过程中遇到问题:
|
||||
|
||||
- **GitHub Issues**:提问或报告问题
|
||||
- **GitHub Discussions**:参与讨论
|
||||
- **代码注释**:查看现有代码的注释和文档
|
||||
|
||||
## 致谢
|
||||
|
||||
感谢所有为 GFramework 做出贡献的开发者!你们的努力让这个项目变得更好。
|
||||
|
||||
## 许可证
|
||||
|
||||
通过向本项目提交代码,你同意你的贡献将在 Apache License 2.0 下发布。
|
||||
938
docs/zh-CN/core/configuration.md
Normal file
938
docs/zh-CN/core/configuration.md
Normal file
@ -0,0 +1,938 @@
|
||||
# Configuration 包使用说明
|
||||
|
||||
## 概述
|
||||
|
||||
Configuration 包提供了线程安全的配置管理系统,支持类型安全的配置存储、访问、监听和持久化。配置管理器可以用于管理游戏设置、运行时参数、开发配置等各种键值对数据。
|
||||
|
||||
配置系统是 GFramework 架构中的实用工具(Utility),可以在架构的任何层级中使用,提供统一的配置管理能力。
|
||||
|
||||
## 核心接口
|
||||
|
||||
### IConfigurationManager
|
||||
|
||||
配置管理器接口,提供类型安全的配置存储和访问。所有方法都是线程安全的。
|
||||
|
||||
**核心方法:**
|
||||
|
||||
```csharp
|
||||
// 配置访问
|
||||
T? GetConfig<T>(string key); // 获取配置值
|
||||
T GetConfig<T>(string key, T defaultValue); // 获取配置值(带默认值)
|
||||
void SetConfig<T>(string key, T value); // 设置配置值
|
||||
bool HasConfig(string key); // 检查配置是否存在
|
||||
bool RemoveConfig(string key); // 移除配置
|
||||
void Clear(); // 清空所有配置
|
||||
|
||||
// 配置监听
|
||||
IUnRegister WatchConfig<T>(string key, Action<T> onChange); // 监听配置变化
|
||||
|
||||
// 持久化
|
||||
void LoadFromJson(string json); // 从 JSON 加载
|
||||
string SaveToJson(); // 保存为 JSON
|
||||
void LoadFromFile(string path); // 从文件加载
|
||||
void SaveToFile(string path); // 保存到文件
|
||||
|
||||
// 工具方法
|
||||
int Count { get; } // 获取配置数量
|
||||
IEnumerable<string> GetAllKeys(); // 获取所有配置键
|
||||
```
|
||||
|
||||
## 核心类
|
||||
|
||||
### ConfigurationManager
|
||||
|
||||
配置管理器实现,提供线程安全的配置存储和访问。
|
||||
|
||||
**特性:**
|
||||
|
||||
- 线程安全:所有公共方法都是线程安全的
|
||||
- 类型安全:支持泛型类型的配置值
|
||||
- 自动类型转换:支持基本类型的自动转换
|
||||
- 配置监听:支持监听配置变化并触发回调
|
||||
- JSON 持久化:支持 JSON 格式的配置加载和保存
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```csharp
|
||||
// 创建配置管理器
|
||||
var configManager = new ConfigurationManager();
|
||||
|
||||
// 设置配置
|
||||
configManager.SetConfig("game.difficulty", "Normal");
|
||||
configManager.SetConfig("audio.volume", 0.8f);
|
||||
configManager.SetConfig("graphics.quality", 2);
|
||||
|
||||
// 获取配置
|
||||
var difficulty = configManager.GetConfig<string>("game.difficulty");
|
||||
var volume = configManager.GetConfig<float>("audio.volume");
|
||||
var quality = configManager.GetConfig<int>("graphics.quality");
|
||||
|
||||
// 使用默认值
|
||||
var fov = configManager.GetConfig("graphics.fov", 90);
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 1. 设置和获取配置
|
||||
|
||||
```csharp
|
||||
public class GameSettings : IUtility
|
||||
{
|
||||
private readonly IConfigurationManager _config = new ConfigurationManager();
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
// 设置游戏配置
|
||||
_config.SetConfig("player.name", "Player1");
|
||||
_config.SetConfig("player.level", 1);
|
||||
_config.SetConfig("player.experience", 0);
|
||||
|
||||
// 设置游戏选项
|
||||
_config.SetConfig("options.showTutorial", true);
|
||||
_config.SetConfig("options.language", "zh-CN");
|
||||
}
|
||||
|
||||
public string GetPlayerName()
|
||||
{
|
||||
return _config.GetConfig<string>("player.name") ?? "Unknown";
|
||||
}
|
||||
|
||||
public int GetPlayerLevel()
|
||||
{
|
||||
return _config.GetConfig("player.level", 1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 检查和移除配置
|
||||
|
||||
```csharp
|
||||
public class ConfigurationService
|
||||
{
|
||||
private readonly IConfigurationManager _config;
|
||||
|
||||
public ConfigurationService(IConfigurationManager config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public void ResetPlayerData()
|
||||
{
|
||||
// 检查配置是否存在
|
||||
if (_config.HasConfig("player.name"))
|
||||
{
|
||||
_config.RemoveConfig("player.name");
|
||||
}
|
||||
|
||||
if (_config.HasConfig("player.level"))
|
||||
{
|
||||
_config.RemoveConfig("player.level");
|
||||
}
|
||||
|
||||
// 或者清空所有配置
|
||||
// _config.Clear();
|
||||
}
|
||||
|
||||
public void PrintAllConfigs()
|
||||
{
|
||||
Console.WriteLine($"Total configs: {_config.Count}");
|
||||
|
||||
foreach (var key in _config.GetAllKeys())
|
||||
{
|
||||
Console.WriteLine($"Key: {key}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 支持的数据类型
|
||||
|
||||
```csharp
|
||||
public class TypeExamples
|
||||
{
|
||||
private readonly IConfigurationManager _config = new ConfigurationManager();
|
||||
|
||||
public void SetupConfigs()
|
||||
{
|
||||
// 基本类型
|
||||
_config.SetConfig("int.value", 42);
|
||||
_config.SetConfig("float.value", 3.14f);
|
||||
_config.SetConfig("double.value", 2.718);
|
||||
_config.SetConfig("bool.value", true);
|
||||
_config.SetConfig("string.value", "Hello");
|
||||
|
||||
// 复杂类型
|
||||
_config.SetConfig("vector.position", new Vector3(1, 2, 3));
|
||||
_config.SetConfig("list.items", new List<string> { "A", "B", "C" });
|
||||
_config.SetConfig("dict.data", new Dictionary<string, int>
|
||||
{
|
||||
["key1"] = 1,
|
||||
["key2"] = 2
|
||||
});
|
||||
}
|
||||
|
||||
public void GetConfigs()
|
||||
{
|
||||
var intValue = _config.GetConfig<int>("int.value");
|
||||
var floatValue = _config.GetConfig<float>("float.value");
|
||||
var boolValue = _config.GetConfig<bool>("bool.value");
|
||||
var stringValue = _config.GetConfig<string>("string.value");
|
||||
|
||||
var position = _config.GetConfig<Vector3>("vector.position");
|
||||
var items = _config.GetConfig<List<string>>("list.items");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 1. 配置监听(热更新)
|
||||
|
||||
配置监听允许在配置值变化时自动触发回调,实现配置的热更新。
|
||||
|
||||
```csharp
|
||||
public class AudioManager : AbstractSystem
|
||||
{
|
||||
private IUnRegister _volumeWatcher;
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
var config = this.GetUtility<IConfigurationManager>();
|
||||
|
||||
// 监听音量配置变化
|
||||
_volumeWatcher = config.WatchConfig<float>("audio.masterVolume", newVolume =>
|
||||
{
|
||||
UpdateMasterVolume(newVolume);
|
||||
this.GetUtility<ILogger>()?.Info($"Master volume changed to: {newVolume}");
|
||||
});
|
||||
|
||||
// 监听音效开关
|
||||
config.WatchConfig<bool>("audio.sfxEnabled", enabled =>
|
||||
{
|
||||
if (enabled)
|
||||
EnableSoundEffects();
|
||||
else
|
||||
DisableSoundEffects();
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateMasterVolume(float volume)
|
||||
{
|
||||
// 更新音频引擎的主音量
|
||||
AudioEngine.SetMasterVolume(volume);
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
// 取消监听
|
||||
_volumeWatcher?.UnRegister();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 多个监听器
|
||||
|
||||
```csharp
|
||||
public class GraphicsManager : AbstractSystem
|
||||
{
|
||||
protected override void OnInit()
|
||||
{
|
||||
var config = this.GetUtility<IConfigurationManager>();
|
||||
|
||||
// 多个组件监听同一个配置
|
||||
config.WatchConfig<int>("graphics.quality", quality =>
|
||||
{
|
||||
UpdateTextureQuality(quality);
|
||||
});
|
||||
|
||||
config.WatchConfig<int>("graphics.quality", quality =>
|
||||
{
|
||||
UpdateShadowQuality(quality);
|
||||
});
|
||||
|
||||
config.WatchConfig<int>("graphics.quality", quality =>
|
||||
{
|
||||
UpdatePostProcessing(quality);
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateTextureQuality(int quality) { }
|
||||
private void UpdateShadowQuality(int quality) { }
|
||||
private void UpdatePostProcessing(int quality) { }
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 配置持久化
|
||||
|
||||
#### 保存和加载 JSON
|
||||
|
||||
```csharp
|
||||
public class ConfigurationPersistence
|
||||
{
|
||||
private readonly IConfigurationManager _config;
|
||||
private readonly string _configPath = "config/game_settings.json";
|
||||
|
||||
public ConfigurationPersistence(IConfigurationManager config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public void SaveConfiguration()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 保存到文件
|
||||
_config.SaveToFile(_configPath);
|
||||
Console.WriteLine($"Configuration saved to {_configPath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to save configuration: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadConfiguration()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 从文件加载
|
||||
if (File.Exists(_configPath))
|
||||
{
|
||||
_config.LoadFromFile(_configPath);
|
||||
Console.WriteLine($"Configuration loaded from {_configPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Configuration file not found, using defaults");
|
||||
SetDefaultConfiguration();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to load configuration: {ex.Message}");
|
||||
SetDefaultConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetDefaultConfiguration()
|
||||
{
|
||||
_config.SetConfig("audio.masterVolume", 1.0f);
|
||||
_config.SetConfig("audio.musicVolume", 0.8f);
|
||||
_config.SetConfig("audio.sfxVolume", 1.0f);
|
||||
_config.SetConfig("graphics.quality", 2);
|
||||
_config.SetConfig("graphics.fullscreen", true);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### JSON 字符串操作
|
||||
|
||||
```csharp
|
||||
public class ConfigurationExport
|
||||
{
|
||||
private readonly IConfigurationManager _config;
|
||||
|
||||
public ConfigurationExport(IConfigurationManager config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public string ExportToJson()
|
||||
{
|
||||
// 导出为 JSON 字符串
|
||||
return _config.SaveToJson();
|
||||
}
|
||||
|
||||
public void ImportFromJson(string json)
|
||||
{
|
||||
// 从 JSON 字符串导入
|
||||
_config.LoadFromJson(json);
|
||||
}
|
||||
|
||||
public void ShareConfiguration()
|
||||
{
|
||||
// 导出配置用于分享
|
||||
var json = _config.SaveToJson();
|
||||
|
||||
// 可以通过网络发送、保存到剪贴板等
|
||||
Clipboard.SetText(json);
|
||||
Console.WriteLine("Configuration copied to clipboard");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 多环境配置
|
||||
|
||||
```csharp
|
||||
public class EnvironmentConfiguration
|
||||
{
|
||||
private readonly IConfigurationManager _config;
|
||||
private readonly string _environment;
|
||||
|
||||
public EnvironmentConfiguration(IConfigurationManager config, string environment)
|
||||
{
|
||||
_config = config;
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
public void LoadEnvironmentConfig()
|
||||
{
|
||||
// 根据环境加载不同的配置文件
|
||||
var configPath = _environment switch
|
||||
{
|
||||
"development" => "config/dev.json",
|
||||
"staging" => "config/staging.json",
|
||||
"production" => "config/prod.json",
|
||||
_ => "config/default.json"
|
||||
};
|
||||
|
||||
if (File.Exists(configPath))
|
||||
{
|
||||
_config.LoadFromFile(configPath);
|
||||
Console.WriteLine($"Loaded {_environment} configuration");
|
||||
}
|
||||
|
||||
// 设置环境特定的配置
|
||||
ApplyEnvironmentOverrides();
|
||||
}
|
||||
|
||||
private void ApplyEnvironmentOverrides()
|
||||
{
|
||||
switch (_environment)
|
||||
{
|
||||
case "development":
|
||||
_config.SetConfig("debug.enabled", true);
|
||||
_config.SetConfig("logging.level", "Debug");
|
||||
_config.SetConfig("api.endpoint", "http://localhost:3000");
|
||||
break;
|
||||
|
||||
case "production":
|
||||
_config.SetConfig("debug.enabled", false);
|
||||
_config.SetConfig("logging.level", "Warning");
|
||||
_config.SetConfig("api.endpoint", "https://api.production.com");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 配置验证
|
||||
|
||||
```csharp
|
||||
public class ConfigurationValidator
|
||||
{
|
||||
private readonly IConfigurationManager _config;
|
||||
|
||||
public ConfigurationValidator(IConfigurationManager config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public bool ValidateConfiguration()
|
||||
{
|
||||
var isValid = true;
|
||||
|
||||
// 验证必需的配置项
|
||||
if (!_config.HasConfig("game.version"))
|
||||
{
|
||||
Console.WriteLine("Error: game.version is required");
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 验证配置值范围
|
||||
var volume = _config.GetConfig("audio.masterVolume", -1f);
|
||||
if (volume < 0f || volume > 1f)
|
||||
{
|
||||
Console.WriteLine("Error: audio.masterVolume must be between 0 and 1");
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 验证配置类型
|
||||
try
|
||||
{
|
||||
var quality = _config.GetConfig<int>("graphics.quality");
|
||||
if (quality < 0 || quality > 3)
|
||||
{
|
||||
Console.WriteLine("Error: graphics.quality must be between 0 and 3");
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Error: graphics.quality must be an integer");
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
public void ApplyConstraints()
|
||||
{
|
||||
// 自动修正超出范围的值
|
||||
var volume = _config.GetConfig("audio.masterVolume", 1f);
|
||||
if (volume < 0f) _config.SetConfig("audio.masterVolume", 0f);
|
||||
if (volume > 1f) _config.SetConfig("audio.masterVolume", 1f);
|
||||
|
||||
var quality = _config.GetConfig("graphics.quality", 2);
|
||||
if (quality < 0) _config.SetConfig("graphics.quality", 0);
|
||||
if (quality > 3) _config.SetConfig("graphics.quality", 3);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 配置分组管理
|
||||
|
||||
```csharp
|
||||
public class ConfigurationGroups
|
||||
{
|
||||
private readonly IConfigurationManager _config;
|
||||
|
||||
public ConfigurationGroups(IConfigurationManager config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
// 音频配置组
|
||||
public class AudioConfig
|
||||
{
|
||||
public float MasterVolume { get; set; } = 1.0f;
|
||||
public float MusicVolume { get; set; } = 0.8f;
|
||||
public float SfxVolume { get; set; } = 1.0f;
|
||||
public bool Muted { get; set; } = false;
|
||||
}
|
||||
|
||||
// 图形配置组
|
||||
public class GraphicsConfig
|
||||
{
|
||||
public int Quality { get; set; } = 2;
|
||||
public bool Fullscreen { get; set; } = true;
|
||||
public int ResolutionWidth { get; set; } = 1920;
|
||||
public int ResolutionHeight { get; set; } = 1080;
|
||||
public bool VSync { get; set; } = true;
|
||||
}
|
||||
|
||||
public void SaveAudioConfig(AudioConfig audio)
|
||||
{
|
||||
_config.SetConfig("audio.masterVolume", audio.MasterVolume);
|
||||
_config.SetConfig("audio.musicVolume", audio.MusicVolume);
|
||||
_config.SetConfig("audio.sfxVolume", audio.SfxVolume);
|
||||
_config.SetConfig("audio.muted", audio.Muted);
|
||||
}
|
||||
|
||||
public AudioConfig LoadAudioConfig()
|
||||
{
|
||||
return new AudioConfig
|
||||
{
|
||||
MasterVolume = _config.GetConfig("audio.masterVolume", 1.0f),
|
||||
MusicVolume = _config.GetConfig("audio.musicVolume", 0.8f),
|
||||
SfxVolume = _config.GetConfig("audio.sfxVolume", 1.0f),
|
||||
Muted = _config.GetConfig("audio.muted", false)
|
||||
};
|
||||
}
|
||||
|
||||
public void SaveGraphicsConfig(GraphicsConfig graphics)
|
||||
{
|
||||
_config.SetConfig("graphics.quality", graphics.Quality);
|
||||
_config.SetConfig("graphics.fullscreen", graphics.Fullscreen);
|
||||
_config.SetConfig("graphics.resolutionWidth", graphics.ResolutionWidth);
|
||||
_config.SetConfig("graphics.resolutionHeight", graphics.ResolutionHeight);
|
||||
_config.SetConfig("graphics.vsync", graphics.VSync);
|
||||
}
|
||||
|
||||
public GraphicsConfig LoadGraphicsConfig()
|
||||
{
|
||||
return new GraphicsConfig
|
||||
{
|
||||
Quality = _config.GetConfig("graphics.quality", 2),
|
||||
Fullscreen = _config.GetConfig("graphics.fullscreen", true),
|
||||
ResolutionWidth = _config.GetConfig("graphics.resolutionWidth", 1920),
|
||||
ResolutionHeight = _config.GetConfig("graphics.resolutionHeight", 1080),
|
||||
VSync = _config.GetConfig("graphics.vsync", true)
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 在架构中使用
|
||||
|
||||
### 注册为 Utility
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : Architecture<GameArchitecture>
|
||||
{
|
||||
protected override void Init()
|
||||
{
|
||||
// 注册配置管理器
|
||||
this.RegisterUtility<IConfigurationManager>(new ConfigurationManager());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在 System 中使用
|
||||
|
||||
```csharp
|
||||
public class SettingsSystem : AbstractSystem
|
||||
{
|
||||
private IConfigurationManager _config;
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_config = this.GetUtility<IConfigurationManager>();
|
||||
|
||||
// 加载配置
|
||||
LoadSettings();
|
||||
|
||||
// 监听配置变化
|
||||
_config.WatchConfig<string>("game.language", OnLanguageChanged);
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
_config.LoadFromFile("settings.json");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 使用默认设置
|
||||
SetDefaultSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetDefaultSettings()
|
||||
{
|
||||
_config.SetConfig("game.language", "en-US");
|
||||
_config.SetConfig("game.difficulty", "Normal");
|
||||
_config.SetConfig("audio.masterVolume", 1.0f);
|
||||
}
|
||||
|
||||
private void OnLanguageChanged(string newLanguage)
|
||||
{
|
||||
// 切换游戏语言
|
||||
LocalizationManager.SetLanguage(newLanguage);
|
||||
}
|
||||
|
||||
public void SaveSettings()
|
||||
{
|
||||
_config.SaveToFile("settings.json");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在 Controller 中使用
|
||||
|
||||
```csharp
|
||||
public class SettingsController : IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public void ApplyGraphicsSettings(int quality, bool fullscreen)
|
||||
{
|
||||
var config = this.GetUtility<IConfigurationManager>();
|
||||
|
||||
// 更新配置(会自动触发监听器)
|
||||
config.SetConfig("graphics.quality", quality);
|
||||
config.SetConfig("graphics.fullscreen", fullscreen);
|
||||
|
||||
// 保存配置
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
public void ResetToDefaults()
|
||||
{
|
||||
var config = this.GetUtility<IConfigurationManager>();
|
||||
|
||||
// 清空所有配置
|
||||
config.Clear();
|
||||
|
||||
// 重新设置默认值
|
||||
config.SetConfig("audio.masterVolume", 1.0f);
|
||||
config.SetConfig("graphics.quality", 2);
|
||||
config.SetConfig("game.language", "en-US");
|
||||
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
private void SaveSettings()
|
||||
{
|
||||
var config = this.GetUtility<IConfigurationManager>();
|
||||
config.SaveToFile("settings.json");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 配置键命名规范
|
||||
|
||||
使用分层的点号命名法,便于组织和管理:
|
||||
|
||||
```csharp
|
||||
// 推荐的命名方式
|
||||
_config.SetConfig("audio.master.volume", 1.0f);
|
||||
_config.SetConfig("audio.music.volume", 0.8f);
|
||||
_config.SetConfig("graphics.quality.level", 2);
|
||||
_config.SetConfig("graphics.resolution.width", 1920);
|
||||
_config.SetConfig("player.stats.health", 100);
|
||||
_config.SetConfig("player.stats.mana", 50);
|
||||
|
||||
// 避免的命名方式
|
||||
_config.SetConfig("AudioMasterVolume", 1.0f); // 不使用驼峰命名
|
||||
_config.SetConfig("vol", 1.0f); // 不使用缩写
|
||||
_config.SetConfig("config_1", 1.0f); // 不使用无意义的名称
|
||||
```
|
||||
|
||||
### 2. 使用默认值
|
||||
|
||||
始终为 `GetConfig` 提供合理的默认值,避免空引用:
|
||||
|
||||
```csharp
|
||||
// 推荐
|
||||
var volume = _config.GetConfig("audio.volume", 1.0f);
|
||||
var quality = _config.GetConfig("graphics.quality", 2);
|
||||
|
||||
// 不推荐
|
||||
var volume = _config.GetConfig<float>("audio.volume"); // 可能返回 0
|
||||
if (volume == 0) volume = 1.0f; // 需要额外的检查
|
||||
```
|
||||
|
||||
### 3. 配置文件组织
|
||||
|
||||
将配置文件按环境和用途分类:
|
||||
|
||||
```
|
||||
config/
|
||||
├── default.json # 默认配置
|
||||
├── dev.json # 开发环境配置
|
||||
├── staging.json # 测试环境配置
|
||||
├── prod.json # 生产环境配置
|
||||
└── user/
|
||||
├── settings.json # 用户设置
|
||||
└── keybindings.json # 键位绑定
|
||||
```
|
||||
|
||||
### 4. 配置安全
|
||||
|
||||
不要在配置中存储敏感信息:
|
||||
|
||||
```csharp
|
||||
// 不要这样做
|
||||
_config.SetConfig("api.key", "secret_key_12345");
|
||||
_config.SetConfig("user.password", "password123");
|
||||
|
||||
// 应该使用专门的安全存储
|
||||
SecureStorage.SetSecret("api.key", "secret_key_12345");
|
||||
```
|
||||
|
||||
### 5. 监听器管理
|
||||
|
||||
及时注销不再需要的监听器,避免内存泄漏:
|
||||
|
||||
```csharp
|
||||
public class MySystem : AbstractSystem
|
||||
{
|
||||
private readonly List<IUnRegister> _watchers = new();
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
var config = this.GetUtility<IConfigurationManager>();
|
||||
|
||||
// 保存监听器引用
|
||||
_watchers.Add(config.WatchConfig<float>("audio.volume", OnVolumeChanged));
|
||||
_watchers.Add(config.WatchConfig<int>("graphics.quality", OnQualityChanged));
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
// 注销所有监听器
|
||||
foreach (var watcher in _watchers)
|
||||
{
|
||||
watcher.UnRegister();
|
||||
}
|
||||
_watchers.Clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 线程安全使用
|
||||
|
||||
虽然 `ConfigurationManager` 是线程安全的,但在多线程环境中仍需注意:
|
||||
|
||||
```csharp
|
||||
public class ThreadSafeConfigAccess
|
||||
{
|
||||
private readonly IConfigurationManager _config;
|
||||
|
||||
public void UpdateFromMultipleThreads()
|
||||
{
|
||||
// 可以安全地从多个线程访问
|
||||
Parallel.For(0, 10, i =>
|
||||
{
|
||||
_config.SetConfig($"thread.{i}.value", i);
|
||||
var value = _config.GetConfig($"thread.{i}.value", 0);
|
||||
});
|
||||
}
|
||||
|
||||
public void WatchFromMultipleThreads()
|
||||
{
|
||||
// 监听器回调可能在不同线程执行
|
||||
_config.WatchConfig<int>("shared.value", newValue =>
|
||||
{
|
||||
// 确保线程安全的操作
|
||||
lock (_lockObject)
|
||||
{
|
||||
UpdateSharedResource(newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private readonly object _lockObject = new();
|
||||
private void UpdateSharedResource(int value) { }
|
||||
}
|
||||
```
|
||||
|
||||
### 7. 配置变更通知
|
||||
|
||||
避免在配置监听器中触发大量的配置变更,可能导致循环调用:
|
||||
|
||||
```csharp
|
||||
// 不推荐:可能导致无限循环
|
||||
_config.WatchConfig<int>("value.a", a =>
|
||||
{
|
||||
_config.SetConfig("value.b", a + 1); // 触发 b 的监听器
|
||||
});
|
||||
|
||||
_config.WatchConfig<int>("value.b", b =>
|
||||
{
|
||||
_config.SetConfig("value.a", b + 1); // 触发 a 的监听器
|
||||
});
|
||||
|
||||
// 推荐:使用标志位避免循环
|
||||
private bool _isUpdating = false;
|
||||
|
||||
_config.WatchConfig<int>("value.a", a =>
|
||||
{
|
||||
if (_isUpdating) return;
|
||||
_isUpdating = true;
|
||||
_config.SetConfig("value.b", a + 1);
|
||||
_isUpdating = false;
|
||||
});
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 配置值类型转换失败怎么办?
|
||||
|
||||
A: `ConfigurationManager` 会尝试自动转换类型,如果失败会返回默认值。建议使用带默认值的 `GetConfig` 方法:
|
||||
|
||||
```csharp
|
||||
// 如果转换失败,返回默认值 1.0f
|
||||
var volume = _config.GetConfig("audio.volume", 1.0f);
|
||||
```
|
||||
|
||||
### Q2: 如何处理配置文件不存在的情况?
|
||||
|
||||
A: 使用 try-catch 捕获异常,并提供默认配置:
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
_config.LoadFromFile("settings.json");
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// 使用默认配置
|
||||
SetDefaultConfiguration();
|
||||
// 保存默认配置
|
||||
_config.SaveToFile("settings.json");
|
||||
}
|
||||
```
|
||||
|
||||
### Q3: 配置监听器何时被触发?
|
||||
|
||||
A: 只有当配置值真正发生变化时才会触发监听器。如果设置相同的值,监听器不会被触发:
|
||||
|
||||
```csharp
|
||||
_config.SetConfig("key", 42);
|
||||
|
||||
_config.WatchConfig<int>("key", value =>
|
||||
{
|
||||
Console.WriteLine($"Changed to: {value}");
|
||||
});
|
||||
|
||||
_config.SetConfig("key", 42); // 不会触发(值未变化)
|
||||
_config.SetConfig("key", 100); // 会触发
|
||||
```
|
||||
|
||||
### Q4: 如何实现配置的版本控制?
|
||||
|
||||
A: 可以在配置中添加版本号,并在加载时进行迁移:
|
||||
|
||||
```csharp
|
||||
public class ConfigurationMigration
|
||||
{
|
||||
private readonly IConfigurationManager _config;
|
||||
|
||||
public void LoadAndMigrate(string path)
|
||||
{
|
||||
_config.LoadFromFile(path);
|
||||
|
||||
var version = _config.GetConfig("config.version", 1);
|
||||
|
||||
if (version < 2)
|
||||
{
|
||||
MigrateToV2();
|
||||
}
|
||||
|
||||
if (version < 3)
|
||||
{
|
||||
MigrateToV3();
|
||||
}
|
||||
|
||||
_config.SetConfig("config.version", 3);
|
||||
_config.SaveToFile(path);
|
||||
}
|
||||
|
||||
private void MigrateToV2()
|
||||
{
|
||||
// 迁移逻辑
|
||||
if (_config.HasConfig("old.key"))
|
||||
{
|
||||
var value = _config.GetConfig<string>("old.key");
|
||||
_config.SetConfig("new.key", value);
|
||||
_config.RemoveConfig("old.key");
|
||||
}
|
||||
}
|
||||
|
||||
private void MigrateToV3() { }
|
||||
}
|
||||
```
|
||||
|
||||
### Q5: 配置管理器的性能如何?
|
||||
|
||||
A: `ConfigurationManager` 使用 `ConcurrentDictionary` 实现,具有良好的并发性能。但要注意:
|
||||
|
||||
- 避免频繁的文件 I/O 操作
|
||||
- 监听器回调应保持轻量
|
||||
- 大量配置项时考虑分组管理
|
||||
|
||||
```csharp
|
||||
// 推荐:批量更新后一次性保存
|
||||
_config.SetConfig("key1", value1);
|
||||
_config.SetConfig("key2", value2);
|
||||
_config.SetConfig("key3", value3);
|
||||
_config.SaveToFile("settings.json"); // 一次性保存
|
||||
|
||||
// 不推荐:每次更新都保存
|
||||
_config.SetConfig("key1", value1);
|
||||
_config.SaveToFile("settings.json");
|
||||
_config.SetConfig("key2", value2);
|
||||
_config.SaveToFile("settings.json");
|
||||
```
|
||||
|
||||
## 相关包
|
||||
|
||||
- [`architecture`](./architecture.md) - 配置管理器作为 Utility 注册到架构
|
||||
- [`utility`](./utility.md) - 配置管理器实现 IUtility 接口
|
||||
- [`events`](./events.md) - 配置变化可以触发事件
|
||||
- [`logging`](./logging.md) - 配置管理器内部使用日志记录
|
||||
506
docs/zh-CN/core/coroutine.md
Normal file
506
docs/zh-CN/core/coroutine.md
Normal file
@ -0,0 +1,506 @@
|
||||
---
|
||||
title: 协程系统
|
||||
description: 协程系统提供了轻量级的异步操作管理机制,支持时间延迟、事件等待、任务等待等多种场景。
|
||||
---
|
||||
|
||||
# 协程系统
|
||||
|
||||
## 概述
|
||||
|
||||
协程系统是 GFramework 中用于管理异步操作的核心机制。通过协程,你可以编写看起来像同步代码的异步逻辑,避免回调地狱,使代码更加清晰易读。
|
||||
|
||||
协程系统基于 C# 的迭代器(IEnumerator)实现,提供了丰富的等待指令(YieldInstruction),可以轻松处理时间延迟、事件等待、任务等待等各种异步场景。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 轻量级协程调度器
|
||||
- 丰富的等待指令(30+ 种)
|
||||
- 支持协程嵌套和组合
|
||||
- 协程标签和批量管理
|
||||
- 与事件系统、命令系统、CQRS 深度集成
|
||||
- 异常处理和错误恢复
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 协程调度器
|
||||
|
||||
`CoroutineScheduler` 是协程系统的核心,负责管理和执行所有协程:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.coroutine;
|
||||
|
||||
// 创建调度器(通常由架构自动管理)
|
||||
var scheduler = new CoroutineScheduler(timeSource);
|
||||
|
||||
// 运行协程
|
||||
var handle = scheduler.Run(MyCoroutine());
|
||||
|
||||
// 每帧更新
|
||||
scheduler.Update();
|
||||
```
|
||||
|
||||
### 协程句柄
|
||||
|
||||
`CoroutineHandle` 用于标识和控制协程:
|
||||
|
||||
```csharp
|
||||
// 运行协程并获取句柄
|
||||
var handle = scheduler.Run(MyCoroutine());
|
||||
|
||||
// 检查协程是否存活
|
||||
if (scheduler.IsCoroutineAlive(handle))
|
||||
{
|
||||
// 停止协程
|
||||
scheduler.Stop(handle);
|
||||
}
|
||||
```
|
||||
|
||||
### 等待指令
|
||||
|
||||
等待指令(YieldInstruction)定义了协程的等待行为:
|
||||
|
||||
```csharp
|
||||
public interface IYieldInstruction
|
||||
{
|
||||
bool IsDone { get; }
|
||||
void Update(double deltaTime);
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 创建简单协程
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.coroutine;
|
||||
using GFramework.Core.coroutine.instructions;
|
||||
|
||||
public IEnumerator<IYieldInstruction> SimpleCoroutine()
|
||||
{
|
||||
Console.WriteLine("开始");
|
||||
|
||||
// 等待 2 秒
|
||||
yield return new Delay(2.0);
|
||||
|
||||
Console.WriteLine("2 秒后");
|
||||
|
||||
// 等待 1 帧
|
||||
yield return new WaitOneFrame();
|
||||
|
||||
Console.WriteLine("下一帧");
|
||||
}
|
||||
```
|
||||
|
||||
### 使用协程辅助方法
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.coroutine;
|
||||
|
||||
public IEnumerator<IYieldInstruction> HelperCoroutine()
|
||||
{
|
||||
// 等待指定秒数
|
||||
yield return CoroutineHelper.WaitForSeconds(1.5);
|
||||
|
||||
// 等待一帧
|
||||
yield return CoroutineHelper.WaitForOneFrame();
|
||||
|
||||
// 等待多帧
|
||||
yield return CoroutineHelper.WaitForFrames(10);
|
||||
|
||||
// 等待条件满足
|
||||
yield return CoroutineHelper.WaitUntil(() => isReady);
|
||||
|
||||
// 等待条件不满足
|
||||
yield return CoroutineHelper.WaitWhile(() => isLoading);
|
||||
}
|
||||
```
|
||||
|
||||
### 在架构组件中使用
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.model;
|
||||
using GFramework.Core.extensions;
|
||||
|
||||
public class PlayerModel : AbstractModel
|
||||
{
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 启动协程
|
||||
this.StartCoroutine(RegenerateHealth());
|
||||
}
|
||||
|
||||
private IEnumerator<IYieldInstruction> RegenerateHealth()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// 每秒恢复 1 点生命值
|
||||
yield return CoroutineHelper.WaitForSeconds(1.0);
|
||||
Health = Math.Min(Health + 1, MaxHealth);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 等待事件
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.coroutine.instructions;
|
||||
|
||||
public IEnumerator<IYieldInstruction> WaitForEventExample()
|
||||
{
|
||||
Console.WriteLine("等待玩家死亡事件...");
|
||||
|
||||
// 等待事件触发
|
||||
var waitEvent = new WaitForEvent<PlayerDiedEvent>(eventBus);
|
||||
yield return waitEvent;
|
||||
|
||||
// 获取事件数据
|
||||
var eventData = waitEvent.EventData;
|
||||
Console.WriteLine($"玩家 {eventData.PlayerId} 死亡");
|
||||
}
|
||||
```
|
||||
|
||||
### 等待事件(带超时)
|
||||
|
||||
```csharp
|
||||
public IEnumerator<IYieldInstruction> WaitForEventWithTimeout()
|
||||
{
|
||||
var waitEvent = new WaitForEventWithTimeout<PlayerJoinedEvent>(
|
||||
eventBus,
|
||||
timeout: 5.0
|
||||
);
|
||||
|
||||
yield return waitEvent;
|
||||
|
||||
if (waitEvent.IsTimeout)
|
||||
{
|
||||
Console.WriteLine("等待超时");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"玩家加入: {waitEvent.EventData.PlayerName}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 等待 Task
|
||||
|
||||
```csharp
|
||||
public IEnumerator<IYieldInstruction> WaitForTaskExample()
|
||||
{
|
||||
// 创建异步任务
|
||||
var task = LoadDataAsync();
|
||||
|
||||
// 在协程中等待 Task 完成
|
||||
var waitTask = new WaitForTask(task);
|
||||
yield return waitTask;
|
||||
|
||||
// 检查异常
|
||||
if (waitTask.Exception != null)
|
||||
{
|
||||
Console.WriteLine($"任务失败: {waitTask.Exception.Message}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("任务完成");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadDataAsync()
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
// 加载数据...
|
||||
}
|
||||
```
|
||||
|
||||
### 等待多个协程
|
||||
|
||||
```csharp
|
||||
public IEnumerator<IYieldInstruction> WaitForMultipleCoroutines()
|
||||
{
|
||||
var coroutine1 = LoadTexture();
|
||||
var coroutine2 = LoadAudio();
|
||||
var coroutine3 = LoadModel();
|
||||
|
||||
// 等待所有协程完成
|
||||
yield return new WaitForAllCoroutines(
|
||||
scheduler,
|
||||
coroutine1,
|
||||
coroutine2,
|
||||
coroutine3
|
||||
);
|
||||
|
||||
Console.WriteLine("所有资源加载完成");
|
||||
}
|
||||
```
|
||||
|
||||
### 协程嵌套
|
||||
|
||||
```csharp
|
||||
public IEnumerator<IYieldInstruction> ParentCoroutine()
|
||||
{
|
||||
Console.WriteLine("父协程开始");
|
||||
|
||||
// 等待子协程完成
|
||||
yield return new WaitForCoroutine(scheduler, ChildCoroutine());
|
||||
|
||||
Console.WriteLine("子协程完成");
|
||||
}
|
||||
|
||||
private IEnumerator<IYieldInstruction> ChildCoroutine()
|
||||
{
|
||||
yield return CoroutineHelper.WaitForSeconds(1.0);
|
||||
Console.WriteLine("子协程执行");
|
||||
}
|
||||
```
|
||||
|
||||
### 带进度的等待
|
||||
|
||||
```csharp
|
||||
public IEnumerator<IYieldInstruction> LoadingWithProgress()
|
||||
{
|
||||
Console.WriteLine("开始加载...");
|
||||
|
||||
yield return CoroutineHelper.WaitForProgress(
|
||||
duration: 3.0,
|
||||
onProgress: progress =>
|
||||
{
|
||||
Console.WriteLine($"加载进度: {progress * 100:F0}%");
|
||||
}
|
||||
);
|
||||
|
||||
Console.WriteLine("加载完成");
|
||||
}
|
||||
```
|
||||
|
||||
### 协程标签管理
|
||||
|
||||
```csharp
|
||||
// 使用标签运行协程
|
||||
var handle1 = scheduler.Run(Coroutine1(), tag: "gameplay");
|
||||
var handle2 = scheduler.Run(Coroutine2(), tag: "gameplay");
|
||||
var handle3 = scheduler.Run(Coroutine3(), tag: "ui");
|
||||
|
||||
// 停止所有带特定标签的协程
|
||||
scheduler.StopAllWithTag("gameplay");
|
||||
|
||||
// 获取标签下的所有协程
|
||||
var gameplayCoroutines = scheduler.GetCoroutinesByTag("gameplay");
|
||||
```
|
||||
|
||||
### 延迟调用和重复调用
|
||||
|
||||
```csharp
|
||||
// 延迟 2 秒后执行
|
||||
scheduler.Run(CoroutineHelper.DelayedCall(2.0, () =>
|
||||
{
|
||||
Console.WriteLine("延迟执行");
|
||||
}));
|
||||
|
||||
// 每隔 1 秒执行一次,共执行 5 次
|
||||
scheduler.Run(CoroutineHelper.RepeatCall(1.0, 5, () =>
|
||||
{
|
||||
Console.WriteLine("重复执行");
|
||||
}));
|
||||
|
||||
// 无限重复,直到条件不满足
|
||||
scheduler.Run(CoroutineHelper.RepeatCallWhile(1.0, () => isRunning, () =>
|
||||
{
|
||||
Console.WriteLine("条件重复");
|
||||
}));
|
||||
```
|
||||
|
||||
### 与命令系统集成
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.coroutine.extensions;
|
||||
|
||||
public IEnumerator<IYieldInstruction> ExecuteCommandInCoroutine()
|
||||
{
|
||||
// 在协程中执行命令
|
||||
var command = new LoadSceneCommand();
|
||||
yield return command.ExecuteAsCoroutine(this);
|
||||
|
||||
Console.WriteLine("场景加载完成");
|
||||
}
|
||||
```
|
||||
|
||||
### 与 CQRS 集成
|
||||
|
||||
```csharp
|
||||
public IEnumerator<IYieldInstruction> QueryInCoroutine()
|
||||
{
|
||||
// 在协程中执行查询
|
||||
var query = new GetPlayerDataQuery { PlayerId = 1 };
|
||||
var waitQuery = query.SendAsCoroutine<GetPlayerDataQuery, PlayerData>(this);
|
||||
|
||||
yield return waitQuery;
|
||||
|
||||
var playerData = waitQuery.Result;
|
||||
Console.WriteLine($"玩家名称: {playerData.Name}");
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用扩展方法启动协程**:通过架构组件的扩展方法启动协程更简洁
|
||||
```csharp
|
||||
✓ this.StartCoroutine(MyCoroutine());
|
||||
✗ scheduler.Run(MyCoroutine());
|
||||
```
|
||||
|
||||
2. **合理使用协程标签**:为相关协程添加标签,便于批量管理
|
||||
```csharp
|
||||
this.StartCoroutine(BattleCoroutine(), tag: "battle");
|
||||
this.StartCoroutine(EffectCoroutine(), tag: "battle");
|
||||
|
||||
// 战斗结束时停止所有战斗相关协程
|
||||
this.StopCoroutinesWithTag("battle");
|
||||
```
|
||||
|
||||
3. **避免在协程中执行耗时操作**:协程在主线程执行,不要阻塞
|
||||
```csharp
|
||||
✗ public IEnumerator<IYieldInstruction> BadCoroutine()
|
||||
{
|
||||
Thread.Sleep(1000); // 阻塞主线程
|
||||
yield return null;
|
||||
}
|
||||
|
||||
✓ public IEnumerator<IYieldInstruction> GoodCoroutine()
|
||||
{
|
||||
yield return CoroutineHelper.WaitForSeconds(1.0); // 非阻塞
|
||||
}
|
||||
```
|
||||
|
||||
4. **正确处理协程异常**:使用 try-catch 捕获异常
|
||||
```csharp
|
||||
public IEnumerator<IYieldInstruction> SafeCoroutine()
|
||||
{
|
||||
var waitTask = new WaitForTask(riskyTask);
|
||||
yield return waitTask;
|
||||
|
||||
if (waitTask.Exception != null)
|
||||
{
|
||||
// 处理异常
|
||||
Logger.Error($"任务失败: {waitTask.Exception.Message}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. **及时停止不需要的协程**:避免资源泄漏
|
||||
```csharp
|
||||
private CoroutineHandle? _healthRegenHandle;
|
||||
|
||||
public void StartHealthRegen()
|
||||
{
|
||||
_healthRegenHandle = this.StartCoroutine(RegenerateHealth());
|
||||
}
|
||||
|
||||
public void StopHealthRegen()
|
||||
{
|
||||
if (_healthRegenHandle.HasValue)
|
||||
{
|
||||
this.StopCoroutine(_healthRegenHandle.Value);
|
||||
_healthRegenHandle = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. **使用 WaitForEvent 时记得释放资源**:避免内存泄漏
|
||||
```csharp
|
||||
public IEnumerator<IYieldInstruction> WaitEventExample()
|
||||
{
|
||||
using var waitEvent = new WaitForEvent<GameEvent>(eventBus);
|
||||
yield return waitEvent;
|
||||
// using 确保资源被释放
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:协程什么时候执行?
|
||||
|
||||
**解答**:
|
||||
协程在调度器的 `Update()` 方法中执行。在 GFramework 中,架构会自动在每帧调用调度器的更新方法。
|
||||
|
||||
### 问题:协程是多线程的吗?
|
||||
|
||||
**解答**:
|
||||
不是。协程在主线程中执行,是单线程的。它们通过分帧执行来实现异步效果,不会阻塞主线程。
|
||||
|
||||
### 问题:如何在协程中等待异步方法?
|
||||
|
||||
**解答**:
|
||||
使用 `WaitForTask` 等待 Task 完成:
|
||||
|
||||
```csharp
|
||||
public IEnumerator<IYieldInstruction> WaitAsyncMethod()
|
||||
{
|
||||
var task = SomeAsyncMethod();
|
||||
yield return new WaitForTask(task);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:协程可以返回值吗?
|
||||
|
||||
**解答**:
|
||||
协程本身不能直接返回值,但可以通过闭包或类成员变量传递结果:
|
||||
|
||||
```csharp
|
||||
private int _result;
|
||||
|
||||
public IEnumerator<IYieldInstruction> CoroutineWithResult()
|
||||
{
|
||||
yield return CoroutineHelper.WaitForSeconds(1.0);
|
||||
_result = 42;
|
||||
}
|
||||
|
||||
// 使用
|
||||
this.StartCoroutine(CoroutineWithResult());
|
||||
// 稍后访问 _result
|
||||
```
|
||||
|
||||
### 问题:如何停止所有协程?
|
||||
|
||||
**解答**:
|
||||
使用调度器的 `StopAll()` 方法:
|
||||
|
||||
```csharp
|
||||
// 停止所有协程
|
||||
scheduler.StopAll();
|
||||
|
||||
// 或通过扩展方法
|
||||
this.StopAllCoroutines();
|
||||
```
|
||||
|
||||
### 问题:协程中的异常会怎样?
|
||||
|
||||
**解答**:
|
||||
协程中未捕获的异常会触发 `OnCoroutineException` 事件,并停止该协程:
|
||||
|
||||
```csharp
|
||||
scheduler.OnCoroutineException += (handle, exception) =>
|
||||
{
|
||||
Logger.Error($"协程异常: {exception.Message}");
|
||||
};
|
||||
```
|
||||
|
||||
### 问题:WaitForSeconds 和 Delay 有什么区别?
|
||||
|
||||
**解答**:
|
||||
它们是相同的,`WaitForSeconds` 是辅助方法,内部创建 `Delay` 实例:
|
||||
|
||||
```csharp
|
||||
// 两者等价
|
||||
yield return CoroutineHelper.WaitForSeconds(1.0);
|
||||
yield return new Delay(1.0);
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [事件系统](/zh-CN/core/events) - 协程与事件系统集成
|
||||
- [命令系统](/zh-CN/core/command) - 在协程中执行命令
|
||||
- [CQRS](/zh-CN/core/cqrs) - 在协程中执行查询和命令
|
||||
- [协程系统教程](/zh-CN/tutorials/coroutine-tutorial) - 分步教程
|
||||
613
docs/zh-CN/core/cqrs.md
Normal file
613
docs/zh-CN/core/cqrs.md
Normal file
@ -0,0 +1,613 @@
|
||||
---
|
||||
title: CQRS 与 Mediator
|
||||
description: CQRS 模式通过 Mediator 实现命令查询职责分离,提供清晰的业务逻辑组织方式。
|
||||
---
|
||||
|
||||
# CQRS 与 Mediator
|
||||
|
||||
## 概述
|
||||
|
||||
CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种架构模式,将数据的读取(Query)和修改(Command)操作分离。GFramework
|
||||
通过集成 Mediator 库实现了 CQRS 模式,提供了类型安全、解耦的业务逻辑处理方式。
|
||||
|
||||
通过 CQRS,你可以将复杂的业务逻辑拆分为独立的命令和查询处理器,每个处理器只负责单一职责,使代码更易于测试和维护。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 命令查询职责分离
|
||||
- 基于 Mediator 模式的解耦设计
|
||||
- 支持管道行为(Behaviors)
|
||||
- 异步处理支持
|
||||
- 与架构系统深度集成
|
||||
- 支持流式处理
|
||||
|
||||
## 核心概念
|
||||
|
||||
### Command(命令)
|
||||
|
||||
命令表示修改系统状态的操作,如创建、更新、删除:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.cqrs.command;
|
||||
using GFramework.Core.Abstractions.cqrs.command;
|
||||
|
||||
// 定义命令输入
|
||||
public class CreatePlayerInput : ICommandInput
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Level { get; set; }
|
||||
}
|
||||
|
||||
// 定义命令
|
||||
public class CreatePlayerCommand : CommandBase<CreatePlayerInput, int>
|
||||
{
|
||||
public CreatePlayerCommand(CreatePlayerInput input) : base(input) { }
|
||||
}
|
||||
```
|
||||
|
||||
### Query(查询)
|
||||
|
||||
查询表示读取系统状态的操作,不修改数据:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.cqrs.query;
|
||||
using GFramework.Core.Abstractions.cqrs.query;
|
||||
|
||||
// 定义查询输入
|
||||
public class GetPlayerInput : IQueryInput
|
||||
{
|
||||
public int PlayerId { get; set; }
|
||||
}
|
||||
|
||||
// 定义查询
|
||||
public class GetPlayerQuery : QueryBase<GetPlayerInput, PlayerData>
|
||||
{
|
||||
public GetPlayerQuery(GetPlayerInput input) : base(input) { }
|
||||
}
|
||||
```
|
||||
|
||||
### Handler(处理器)
|
||||
|
||||
处理器负责执行命令或查询的具体逻辑:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.cqrs.command;
|
||||
using Mediator;
|
||||
|
||||
// 命令处理器
|
||||
public class CreatePlayerCommandHandler : AbstractCommandHandler<CreatePlayerCommand, int>
|
||||
{
|
||||
public override async ValueTask<int> Handle(
|
||||
CreatePlayerCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var input = command.Input;
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
|
||||
// 创建玩家
|
||||
var playerId = playerModel.CreatePlayer(input.Name, input.Level);
|
||||
|
||||
return playerId;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mediator(中介者)
|
||||
|
||||
Mediator 负责将命令/查询路由到对应的处理器:
|
||||
|
||||
```csharp
|
||||
// 通过 Mediator 发送命令
|
||||
var command = new CreatePlayerCommand(new CreatePlayerInput
|
||||
{
|
||||
Name = "Player1",
|
||||
Level = 1
|
||||
});
|
||||
|
||||
var playerId = await mediator.Send(command);
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 定义和发送命令
|
||||
|
||||
```csharp
|
||||
// 1. 定义命令输入
|
||||
public class SaveGameInput : ICommandInput
|
||||
{
|
||||
public int SlotId { get; set; }
|
||||
public GameData Data { get; set; }
|
||||
}
|
||||
|
||||
// 2. 定义命令
|
||||
public class SaveGameCommand : CommandBase<SaveGameInput, Unit>
|
||||
{
|
||||
public SaveGameCommand(SaveGameInput input) : base(input) { }
|
||||
}
|
||||
|
||||
// 3. 实现命令处理器
|
||||
public class SaveGameCommandHandler : AbstractCommandHandler<SaveGameCommand>
|
||||
{
|
||||
public override async ValueTask<Unit> Handle(
|
||||
SaveGameCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var input = command.Input;
|
||||
var saveSystem = this.GetSystem<SaveSystem>();
|
||||
|
||||
// 保存游戏
|
||||
await saveSystem.SaveAsync(input.SlotId, input.Data);
|
||||
|
||||
// 发送事件
|
||||
this.SendEvent(new GameSavedEvent { SlotId = input.SlotId });
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 发送命令
|
||||
public async Task SaveGame()
|
||||
{
|
||||
var mediator = this.GetService<IMediator>();
|
||||
|
||||
var command = new SaveGameCommand(new SaveGameInput
|
||||
{
|
||||
SlotId = 1,
|
||||
Data = currentGameData
|
||||
});
|
||||
|
||||
await mediator.Send(command);
|
||||
}
|
||||
```
|
||||
|
||||
### 定义和发送查询
|
||||
|
||||
```csharp
|
||||
// 1. 定义查询输入
|
||||
public class GetHighScoresInput : IQueryInput
|
||||
{
|
||||
public int Count { get; set; } = 10;
|
||||
}
|
||||
|
||||
// 2. 定义查询
|
||||
public class GetHighScoresQuery : QueryBase<GetHighScoresInput, List<ScoreData>>
|
||||
{
|
||||
public GetHighScoresQuery(GetHighScoresInput input) : base(input) { }
|
||||
}
|
||||
|
||||
// 3. 实现查询处理器
|
||||
public class GetHighScoresQueryHandler : AbstractQueryHandler<GetHighScoresQuery, List<ScoreData>>
|
||||
{
|
||||
public override async ValueTask<List<ScoreData>> Handle(
|
||||
GetHighScoresQuery query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var input = query.Input;
|
||||
var scoreModel = this.GetModel<ScoreModel>();
|
||||
|
||||
// 查询高分榜
|
||||
var scores = await scoreModel.GetTopScoresAsync(input.Count);
|
||||
|
||||
return scores;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 发送查询
|
||||
public async Task<List<ScoreData>> GetHighScores()
|
||||
{
|
||||
var mediator = this.GetService<IMediator>();
|
||||
|
||||
var query = new GetHighScoresQuery(new GetHighScoresInput
|
||||
{
|
||||
Count = 10
|
||||
});
|
||||
|
||||
var scores = await mediator.Send(query);
|
||||
return scores;
|
||||
}
|
||||
```
|
||||
|
||||
### 注册处理器
|
||||
|
||||
在架构中注册 Mediator 和处理器:
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void Init()
|
||||
{
|
||||
// 注册 Mediator 行为
|
||||
RegisterMediatorBehavior<LoggingBehavior>();
|
||||
RegisterMediatorBehavior<PerformanceBehavior>();
|
||||
|
||||
// 处理器会自动通过依赖注入注册
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### Request(请求)
|
||||
|
||||
Request 是更通用的消息类型,可以用于任何场景:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.cqrs.request;
|
||||
using GFramework.Core.Abstractions.cqrs.request;
|
||||
|
||||
// 定义请求输入
|
||||
public class ValidatePlayerInput : IRequestInput
|
||||
{
|
||||
public string PlayerName { get; set; }
|
||||
}
|
||||
|
||||
// 定义请求
|
||||
public class ValidatePlayerRequest : RequestBase<ValidatePlayerInput, bool>
|
||||
{
|
||||
public ValidatePlayerRequest(ValidatePlayerInput input) : base(input) { }
|
||||
}
|
||||
|
||||
// 实现请求处理器
|
||||
public class ValidatePlayerRequestHandler : AbstractRequestHandler<ValidatePlayerRequest, bool>
|
||||
{
|
||||
public override async ValueTask<bool> Handle(
|
||||
ValidatePlayerRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var input = request.Input;
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
|
||||
// 验证玩家名称
|
||||
var isValid = await playerModel.IsNameValidAsync(input.PlayerName);
|
||||
|
||||
return isValid;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Notification(通知)
|
||||
|
||||
Notification 用于一对多的消息广播:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.cqrs.notification;
|
||||
using GFramework.Core.Abstractions.cqrs.notification;
|
||||
|
||||
// 定义通知输入
|
||||
public class PlayerLevelUpInput : INotificationInput
|
||||
{
|
||||
public int PlayerId { get; set; }
|
||||
public int NewLevel { get; set; }
|
||||
}
|
||||
|
||||
// 定义通知
|
||||
public class PlayerLevelUpNotification : NotificationBase<PlayerLevelUpInput>
|
||||
{
|
||||
public PlayerLevelUpNotification(PlayerLevelUpInput input) : base(input) { }
|
||||
}
|
||||
|
||||
// 实现通知处理器 1
|
||||
public class AchievementNotificationHandler : AbstractNotificationHandler<PlayerLevelUpNotification>
|
||||
{
|
||||
public override async ValueTask Handle(
|
||||
PlayerLevelUpNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var input = notification.Input;
|
||||
// 检查成就
|
||||
CheckLevelAchievements(input.PlayerId, input.NewLevel);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
// 实现通知处理器 2
|
||||
public class RewardNotificationHandler : AbstractNotificationHandler<PlayerLevelUpNotification>
|
||||
{
|
||||
public override async ValueTask Handle(
|
||||
PlayerLevelUpNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var input = notification.Input;
|
||||
// 发放奖励
|
||||
GiveRewards(input.PlayerId, input.NewLevel);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
// 发布通知(所有处理器都会收到)
|
||||
var notification = new PlayerLevelUpNotification(new PlayerLevelUpInput
|
||||
{
|
||||
PlayerId = 1,
|
||||
NewLevel = 10
|
||||
});
|
||||
|
||||
await mediator.Publish(notification);
|
||||
```
|
||||
|
||||
### Pipeline Behaviors(管道行为)
|
||||
|
||||
Behaviors 可以在处理器执行前后添加横切关注点:
|
||||
|
||||
```csharp
|
||||
using Mediator;
|
||||
|
||||
// 日志行为
|
||||
public class LoggingBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
|
||||
where TMessage : IMessage
|
||||
{
|
||||
public async ValueTask<TResponse> Handle(
|
||||
TMessage message,
|
||||
CancellationToken cancellationToken,
|
||||
MessageHandlerDelegate<TMessage, TResponse> next)
|
||||
{
|
||||
var messageName = message.GetType().Name;
|
||||
Console.WriteLine($"[开始] {messageName}");
|
||||
|
||||
var response = await next(message, cancellationToken);
|
||||
|
||||
Console.WriteLine($"[完成] {messageName}");
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
// 性能监控行为
|
||||
public class PerformanceBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
|
||||
where TMessage : IMessage
|
||||
{
|
||||
public async ValueTask<TResponse> Handle(
|
||||
TMessage message,
|
||||
CancellationToken cancellationToken,
|
||||
MessageHandlerDelegate<TMessage, TResponse> next)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
var response = await next(message, cancellationToken);
|
||||
|
||||
stopwatch.Stop();
|
||||
var elapsed = stopwatch.ElapsedMilliseconds;
|
||||
|
||||
if (elapsed > 100)
|
||||
{
|
||||
Console.WriteLine($"警告: {message.GetType().Name} 耗时 {elapsed}ms");
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
// 注册行为
|
||||
RegisterMediatorBehavior<LoggingBehavior<,>>();
|
||||
RegisterMediatorBehavior<PerformanceBehavior<,>>();
|
||||
```
|
||||
|
||||
### 验证行为
|
||||
|
||||
```csharp
|
||||
public class ValidationBehavior<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
|
||||
where TMessage : IMessage
|
||||
{
|
||||
public async ValueTask<TResponse> Handle(
|
||||
TMessage message,
|
||||
CancellationToken cancellationToken,
|
||||
MessageHandlerDelegate<TMessage, TResponse> next)
|
||||
{
|
||||
// 验证输入
|
||||
if (message is IValidatable validatable)
|
||||
{
|
||||
var errors = validatable.Validate();
|
||||
if (errors.Any())
|
||||
{
|
||||
throw new ValidationException(errors);
|
||||
}
|
||||
}
|
||||
|
||||
return await next(message, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 流式处理
|
||||
|
||||
处理大量数据时使用流式处理:
|
||||
|
||||
```csharp
|
||||
// 流式查询
|
||||
public class GetAllPlayersStreamQuery : QueryBase<EmptyInput, IAsyncEnumerable<PlayerData>>
|
||||
{
|
||||
public GetAllPlayersStreamQuery() : base(new EmptyInput()) { }
|
||||
}
|
||||
|
||||
// 流式查询处理器
|
||||
public class GetAllPlayersStreamQueryHandler : AbstractStreamQueryHandler<GetAllPlayersStreamQuery, PlayerData>
|
||||
{
|
||||
public override async IAsyncEnumerable<PlayerData> Handle(
|
||||
GetAllPlayersStreamQuery query,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
|
||||
await foreach (var player in playerModel.GetAllPlayersAsync(cancellationToken))
|
||||
{
|
||||
yield return player;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用流式查询
|
||||
var query = new GetAllPlayersStreamQuery();
|
||||
var stream = await mediator.CreateStream(query);
|
||||
|
||||
await foreach (var player in stream)
|
||||
{
|
||||
Console.WriteLine($"玩家: {player.Name}");
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **命令和查询分离**:严格区分修改和读取操作
|
||||
```csharp
|
||||
✓ CreatePlayerCommand, GetPlayerQuery // 职责清晰
|
||||
✗ PlayerCommand // 职责不明确
|
||||
```
|
||||
|
||||
2. **使用有意义的命名**:命令用动词,查询用 Get
|
||||
```csharp
|
||||
✓ CreatePlayerCommand, UpdateScoreCommand, GetHighScoresQuery
|
||||
✗ PlayerCommand, ScoreCommand, ScoresQuery
|
||||
```
|
||||
|
||||
3. **输入验证**:在处理器中验证输入
|
||||
```csharp
|
||||
public override async ValueTask<int> Handle(...)
|
||||
{
|
||||
if (string.IsNullOrEmpty(command.Input.Name))
|
||||
throw new ArgumentException("Name is required");
|
||||
|
||||
// 处理逻辑
|
||||
}
|
||||
```
|
||||
|
||||
4. **使用 Behaviors 处理横切关注点**:日志、性能、验证等
|
||||
```csharp
|
||||
RegisterMediatorBehavior<LoggingBehavior<,>>();
|
||||
RegisterMediatorBehavior<ValidationBehavior<,>>();
|
||||
```
|
||||
|
||||
5. **保持处理器简单**:一个处理器只做一件事
|
||||
```csharp
|
||||
✓ 处理器只负责业务逻辑,通过架构组件访问数据
|
||||
✗ 处理器中包含复杂的数据访问和业务逻辑
|
||||
```
|
||||
|
||||
6. **使用 CancellationToken**:支持操作取消
|
||||
```csharp
|
||||
public override async ValueTask<T> Handle(..., CancellationToken cancellationToken)
|
||||
{
|
||||
await someAsyncOperation(cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:Command 和 Query 有什么区别?
|
||||
|
||||
**解答**:
|
||||
|
||||
- **Command**:修改系统状态,可能有副作用,通常返回 void 或简单结果
|
||||
- **Query**:只读取数据,无副作用,返回查询结果
|
||||
|
||||
```csharp
|
||||
// Command: 修改状态
|
||||
CreatePlayerCommand -> 创建玩家
|
||||
UpdateScoreCommand -> 更新分数
|
||||
|
||||
// Query: 读取数据
|
||||
GetPlayerQuery -> 获取玩家信息
|
||||
GetHighScoresQuery -> 获取高分榜
|
||||
```
|
||||
|
||||
### 问题:什么时候使用 Request?
|
||||
|
||||
**解答**:
|
||||
Request 是更通用的消息类型,当操作既不是纯命令也不是纯查询时使用:
|
||||
|
||||
```csharp
|
||||
// 验证操作:读取数据并返回结果,但不修改状态
|
||||
ValidatePlayerRequest
|
||||
|
||||
// 计算操作:基于输入计算结果
|
||||
CalculateDamageRequest
|
||||
```
|
||||
|
||||
### 问题:Notification 和 Event 有什么区别?
|
||||
|
||||
**解答**:
|
||||
|
||||
- **Notification**:通过 Mediator 发送,处理器在同一请求上下文中执行
|
||||
- **Event**:通过 EventBus 发送,监听器异步执行
|
||||
|
||||
```csharp
|
||||
// Notification: 同步处理
|
||||
await mediator.Publish(notification); // 等待所有处理器完成
|
||||
|
||||
// Event: 异步处理
|
||||
this.SendEvent(event); // 立即返回,监听器异步执行
|
||||
```
|
||||
|
||||
### 问题:如何处理命令失败?
|
||||
|
||||
**解答**:
|
||||
使用异常或返回 Result 类型:
|
||||
|
||||
```csharp
|
||||
// 方式 1: 抛出异常
|
||||
public override async ValueTask<Unit> Handle(...)
|
||||
{
|
||||
if (!IsValid())
|
||||
throw new InvalidOperationException("Invalid operation");
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
|
||||
// 方式 2: 返回 Result
|
||||
public override async ValueTask<Result> Handle(...)
|
||||
{
|
||||
if (!IsValid())
|
||||
return Result.Failure("Invalid operation");
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:处理器可以调用其他处理器吗?
|
||||
|
||||
**解答**:
|
||||
可以,通过 Mediator 发送新的命令或查询:
|
||||
|
||||
```csharp
|
||||
public override async ValueTask<Unit> Handle(...)
|
||||
{
|
||||
var mediator = this.GetService<IMediator>();
|
||||
|
||||
// 调用其他命令
|
||||
await mediator.Send(new AnotherCommand(...));
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何测试处理器?
|
||||
|
||||
**解答**:
|
||||
处理器是独立的类,易于单元测试:
|
||||
|
||||
```csharp
|
||||
[Test]
|
||||
public async Task CreatePlayer_ShouldReturnPlayerId()
|
||||
{
|
||||
// Arrange
|
||||
var handler = new CreatePlayerCommandHandler();
|
||||
handler.SetContext(mockContext);
|
||||
|
||||
var command = new CreatePlayerCommand(new CreatePlayerInput
|
||||
{
|
||||
Name = "Test",
|
||||
Level = 1
|
||||
});
|
||||
|
||||
// Act
|
||||
var playerId = await handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.That(playerId, Is.GreaterThan(0));
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [命令系统](/zh-CN/core/command) - 传统命令模式
|
||||
- [查询系统](/zh-CN/core/query) - 传统查询模式
|
||||
- [事件系统](/zh-CN/core/events) - 事件驱动架构
|
||||
- [协程系统](/zh-CN/core/coroutine) - 在协程中使用 CQRS
|
||||
1005
docs/zh-CN/core/ecs.md
Normal file
1005
docs/zh-CN/core/ecs.md
Normal file
File diff suppressed because it is too large
Load Diff
677
docs/zh-CN/core/functional.md
Normal file
677
docs/zh-CN/core/functional.md
Normal file
@ -0,0 +1,677 @@
|
||||
# 函数式编程指南
|
||||
|
||||
## 概述
|
||||
|
||||
GFramework.Core 提供了一套完整的函数式编程工具,帮助开发者编写更安全、更简洁、更易维护的代码。函数式编程强调不可变性、纯函数和声明式编程风格,能够有效减少副作用,提高代码的可测试性和可组合性。
|
||||
|
||||
本模块提供以下核心功能:
|
||||
|
||||
- **Option 类型**:安全处理可能不存在的值,替代 null 引用
|
||||
- **Result 类型**:优雅处理操作结果和错误,避免异常传播
|
||||
- **管道操作**:构建流式的函数调用链
|
||||
- **函数组合**:组合多个函数形成新函数
|
||||
- **控制流扩展**:函数式风格的条件执行和重试机制
|
||||
- **异步函数式编程**:支持异步操作的函数式封装
|
||||
|
||||
## 核心概念
|
||||
|
||||
### Option 类型
|
||||
|
||||
`Option<T>` 表示可能存在或不存在的值,用于替代 null 引用。它有两种状态:
|
||||
|
||||
- **Some**:包含一个值
|
||||
- **None**:不包含值
|
||||
|
||||
使用 Option 可以在编译时强制处理"无值"的情况,避免空引用异常。
|
||||
|
||||
### Result 类型
|
||||
|
||||
`Result<T>` 表示操作的结果,可能是成功值或失败异常。它有三种状态:
|
||||
|
||||
- **Success**:操作成功,包含返回值
|
||||
- **Faulted**:操作失败,包含异常信息
|
||||
- **Bottom**:未初始化状态
|
||||
|
||||
Result 类型将错误处理显式化,避免使用异常进行流程控制。
|
||||
|
||||
### 管道操作
|
||||
|
||||
管道操作允许将值通过一系列函数进行转换,形成流式的调用链。这种风格使代码更易读,逻辑更清晰。
|
||||
|
||||
### 函数组合
|
||||
|
||||
函数组合是将多个简单函数组合成复杂函数的技术。通过组合,可以构建可复用的函数库,提高代码的模块化程度。
|
||||
|
||||
## 基本用法
|
||||
|
||||
### Option 基础
|
||||
|
||||
#### 创建 Option
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.functional;
|
||||
|
||||
// 创建包含值的 Option
|
||||
var someValue = Option<int>.Some(42);
|
||||
|
||||
// 创建空 Option
|
||||
var noneValue = Option<int>.None;
|
||||
|
||||
// 隐式转换
|
||||
Option<string> name = "Alice"; // Some("Alice")
|
||||
Option<string> empty = null; // None
|
||||
```
|
||||
|
||||
#### 获取值
|
||||
|
||||
```csharp
|
||||
// 使用默认值
|
||||
var value1 = someValue.GetOrElse(0); // 42
|
||||
var value2 = noneValue.GetOrElse(0); // 0
|
||||
|
||||
// 使用工厂函数(延迟计算)
|
||||
var value3 = noneValue.GetOrElse(() => ExpensiveDefault());
|
||||
```
|
||||
|
||||
#### 转换值
|
||||
|
||||
```csharp
|
||||
// Map:映射值到新类型
|
||||
var option = Option<int>.Some(42);
|
||||
var mapped = option.Map(x => x.ToString()); // Option<string>.Some("42")
|
||||
|
||||
// Bind:链式转换(单子绑定)
|
||||
var result = Option<string>.Some("42")
|
||||
.Bind(s => int.TryParse(s, out var i)
|
||||
? Option<int>.Some(i)
|
||||
: Option<int>.None);
|
||||
```
|
||||
|
||||
#### 过滤值
|
||||
|
||||
```csharp
|
||||
var option = Option<int>.Some(42);
|
||||
var filtered = option.Filter(x => x > 0); // Some(42)
|
||||
var filtered2 = option.Filter(x => x < 0); // None
|
||||
```
|
||||
|
||||
#### 模式匹配
|
||||
|
||||
```csharp
|
||||
// 返回值的模式匹配
|
||||
var message = option.Match(
|
||||
some: value => $"Value: {value}",
|
||||
none: () => "No value"
|
||||
);
|
||||
|
||||
// 副作用的模式匹配
|
||||
option.Match(
|
||||
some: value => Console.WriteLine($"Value: {value}"),
|
||||
none: () => Console.WriteLine("No value")
|
||||
);
|
||||
```
|
||||
|
||||
### Result 基础
|
||||
|
||||
#### 创建 Result
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.functional;
|
||||
|
||||
// 创建成功结果
|
||||
var success = Result<int>.Succeed(42);
|
||||
var success2 = Result<int>.Success(42); // 别名
|
||||
|
||||
// 创建失败结果
|
||||
var failure = Result<int>.Fail(new Exception("Error"));
|
||||
var failure2 = Result<int>.Failure("Error message");
|
||||
|
||||
// 隐式转换
|
||||
Result<int> result = 42; // Success(42)
|
||||
```
|
||||
|
||||
#### 安全执行
|
||||
|
||||
```csharp
|
||||
// 自动捕获异常
|
||||
var result = Result<int>.Try(() => int.Parse("42"));
|
||||
|
||||
// 异步安全执行
|
||||
var asyncResult = await ResultExtensions.TryAsync(async () =>
|
||||
await GetDataAsync());
|
||||
```
|
||||
|
||||
#### 获取值
|
||||
|
||||
```csharp
|
||||
// 失败时返回默认值
|
||||
var value1 = result.IfFail(0);
|
||||
|
||||
// 失败时通过函数处理
|
||||
var value2 = result.IfFail(ex =>
|
||||
{
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
return -1;
|
||||
});
|
||||
```
|
||||
|
||||
#### 转换值
|
||||
|
||||
```csharp
|
||||
// Map:映射成功值
|
||||
var mapped = result.Map(x => x * 2);
|
||||
|
||||
// Bind:链式转换
|
||||
var bound = result.Bind(x => x > 0
|
||||
? Result<string>.Succeed(x.ToString())
|
||||
: Result<string>.Fail(new ArgumentException("Must be positive")));
|
||||
|
||||
// 异步映射
|
||||
var asyncMapped = await result.MapAsync(async x =>
|
||||
await ProcessAsync(x));
|
||||
```
|
||||
|
||||
#### 模式匹配
|
||||
|
||||
```csharp
|
||||
// 返回值的模式匹配
|
||||
var message = result.Match(
|
||||
succ: value => $"Success: {value}",
|
||||
fail: ex => $"Error: {ex.Message}"
|
||||
);
|
||||
|
||||
// 副作用的模式匹配
|
||||
result.Match(
|
||||
succ: value => Console.WriteLine($"Success: {value}"),
|
||||
fail: ex => Console.WriteLine($"Error: {ex.Message}")
|
||||
);
|
||||
```
|
||||
|
||||
### 管道操作
|
||||
|
||||
#### Pipe:管道转换
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.functional.pipe;
|
||||
|
||||
var result = 42
|
||||
.Pipe(x => x * 2) // 84
|
||||
.Pipe(x => x.ToString()) // "84"
|
||||
.Pipe(s => $"Result: {s}"); // "Result: 84"
|
||||
```
|
||||
|
||||
#### Tap:副作用操作
|
||||
|
||||
```csharp
|
||||
var result = GetUser()
|
||||
.Tap(user => Console.WriteLine($"User: {user.Name}"))
|
||||
.Tap(user => _logger.LogInfo($"Processing user {user.Id}"))
|
||||
.Pipe(user => new UserDto { Id = user.Id, Name = user.Name });
|
||||
```
|
||||
|
||||
#### Let:作用域转换
|
||||
|
||||
```csharp
|
||||
var dto = GetUser().Let(user => new UserDto
|
||||
{
|
||||
Id = user.Id,
|
||||
Name = user.Name,
|
||||
Email = user.Email
|
||||
});
|
||||
```
|
||||
|
||||
#### PipeIf:条件管道
|
||||
|
||||
```csharp
|
||||
var result = 42.PipeIf(
|
||||
predicate: x => x > 0,
|
||||
ifTrue: x => $"Positive: {x}",
|
||||
ifFalse: x => $"Non-positive: {x}"
|
||||
);
|
||||
```
|
||||
|
||||
### 函数组合
|
||||
|
||||
#### Compose:函数组合
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.functional.functions;
|
||||
|
||||
Func<int, int> addOne = x => x + 1;
|
||||
Func<int, int> multiplyTwo = x => x * 2;
|
||||
|
||||
// f(g(x)) = (x + 1) * 2
|
||||
var composed = multiplyTwo.Compose(addOne);
|
||||
var result = composed(5); // (5 + 1) * 2 = 12
|
||||
```
|
||||
|
||||
#### AndThen:链式组合
|
||||
|
||||
```csharp
|
||||
// g(f(x)) = (x + 1) * 2
|
||||
var chained = addOne.AndThen(multiplyTwo);
|
||||
var result = chained(5); // (5 + 1) * 2 = 12
|
||||
```
|
||||
|
||||
#### Curry:柯里化
|
||||
|
||||
```csharp
|
||||
Func<int, int, int> add = (x, y) => x + y;
|
||||
var curriedAdd = add.Curry();
|
||||
|
||||
var add5 = curriedAdd(5);
|
||||
var result = add5(3); // 8
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### Result 扩展操作
|
||||
|
||||
#### 链式副作用
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.functional.result;
|
||||
|
||||
Result<int>.Succeed(42)
|
||||
.OnSuccess(x => Console.WriteLine($"Value: {x}"))
|
||||
.OnFailure(ex => Console.WriteLine($"Error: {ex.Message}"))
|
||||
.Map(x => x * 2);
|
||||
```
|
||||
|
||||
#### 验证约束
|
||||
|
||||
```csharp
|
||||
var result = Result<int>.Succeed(42)
|
||||
.Ensure(x => x > 0, "Value must be positive")
|
||||
.Ensure(x => x < 100, "Value must be less than 100");
|
||||
```
|
||||
|
||||
#### 聚合多个结果
|
||||
|
||||
```csharp
|
||||
var results = new[]
|
||||
{
|
||||
Result<int>.Succeed(1),
|
||||
Result<int>.Succeed(2),
|
||||
Result<int>.Succeed(3)
|
||||
};
|
||||
|
||||
var combined = results.Combine(); // Result<List<int>>
|
||||
```
|
||||
|
||||
### 控制流扩展
|
||||
|
||||
#### TakeIf:条件返回
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.functional.control;
|
||||
|
||||
var user = GetUser().TakeIf(u => u.IsActive); // 活跃用户或 null
|
||||
|
||||
// 值类型版本
|
||||
var number = 42.TakeIfValue(x => x > 0); // 42 或 null
|
||||
```
|
||||
|
||||
#### When:条件执行
|
||||
|
||||
```csharp
|
||||
var result = 42
|
||||
.When(x => x > 0, x => Console.WriteLine($"Positive: {x}"))
|
||||
.When(x => x % 2 == 0, x => Console.WriteLine("Even"));
|
||||
```
|
||||
|
||||
#### RepeatUntil:重复执行
|
||||
|
||||
```csharp
|
||||
var result = 1.RepeatUntil(
|
||||
func: x => x * 2,
|
||||
predicate: x => x >= 100,
|
||||
maxIterations: 10
|
||||
); // 128
|
||||
```
|
||||
|
||||
#### Retry:同步重试
|
||||
|
||||
```csharp
|
||||
var result = ControlExtensions.Retry(
|
||||
func: () => UnstableOperation(),
|
||||
maxRetries: 3,
|
||||
delayMilliseconds: 100
|
||||
);
|
||||
```
|
||||
|
||||
### 异步函数式编程
|
||||
|
||||
#### 异步重试
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.functional.async;
|
||||
|
||||
var result = await (() => UnreliableOperationAsync())
|
||||
.WithRetryAsync(
|
||||
maxRetries: 3,
|
||||
delay: TimeSpan.FromSeconds(1),
|
||||
shouldRetry: ex => ex is TimeoutException
|
||||
);
|
||||
```
|
||||
|
||||
#### 异步安全执行
|
||||
|
||||
```csharp
|
||||
var result = await (() => RiskyOperationAsync()).TryAsync();
|
||||
|
||||
result.Match(
|
||||
value => Console.WriteLine($"Success: {value}"),
|
||||
error => Console.WriteLine($"Failed: {error.Message}")
|
||||
);
|
||||
```
|
||||
|
||||
#### 异步绑定
|
||||
|
||||
```csharp
|
||||
var result = Result<int>.Succeed(42);
|
||||
var bound = await result.BindAsync(async x =>
|
||||
await GetUserAsync(x) is User user
|
||||
? Result<User>.Succeed(user)
|
||||
: Result<User>.Fail(new Exception("User not found"))
|
||||
);
|
||||
```
|
||||
|
||||
### 高级函数操作
|
||||
|
||||
#### Partial:偏函数应用
|
||||
|
||||
```csharp
|
||||
Func<int, int, int> add = (x, y) => x + y;
|
||||
var add5 = add.Partial(5);
|
||||
var result = add5(3); // 8
|
||||
```
|
||||
|
||||
#### Repeat:重复应用
|
||||
|
||||
```csharp
|
||||
var result = 2.Repeat(3, x => x * 2); // 2 * 2 * 2 * 2 = 16
|
||||
```
|
||||
|
||||
#### Once:单次执行
|
||||
|
||||
```csharp
|
||||
var counter = 0;
|
||||
var once = (() => ++counter).Once();
|
||||
|
||||
var result1 = once(); // 1
|
||||
var result2 = once(); // 1(不会再次执行)
|
||||
```
|
||||
|
||||
#### Defer:延迟执行
|
||||
|
||||
```csharp
|
||||
var lazy = (() => ExpensiveComputation()).Defer();
|
||||
// 此时尚未执行
|
||||
var result = lazy.Value; // 首次访问时才执行
|
||||
```
|
||||
|
||||
#### Memoize:结果缓存
|
||||
|
||||
```csharp
|
||||
Func<int, int> expensive = x =>
|
||||
{
|
||||
Thread.Sleep(1000);
|
||||
return x * x;
|
||||
};
|
||||
|
||||
var memoized = expensive.MemoizeUnbounded();
|
||||
var result1 = memoized(5); // 耗时 1 秒
|
||||
var result2 = memoized(5); // 立即返回(从缓存)
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 何时使用 Option
|
||||
|
||||
1. **替代 null 引用**:当函数可能返回空值时
|
||||
2. **配置参数**:表示可选的配置项
|
||||
3. **查找操作**:字典查找、数据库查询等可能失败的操作
|
||||
4. **链式操作**:需要安全地链式调用多个可能返回空值的方法
|
||||
|
||||
```csharp
|
||||
// 不推荐:使用 null
|
||||
public User? FindUser(int id)
|
||||
{
|
||||
return _users.ContainsKey(id) ? _users[id] : null;
|
||||
}
|
||||
|
||||
// 推荐:使用 Option
|
||||
public Option<User> FindUser(int id)
|
||||
{
|
||||
return _users.TryGetValue(id, out var user)
|
||||
? Option<User>.Some(user)
|
||||
: Option<User>.None;
|
||||
}
|
||||
```
|
||||
|
||||
### 何时使用 Result
|
||||
|
||||
1. **错误处理**:需要显式处理错误的操作
|
||||
2. **验证逻辑**:输入验证、业务规则检查
|
||||
3. **外部调用**:网络请求、文件操作等可能失败的 I/O 操作
|
||||
4. **避免异常**:不希望使用异常进行流程控制的场景
|
||||
|
||||
```csharp
|
||||
// 不推荐:使用异常
|
||||
public int ParseNumber(string input)
|
||||
{
|
||||
if (!int.TryParse(input, out var number))
|
||||
throw new ArgumentException("Invalid number");
|
||||
return number;
|
||||
}
|
||||
|
||||
// 推荐:使用 Result
|
||||
public Result<int> ParseNumber(string input)
|
||||
{
|
||||
return int.TryParse(input, out var number)
|
||||
? Result<int>.Succeed(number)
|
||||
: Result<int>.Failure("Invalid number");
|
||||
}
|
||||
```
|
||||
|
||||
### 何时使用管道操作
|
||||
|
||||
1. **数据转换**:需要对数据进行多步转换
|
||||
2. **流式处理**:构建数据处理管道
|
||||
3. **副作用隔离**:使用 Tap 隔离副作用操作
|
||||
4. **提高可读性**:使复杂的嵌套调用变得线性
|
||||
|
||||
```csharp
|
||||
// 不推荐:嵌套调用
|
||||
var result = FormatResult(
|
||||
ValidateInput(
|
||||
ParseInput(
|
||||
GetInput()
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// 推荐:管道操作
|
||||
var result = GetInput()
|
||||
.Pipe(ParseInput)
|
||||
.Pipe(ValidateInput)
|
||||
.Tap(x => _logger.LogInfo($"Validated: {x}"))
|
||||
.Pipe(FormatResult);
|
||||
```
|
||||
|
||||
### 错误处理模式
|
||||
|
||||
#### 模式 1:早期返回
|
||||
|
||||
```csharp
|
||||
public Result<User> CreateUser(string name, string email)
|
||||
{
|
||||
return ValidateName(name)
|
||||
.Bind(_ => ValidateEmail(email))
|
||||
.Bind(_ => CheckDuplicate(email))
|
||||
.Bind(_ => SaveUser(name, email));
|
||||
}
|
||||
```
|
||||
|
||||
#### 模式 2:聚合验证
|
||||
|
||||
```csharp
|
||||
public Result<User> CreateUser(UserDto dto)
|
||||
{
|
||||
var validations = new[]
|
||||
{
|
||||
ValidateName(dto.Name),
|
||||
ValidateEmail(dto.Email),
|
||||
ValidateAge(dto.Age)
|
||||
};
|
||||
|
||||
return validations.Combine()
|
||||
.Bind(_ => SaveUser(dto));
|
||||
}
|
||||
```
|
||||
|
||||
#### 模式 3:错误恢复
|
||||
|
||||
```csharp
|
||||
public Result<Data> GetData(int id)
|
||||
{
|
||||
return GetFromCache(id)
|
||||
.Match(
|
||||
succ: data => Result<Data>.Succeed(data),
|
||||
fail: _ => GetFromDatabase(id)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 组合模式
|
||||
|
||||
#### 模式 1:Option + Result
|
||||
|
||||
```csharp
|
||||
public Result<User> GetActiveUser(int id)
|
||||
{
|
||||
return FindUser(id) // Option<User>
|
||||
.ToResult("User not found") // Result<User>
|
||||
.Ensure(u => u.IsActive, "User is not active");
|
||||
}
|
||||
```
|
||||
|
||||
#### 模式 2:Result + 管道
|
||||
|
||||
```csharp
|
||||
public Result<UserDto> ProcessUser(int id)
|
||||
{
|
||||
return Result<int>.Succeed(id)
|
||||
.Bind(GetUser)
|
||||
.Map(user => user.Tap(u => _logger.LogInfo($"Processing {u.Name}")))
|
||||
.Map(user => new UserDto { Id = user.Id, Name = user.Name });
|
||||
}
|
||||
```
|
||||
|
||||
#### 模式 3:异步组合
|
||||
|
||||
```csharp
|
||||
public async Task<Result<Response>> ProcessRequestAsync(Request request)
|
||||
{
|
||||
return await Result<Request>.Succeed(request)
|
||||
.Ensure(r => r.IsValid, "Invalid request")
|
||||
.BindAsync(async r => await ValidateAsync(r))
|
||||
.BindAsync(async r => await ProcessAsync(r))
|
||||
.MapAsync(async r => await FormatResponseAsync(r));
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Option vs Nullable
|
||||
|
||||
**Q: Option 和 Nullable<T> 有什么区别?**
|
||||
|
||||
A:
|
||||
|
||||
- `Nullable<T>` 只能用于值类型,`Option<T>` 可用于任何类型
|
||||
- `Option<T>` 提供丰富的函数式操作(Map、Bind、Filter 等)
|
||||
- `Option<T>` 强制显式处理"无值"情况,更安全
|
||||
- `Option<T>` 可以与 Result 等其他函数式类型组合
|
||||
|
||||
### Result vs Exception
|
||||
|
||||
**Q: 什么时候应该使用 Result 而不是异常?**
|
||||
|
||||
A:
|
||||
|
||||
- **使用 Result**:预期的错误情况(验证失败、资源不存在等)
|
||||
- **使用 Exception**:意外的错误情况(系统错误、编程错误等)
|
||||
- Result 使错误处理显式化,提高代码可读性
|
||||
- Result 避免异常的性能开销
|
||||
|
||||
### 性能考虑
|
||||
|
||||
**Q: 函数式编程会影响性能吗?**
|
||||
|
||||
A:
|
||||
|
||||
- Option 和 Result 是值类型(struct),性能开销很小
|
||||
- 管道操作本质是方法调用,JIT 会进行内联优化
|
||||
- Memoize 等缓存机制可以提高性能
|
||||
- 对于性能敏感的代码,可以选择性使用函数式特性
|
||||
|
||||
### 与 LINQ 的关系
|
||||
|
||||
**Q: 函数式扩展与 LINQ 有什么区别?**
|
||||
|
||||
A:
|
||||
|
||||
- LINQ 主要用于集合操作,函数式扩展用于单值操作
|
||||
- 两者可以很好地组合使用
|
||||
- Option 和 Result 可以转换为 IEnumerable 与 LINQ 集成
|
||||
|
||||
```csharp
|
||||
// Option 转 LINQ
|
||||
var options = new[]
|
||||
{
|
||||
Option<int>.Some(1),
|
||||
Option<int>.None,
|
||||
Option<int>.Some(3)
|
||||
};
|
||||
|
||||
var values = options
|
||||
.SelectMany(o => o.ToEnumerable())
|
||||
.ToList(); // [1, 3]
|
||||
```
|
||||
|
||||
### 学习曲线
|
||||
|
||||
**Q: 函数式编程难学吗?**
|
||||
|
||||
A:
|
||||
|
||||
- 从简单的 Option 和 Result 开始
|
||||
- 逐步引入管道操作和函数组合
|
||||
- 不需要一次性掌握所有特性
|
||||
- 在实际项目中逐步应用,积累经验
|
||||
|
||||
## 参考资源
|
||||
|
||||
### 相关文档
|
||||
|
||||
- [Architecture 包使用说明](./architecture.md)
|
||||
- [Extensions 扩展方法](./extensions.md)
|
||||
- [CQRS 模式](./cqrs.md)
|
||||
|
||||
### 外部资源
|
||||
|
||||
- [函数式编程原理](https://en.wikipedia.org/wiki/Functional_programming)
|
||||
- [Railway Oriented Programming](https://fsharpforfunandprofit.com/rop/)
|
||||
- [Option 类型模式](https://en.wikipedia.org/wiki/Option_type)
|
||||
|
||||
### 示例代码
|
||||
|
||||
完整的示例代码可以在测试项目中找到:
|
||||
|
||||
- `GFramework.Core.Tests/functional/OptionTests.cs`
|
||||
- `GFramework.Core.Tests/functional/ResultTests.cs`
|
||||
- `GFramework.Core.Tests/functional/pipe/PipeExtensionsTests.cs`
|
||||
- `GFramework.Core.Tests/functional/functions/FunctionExtensionsTests.cs`
|
||||
- `GFramework.Core.Tests/functional/control/ControlExtensionsTests.cs`
|
||||
461
docs/zh-CN/core/lifecycle.md
Normal file
461
docs/zh-CN/core/lifecycle.md
Normal file
@ -0,0 +1,461 @@
|
||||
---
|
||||
title: 生命周期管理
|
||||
description: 生命周期管理提供了标准化的组件初始化和销毁机制,确保资源的正确管理和释放。
|
||||
---
|
||||
|
||||
# 生命周期管理
|
||||
|
||||
## 概述
|
||||
|
||||
生命周期管理是 GFramework 中用于管理组件初始化和销毁的核心机制。通过实现标准的生命周期接口,组件可以在适当的时机执行初始化逻辑和资源清理,确保系统的稳定性和资源的有效管理。
|
||||
|
||||
GFramework 提供了同步和异步两套生命周期接口,适用于不同的使用场景。架构会自动管理所有注册组件的生命周期,开发者只需实现相应的接口即可。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 标准化的初始化和销毁流程
|
||||
- 支持同步和异步操作
|
||||
- 自动生命周期管理
|
||||
- 按注册顺序初始化,按逆序销毁
|
||||
- 与架构系统深度集成
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 生命周期接口层次
|
||||
|
||||
GFramework 提供了一套完整的生命周期接口:
|
||||
|
||||
```csharp
|
||||
// 同步接口
|
||||
public interface IInitializable
|
||||
{
|
||||
void Initialize();
|
||||
}
|
||||
|
||||
public interface IDestroyable
|
||||
{
|
||||
void Destroy();
|
||||
}
|
||||
|
||||
public interface ILifecycle : IInitializable, IDestroyable
|
||||
{
|
||||
}
|
||||
|
||||
// 异步接口
|
||||
public interface IAsyncInitializable
|
||||
{
|
||||
Task InitializeAsync();
|
||||
}
|
||||
|
||||
public interface IAsyncDestroyable
|
||||
{
|
||||
ValueTask DestroyAsync();
|
||||
}
|
||||
|
||||
public interface IAsyncLifecycle : IAsyncInitializable, IAsyncDestroyable
|
||||
{
|
||||
}
|
||||
```
|
||||
|
||||
### 初始化阶段
|
||||
|
||||
组件在注册到架构后会自动进行初始化:
|
||||
|
||||
```csharp
|
||||
public class PlayerModel : AbstractModel
|
||||
{
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 初始化逻辑
|
||||
Console.WriteLine("PlayerModel 初始化");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 销毁阶段
|
||||
|
||||
当架构销毁时,所有实现了 `IDestroyable` 的组件会按注册的逆序被销毁:
|
||||
|
||||
```csharp
|
||||
public class GameSystem : AbstractSystem
|
||||
{
|
||||
public void Destroy()
|
||||
{
|
||||
// 清理资源
|
||||
Console.WriteLine("GameSystem 销毁");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 实现同步生命周期
|
||||
|
||||
最常见的方式是继承框架提供的抽象基类:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.model;
|
||||
|
||||
public class InventoryModel : AbstractModel
|
||||
{
|
||||
private List<Item> _items = new();
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 初始化库存
|
||||
_items = new List<Item>();
|
||||
Console.WriteLine("库存系统已初始化");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实现销毁逻辑
|
||||
|
||||
对于需要清理资源的组件,实现 `IDestroyable` 接口:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.system;
|
||||
using GFramework.Core.Abstractions.lifecycle;
|
||||
|
||||
public class AudioSystem : ISystem, IDestroyable
|
||||
{
|
||||
private AudioEngine _engine;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_engine = new AudioEngine();
|
||||
_engine.Start();
|
||||
}
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
// 清理音频资源
|
||||
_engine?.Stop();
|
||||
_engine?.Dispose();
|
||||
_engine = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在架构中注册
|
||||
|
||||
组件注册后,架构会自动管理其生命周期:
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void Init()
|
||||
{
|
||||
// 注册顺序:Model -> System -> Utility
|
||||
RegisterModel(new PlayerModel()); // 1. 初始化
|
||||
RegisterModel(new InventoryModel()); // 2. 初始化
|
||||
RegisterSystem(new AudioSystem()); // 3. 初始化
|
||||
|
||||
// 销毁顺序会自动反转:
|
||||
// AudioSystem -> InventoryModel -> PlayerModel
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 异步初始化
|
||||
|
||||
对于需要异步操作的组件(如加载配置、连接数据库),使用异步生命周期:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.lifecycle;
|
||||
using GFramework.Core.Abstractions.system;
|
||||
|
||||
public class ConfigurationSystem : ISystem, IAsyncInitializable
|
||||
{
|
||||
private Configuration _config;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// 异步加载配置文件
|
||||
_config = await LoadConfigurationAsync();
|
||||
Console.WriteLine("配置已加载");
|
||||
}
|
||||
|
||||
private async Task<Configuration> LoadConfigurationAsync()
|
||||
{
|
||||
await Task.Delay(100); // 模拟异步操作
|
||||
return new Configuration();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 异步销毁
|
||||
|
||||
对于需要异步清理的资源(如关闭网络连接、保存数据):
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.lifecycle;
|
||||
|
||||
public class NetworkSystem : ISystem, IAsyncDestroyable
|
||||
{
|
||||
private NetworkClient _client;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_client = new NetworkClient();
|
||||
}
|
||||
|
||||
public async ValueTask DestroyAsync()
|
||||
{
|
||||
// 异步关闭连接
|
||||
if (_client != null)
|
||||
{
|
||||
await _client.DisconnectAsync();
|
||||
await _client.DisposeAsync();
|
||||
}
|
||||
Console.WriteLine("网络连接已关闭");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 完整异步生命周期
|
||||
|
||||
同时实现异步初始化和销毁:
|
||||
|
||||
```csharp
|
||||
public class DatabaseSystem : ISystem, IAsyncLifecycle
|
||||
{
|
||||
private DatabaseConnection _connection;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// 异步连接数据库
|
||||
_connection = new DatabaseConnection();
|
||||
await _connection.ConnectAsync("connection-string");
|
||||
Console.WriteLine("数据库已连接");
|
||||
}
|
||||
|
||||
public async ValueTask DestroyAsync()
|
||||
{
|
||||
// 异步关闭数据库连接
|
||||
if (_connection != null)
|
||||
{
|
||||
await _connection.CloseAsync();
|
||||
await _connection.DisposeAsync();
|
||||
}
|
||||
Console.WriteLine("数据库连接已关闭");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 生命周期钩子
|
||||
|
||||
监听架构的生命周期阶段:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.enums;
|
||||
|
||||
public class AnalyticsSystem : AbstractSystem
|
||||
{
|
||||
protected override void OnInit()
|
||||
{
|
||||
Console.WriteLine("分析系统初始化");
|
||||
}
|
||||
|
||||
public override void OnArchitecturePhase(ArchitecturePhase phase)
|
||||
{
|
||||
switch (phase)
|
||||
{
|
||||
case ArchitecturePhase.Initializing:
|
||||
Console.WriteLine("架构正在初始化");
|
||||
break;
|
||||
case ArchitecturePhase.Ready:
|
||||
Console.WriteLine("架构已就绪");
|
||||
StartTracking();
|
||||
break;
|
||||
case ArchitecturePhase.Destroying:
|
||||
Console.WriteLine("架构正在销毁");
|
||||
StopTracking();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void StartTracking() { }
|
||||
private void StopTracking() { }
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **优先使用抽象基类**:继承 `AbstractModel`、`AbstractSystem` 等基类,它们已经实现了生命周期接口
|
||||
```csharp
|
||||
✓ public class MyModel : AbstractModel { }
|
||||
✗ public class MyModel : IModel, IInitializable { }
|
||||
```
|
||||
|
||||
2. **初始化顺序很重要**:按依赖关系注册组件,被依赖的组件先注册
|
||||
```csharp
|
||||
protected override void Init()
|
||||
{
|
||||
RegisterModel(new ConfigModel()); // 先注册配置
|
||||
RegisterModel(new PlayerModel()); // 再注册依赖配置的模型
|
||||
RegisterSystem(new GameplaySystem()); // 最后注册系统
|
||||
}
|
||||
```
|
||||
|
||||
3. **销毁时释放资源**:实现 `Destroy()` 方法清理非托管资源
|
||||
```csharp
|
||||
public void Destroy()
|
||||
{
|
||||
// 释放事件订阅
|
||||
_eventBus.Unsubscribe<GameEvent>(OnGameEvent);
|
||||
|
||||
// 释放非托管资源
|
||||
_nativeHandle?.Dispose();
|
||||
|
||||
// 清空引用
|
||||
_cache?.Clear();
|
||||
}
|
||||
```
|
||||
|
||||
4. **异步操作使用异步接口**:避免在同步方法中阻塞异步操作
|
||||
```csharp
|
||||
✓ public async Task InitializeAsync() { await LoadDataAsync(); }
|
||||
✗ public void Initialize() { LoadDataAsync().Wait(); } // 可能死锁
|
||||
```
|
||||
|
||||
5. **避免在初始化中访问其他组件**:初始化顺序可能导致组件尚未就绪
|
||||
```csharp
|
||||
✗ protected override void OnInit()
|
||||
{
|
||||
var system = this.GetSystem<OtherSystem>(); // 可能尚未初始化
|
||||
}
|
||||
|
||||
✓ public override void OnArchitecturePhase(ArchitecturePhase phase)
|
||||
{
|
||||
if (phase == ArchitecturePhase.Ready)
|
||||
{
|
||||
var system = this.GetSystem<OtherSystem>(); // 安全
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. **使用 OnArchitecturePhase 处理跨组件依赖**:在 Ready 阶段访问其他组件
|
||||
```csharp
|
||||
public override void OnArchitecturePhase(ArchitecturePhase phase)
|
||||
{
|
||||
if (phase == ArchitecturePhase.Ready)
|
||||
{
|
||||
// 此时所有组件都已初始化完成
|
||||
var config = this.GetModel<ConfigModel>();
|
||||
ApplyConfiguration(config);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:什么时候使用同步 vs 异步生命周期?
|
||||
|
||||
**解答**:
|
||||
|
||||
- **同步**:简单的初始化逻辑,如创建对象、设置默认值
|
||||
- **异步**:需要 I/O 操作的场景,如加载文件、网络请求、数据库连接
|
||||
|
||||
```csharp
|
||||
// 同步:简单初始化
|
||||
public class ScoreModel : AbstractModel
|
||||
{
|
||||
protected override void OnInit()
|
||||
{
|
||||
Score = 0; // 简单赋值
|
||||
}
|
||||
}
|
||||
|
||||
// 异步:需要 I/O
|
||||
public class SaveSystem : ISystem, IAsyncInitializable
|
||||
{
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await LoadSaveDataAsync(); // 文件 I/O
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:组件的初始化和销毁顺序是什么?
|
||||
|
||||
**解答**:
|
||||
|
||||
- **初始化顺序**:按注册顺序(先注册先初始化)
|
||||
- **销毁顺序**:按注册的逆序(后注册先销毁)
|
||||
|
||||
```csharp
|
||||
protected override void Init()
|
||||
{
|
||||
RegisterModel(new A()); // 1. 初始化,3. 销毁
|
||||
RegisterModel(new B()); // 2. 初始化,2. 销毁
|
||||
RegisterSystem(new C()); // 3. 初始化,1. 销毁
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何在初始化时访问其他组件?
|
||||
|
||||
**解答**:
|
||||
不要在 `OnInit()` 中访问其他组件,使用 `OnArchitecturePhase()` 在 Ready 阶段访问:
|
||||
|
||||
```csharp
|
||||
public class DependentSystem : AbstractSystem
|
||||
{
|
||||
protected override void OnInit()
|
||||
{
|
||||
// ✗ 不要在这里访问其他组件
|
||||
}
|
||||
|
||||
public override void OnArchitecturePhase(ArchitecturePhase phase)
|
||||
{
|
||||
if (phase == ArchitecturePhase.Ready)
|
||||
{
|
||||
// ✓ 在这里安全访问其他组件
|
||||
var config = this.GetModel<ConfigModel>();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:Destroy() 方法一定会被调用吗?
|
||||
|
||||
**解答**:
|
||||
只有在正常销毁架构时才会调用。如果应用程序崩溃或被强制终止,`Destroy()` 可能不会被调用。因此:
|
||||
|
||||
- 不要依赖 `Destroy()` 保存关键数据
|
||||
- 使用自动保存机制保护重要数据
|
||||
- 非托管资源应该实现 `IDisposable` 模式
|
||||
|
||||
### 问题:可以在 Destroy() 中访问其他组件吗?
|
||||
|
||||
**解答**:
|
||||
不推荐。销毁时其他组件可能已经被销毁。如果必须访问,确保检查组件是否仍然可用:
|
||||
|
||||
```csharp
|
||||
public void Destroy()
|
||||
{
|
||||
// ✗ 不安全
|
||||
var system = this.GetSystem<OtherSystem>();
|
||||
system.DoSomething();
|
||||
|
||||
// ✓ 安全
|
||||
try
|
||||
{
|
||||
var system = this.GetSystem<OtherSystem>();
|
||||
system?.DoSomething();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 组件可能已销毁
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [架构组件](/zh-CN/core/architecture) - 架构基础和组件注册
|
||||
- [Model 层](/zh-CN/core/model) - 数据模型的生命周期
|
||||
- [System 层](/zh-CN/core/system) - 业务系统的生命周期
|
||||
- [异步初始化](/zh-CN/core/async-initialization) - 异步架构初始化详解
|
||||
918
docs/zh-CN/core/pause.md
Normal file
918
docs/zh-CN/core/pause.md
Normal file
@ -0,0 +1,918 @@
|
||||
# 暂停管理系统使用说明
|
||||
|
||||
## 概述
|
||||
|
||||
暂停管理系统(Pause System)提供了一套完整的游戏暂停控制机制,支持多层嵌套暂停、分组暂停、以及灵活的暂停处理器扩展。该系统基于栈结构实现,能够优雅地处理复杂的暂停场景,如菜单叠加、对话框弹出等。
|
||||
|
||||
暂停系统是 GFramework 架构中的核心工具(Utility),与其他系统协同工作,为游戏提供统一的暂停管理能力。
|
||||
|
||||
**主要特性:**
|
||||
|
||||
- **嵌套暂停**:支持多层暂停请求,只有所有请求都解除后才恢复
|
||||
- **分组管理**:不同系统可以独立暂停(游戏逻辑、动画、音频等)
|
||||
- **线程安全**:使用读写锁保证并发安全
|
||||
- **作用域管理**:支持 `using` 语法自动管理暂停生命周期
|
||||
- **事件通知**:状态变化时通知所有注册的处理器
|
||||
- **优先级控制**:处理器按优先级顺序执行
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 暂停栈(Pause Stack)
|
||||
|
||||
暂停系统使用栈结构管理暂停请求。每次调用 `Push` 会将暂停请求压入栈中,调用 `Pop` 会从栈中移除对应的请求。只有当栈为空时,游戏才会恢复运行。
|
||||
|
||||
```
|
||||
栈深度 3: [暂停原因: "库存界面"]
|
||||
栈深度 2: [暂停原因: "对话框"]
|
||||
栈深度 1: [暂停原因: "暂停菜单"]
|
||||
```
|
||||
|
||||
### 暂停组(Pause Group)
|
||||
|
||||
暂停组允许不同系统独立控制暂停状态。例如,打开菜单时可以暂停游戏逻辑但保持 UI 动画运行。
|
||||
|
||||
**预定义组:**
|
||||
|
||||
- `Global` - 全局暂停(影响所有系统)
|
||||
- `Gameplay` - 游戏逻辑暂停(不影响 UI)
|
||||
- `Animation` - 动画暂停
|
||||
- `Audio` - 音频暂停
|
||||
- `Custom1/2/3` - 自定义组
|
||||
|
||||
### 暂停令牌(Pause Token)
|
||||
|
||||
每次暂停请求都会返回一个唯一的令牌,用于后续恢复操作。令牌基于 GUID 实现,确保唯一性。
|
||||
|
||||
```csharp
|
||||
public readonly struct PauseToken
|
||||
{
|
||||
public Guid Id { get; }
|
||||
public bool IsValid => Id != Guid.Empty;
|
||||
}
|
||||
```
|
||||
|
||||
### 暂停处理器(Pause Handler)
|
||||
|
||||
处理器实现具体的暂停/恢复逻辑,如控制物理引擎、音频系统等。处理器按优先级顺序执行。
|
||||
|
||||
```csharp
|
||||
public interface IPauseHandler
|
||||
{
|
||||
int Priority { get; } // 优先级(数值越小越高)
|
||||
void OnPauseStateChanged(PauseGroup group, bool isPaused);
|
||||
}
|
||||
```
|
||||
|
||||
## 核心接口
|
||||
|
||||
### IPauseStackManager
|
||||
|
||||
暂停栈管理器接口,提供暂停控制的所有功能。
|
||||
|
||||
**核心方法:**
|
||||
|
||||
```csharp
|
||||
// 推入暂停请求
|
||||
PauseToken Push(string reason, PauseGroup group = PauseGroup.Global);
|
||||
|
||||
// 弹出暂停请求
|
||||
bool Pop(PauseToken token);
|
||||
|
||||
// 查询暂停状态
|
||||
bool IsPaused(PauseGroup group = PauseGroup.Global);
|
||||
|
||||
// 获取暂停深度
|
||||
int GetPauseDepth(PauseGroup group = PauseGroup.Global);
|
||||
|
||||
// 获取暂停原因列表
|
||||
IReadOnlyList<string> GetPauseReasons(PauseGroup group = PauseGroup.Global);
|
||||
|
||||
// 创建暂停作用域
|
||||
IDisposable PauseScope(string reason, PauseGroup group = PauseGroup.Global);
|
||||
|
||||
// 清空指定组
|
||||
void ClearGroup(PauseGroup group);
|
||||
|
||||
// 清空所有组
|
||||
void ClearAll();
|
||||
|
||||
// 注册/注销处理器
|
||||
void RegisterHandler(IPauseHandler handler);
|
||||
void UnregisterHandler(IPauseHandler handler);
|
||||
|
||||
// 状态变化事件
|
||||
event Action<PauseGroup, bool>? OnPauseStateChanged;
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 1. 获取暂停管理器
|
||||
|
||||
```csharp
|
||||
public class GameController : IController
|
||||
{
|
||||
private IPauseStackManager _pauseManager;
|
||||
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
// 从架构中获取暂停管理器
|
||||
_pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 简单的暂停/恢复
|
||||
|
||||
```csharp
|
||||
public class PauseMenuController : IController
|
||||
{
|
||||
private IPauseStackManager _pauseManager;
|
||||
private PauseToken _pauseToken;
|
||||
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
}
|
||||
|
||||
public void OpenPauseMenu()
|
||||
{
|
||||
// 暂停游戏
|
||||
_pauseToken = _pauseManager.Push("暂停菜单");
|
||||
|
||||
Console.WriteLine($"游戏已暂停,深度: {_pauseManager.GetPauseDepth()}");
|
||||
}
|
||||
|
||||
public void ClosePauseMenu()
|
||||
{
|
||||
// 恢复游戏
|
||||
if (_pauseToken.IsValid)
|
||||
{
|
||||
_pauseManager.Pop(_pauseToken);
|
||||
Console.WriteLine("游戏已恢复");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 使用作用域自动管理
|
||||
|
||||
```csharp
|
||||
public class DialogController : IController
|
||||
{
|
||||
private IPauseStackManager _pauseManager;
|
||||
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public void ShowDialog(string message)
|
||||
{
|
||||
// 使用 using 语法,自动管理暂停生命周期
|
||||
using (_pauseManager.PauseScope("对话框"))
|
||||
{
|
||||
Console.WriteLine($"显示对话框: {message}");
|
||||
// 对话框显示期间游戏暂停
|
||||
WaitForUserInput();
|
||||
}
|
||||
// 离开作用域后自动恢复
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 查询暂停状态
|
||||
|
||||
```csharp
|
||||
public class GameplaySystem : AbstractSystem
|
||||
{
|
||||
private IPauseStackManager _pauseManager;
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
// 检查是否暂停
|
||||
if (_pauseManager.IsPaused(PauseGroup.Gameplay))
|
||||
{
|
||||
return; // 暂停时跳过更新
|
||||
}
|
||||
|
||||
// 正常游戏逻辑
|
||||
UpdateGameLogic(deltaTime);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 1. 嵌套暂停
|
||||
|
||||
```csharp
|
||||
public class UIManager : IController
|
||||
{
|
||||
private IPauseStackManager _pauseManager;
|
||||
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public void ShowNestedMenus()
|
||||
{
|
||||
// 第一层:主菜单
|
||||
var token1 = _pauseManager.Push("主菜单");
|
||||
Console.WriteLine($"深度: {_pauseManager.GetPauseDepth()}"); // 输出: 1
|
||||
|
||||
// 第二层:设置菜单
|
||||
var token2 = _pauseManager.Push("设置菜单");
|
||||
Console.WriteLine($"深度: {_pauseManager.GetPauseDepth()}"); // 输出: 2
|
||||
|
||||
// 第三层:确认对话框
|
||||
var token3 = _pauseManager.Push("确认对话框");
|
||||
Console.WriteLine($"深度: {_pauseManager.GetPauseDepth()}"); // 输出: 3
|
||||
|
||||
// 关闭对话框
|
||||
_pauseManager.Pop(token3);
|
||||
Console.WriteLine($"仍然暂停: {_pauseManager.IsPaused()}"); // 输出: True
|
||||
|
||||
// 关闭设置菜单
|
||||
_pauseManager.Pop(token2);
|
||||
Console.WriteLine($"仍然暂停: {_pauseManager.IsPaused()}"); // 输出: True
|
||||
|
||||
// 关闭主菜单
|
||||
_pauseManager.Pop(token1);
|
||||
Console.WriteLine($"已恢复: {!_pauseManager.IsPaused()}"); // 输出: True
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 分组暂停
|
||||
|
||||
```csharp
|
||||
public class GameManager : IController
|
||||
{
|
||||
private IPauseStackManager _pauseManager;
|
||||
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public void OpenInventory()
|
||||
{
|
||||
// 只暂停游戏逻辑,UI 和音频继续运行
|
||||
var token = _pauseManager.Push("库存界面", PauseGroup.Gameplay);
|
||||
|
||||
Console.WriteLine($"游戏逻辑暂停: {_pauseManager.IsPaused(PauseGroup.Gameplay)}");
|
||||
Console.WriteLine($"音频暂停: {_pauseManager.IsPaused(PauseGroup.Audio)}");
|
||||
Console.WriteLine($"全局暂停: {_pauseManager.IsPaused(PauseGroup.Global)}");
|
||||
}
|
||||
|
||||
public void OpenPauseMenu()
|
||||
{
|
||||
// 全局暂停,影响所有系统
|
||||
var token = _pauseManager.Push("暂停菜单", PauseGroup.Global);
|
||||
|
||||
Console.WriteLine($"所有系统已暂停");
|
||||
}
|
||||
|
||||
public void MuteAudio()
|
||||
{
|
||||
// 只暂停音频
|
||||
var token = _pauseManager.Push("静音", PauseGroup.Audio);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 自定义暂停处理器
|
||||
|
||||
```csharp
|
||||
// 物理引擎暂停处理器
|
||||
public class PhysicsPauseHandler : IPauseHandler
|
||||
{
|
||||
private readonly PhysicsWorld _physicsWorld;
|
||||
|
||||
public PhysicsPauseHandler(PhysicsWorld physicsWorld)
|
||||
{
|
||||
_physicsWorld = physicsWorld;
|
||||
}
|
||||
|
||||
// 高优先级,确保物理引擎最先暂停
|
||||
public int Priority => 10;
|
||||
|
||||
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||
{
|
||||
// 只响应游戏逻辑和全局暂停
|
||||
if (group == PauseGroup.Gameplay || group == PauseGroup.Global)
|
||||
{
|
||||
_physicsWorld.Enabled = !isPaused;
|
||||
Console.WriteLine($"物理引擎 {(isPaused ? "已暂停" : "已恢复")}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 音频系统暂停处理器
|
||||
public class AudioPauseHandler : IPauseHandler
|
||||
{
|
||||
private readonly AudioSystem _audioSystem;
|
||||
|
||||
public AudioPauseHandler(AudioSystem audioSystem)
|
||||
{
|
||||
_audioSystem = audioSystem;
|
||||
}
|
||||
|
||||
public int Priority => 20;
|
||||
|
||||
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||
{
|
||||
// 响应音频和全局暂停
|
||||
if (group == PauseGroup.Audio || group == PauseGroup.Global)
|
||||
{
|
||||
if (isPaused)
|
||||
{
|
||||
_audioSystem.PauseAll();
|
||||
}
|
||||
else
|
||||
{
|
||||
_audioSystem.ResumeAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 注册处理器
|
||||
public class GameInitializer
|
||||
{
|
||||
public void Initialize()
|
||||
{
|
||||
var pauseManager = architecture.GetUtility<IPauseStackManager>();
|
||||
var physicsWorld = GetPhysicsWorld();
|
||||
var audioSystem = GetAudioSystem();
|
||||
|
||||
// 注册处理器
|
||||
pauseManager.RegisterHandler(new PhysicsPauseHandler(physicsWorld));
|
||||
pauseManager.RegisterHandler(new AudioPauseHandler(audioSystem));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 监听暂停状态变化
|
||||
|
||||
```csharp
|
||||
public class PauseIndicator : IController
|
||||
{
|
||||
private IPauseStackManager _pauseManager;
|
||||
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
|
||||
// 订阅状态变化事件
|
||||
_pauseManager.OnPauseStateChanged += OnPauseStateChanged;
|
||||
}
|
||||
|
||||
private void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||
{
|
||||
Console.WriteLine($"暂停状态变化: 组={group}, 暂停={isPaused}");
|
||||
|
||||
if (group == PauseGroup.Global)
|
||||
{
|
||||
if (isPaused)
|
||||
{
|
||||
ShowPauseIndicator();
|
||||
}
|
||||
else
|
||||
{
|
||||
HidePauseIndicator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Cleanup()
|
||||
{
|
||||
_pauseManager.OnPauseStateChanged -= OnPauseStateChanged;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 调试暂停状态
|
||||
|
||||
```csharp
|
||||
public class PauseDebugger : IController
|
||||
{
|
||||
private IPauseStackManager _pauseManager;
|
||||
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public void PrintPauseStatus()
|
||||
{
|
||||
Console.WriteLine("=== 暂停状态 ===");
|
||||
|
||||
foreach (PauseGroup group in Enum.GetValues(typeof(PauseGroup)))
|
||||
{
|
||||
var isPaused = _pauseManager.IsPaused(group);
|
||||
var depth = _pauseManager.GetPauseDepth(group);
|
||||
var reasons = _pauseManager.GetPauseReasons(group);
|
||||
|
||||
Console.WriteLine($"\n组: {group}");
|
||||
Console.WriteLine($" 状态: {(isPaused ? "暂停" : "运行")}");
|
||||
Console.WriteLine($" 深度: {depth}");
|
||||
|
||||
if (reasons.Count > 0)
|
||||
{
|
||||
Console.WriteLine(" 原因:");
|
||||
foreach (var reason in reasons)
|
||||
{
|
||||
Console.WriteLine($" - {reason}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 紧急恢复
|
||||
|
||||
```csharp
|
||||
public class EmergencyController : IController
|
||||
{
|
||||
private IPauseStackManager _pauseManager;
|
||||
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public void ForceResumeAll()
|
||||
{
|
||||
// 清空所有暂停请求(谨慎使用)
|
||||
_pauseManager.ClearAll();
|
||||
Console.WriteLine("已强制恢复所有系统");
|
||||
}
|
||||
|
||||
public void ForceResumeGameplay()
|
||||
{
|
||||
// 只清空游戏逻辑组
|
||||
_pauseManager.ClearGroup(PauseGroup.Gameplay);
|
||||
Console.WriteLine("已强制恢复游戏逻辑");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Godot 集成
|
||||
|
||||
### GodotPauseHandler
|
||||
|
||||
GFramework.Godot 提供了 Godot 引擎的暂停处理器实现:
|
||||
|
||||
```csharp
|
||||
public class GodotPauseHandler : IPauseHandler
|
||||
{
|
||||
private readonly SceneTree _tree;
|
||||
|
||||
public GodotPauseHandler(SceneTree tree)
|
||||
{
|
||||
_tree = tree;
|
||||
}
|
||||
|
||||
public int Priority => 0;
|
||||
|
||||
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||
{
|
||||
// 只有 Global 组影响 Godot 的全局暂停
|
||||
if (group == PauseGroup.Global)
|
||||
{
|
||||
_tree.Paused = isPaused;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在 Godot 中使用
|
||||
|
||||
```csharp
|
||||
public partial class GameRoot : Node
|
||||
{
|
||||
private IPauseStackManager _pauseManager;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 获取暂停管理器
|
||||
_pauseManager = architecture.GetUtility<IPauseStackManager>();
|
||||
|
||||
// 注册 Godot 处理器
|
||||
var godotHandler = new GodotPauseHandler(GetTree());
|
||||
_pauseManager.RegisterHandler(godotHandler);
|
||||
}
|
||||
|
||||
public void OnPauseButtonPressed()
|
||||
{
|
||||
// 暂停游戏
|
||||
_pauseManager.Push("玩家暂停", PauseGroup.Global);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 配合 ProcessMode
|
||||
|
||||
```csharp
|
||||
public partial class PauseMenu : Control
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
// 设置为 Always 模式,暂停时仍然处理输入
|
||||
ProcessMode = ProcessModeEnum.Always;
|
||||
}
|
||||
|
||||
public override void _Input(InputEvent @event)
|
||||
{
|
||||
if (@event.IsActionPressed("ui_cancel"))
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
|
||||
if (pauseManager.IsPaused())
|
||||
{
|
||||
// 恢复游戏
|
||||
ResumeGame();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 暂停游戏
|
||||
PauseGame();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用作用域管理
|
||||
|
||||
优先使用 `PauseScope` 而不是手动 `Push/Pop`,避免忘记恢复:
|
||||
|
||||
```csharp
|
||||
// ✅ 推荐
|
||||
public void ShowDialog()
|
||||
{
|
||||
using (_pauseManager.PauseScope("对话框"))
|
||||
{
|
||||
// 对话框逻辑
|
||||
}
|
||||
// 自动恢复
|
||||
}
|
||||
|
||||
// ❌ 不推荐
|
||||
public void ShowDialog()
|
||||
{
|
||||
var token = _pauseManager.Push("对话框");
|
||||
// 对话框逻辑
|
||||
_pauseManager.Pop(token); // 容易忘记
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 提供清晰的暂停原因
|
||||
|
||||
暂停原因用于调试,应该清晰描述暂停来源:
|
||||
|
||||
```csharp
|
||||
// ✅ 推荐
|
||||
_pauseManager.Push("主菜单 - 设置页面");
|
||||
_pauseManager.Push("过场动画 - 关卡加载");
|
||||
_pauseManager.Push("教程对话框 - 第一关");
|
||||
|
||||
// ❌ 不推荐
|
||||
_pauseManager.Push("pause");
|
||||
_pauseManager.Push("menu");
|
||||
```
|
||||
|
||||
### 3. 合理选择暂停组
|
||||
|
||||
根据实际需求选择合适的暂停组:
|
||||
|
||||
```csharp
|
||||
// 打开库存:只暂停游戏逻辑
|
||||
_pauseManager.Push("库存界面", PauseGroup.Gameplay);
|
||||
|
||||
// 打开暂停菜单:全局暂停
|
||||
_pauseManager.Push("暂停菜单", PauseGroup.Global);
|
||||
|
||||
// 播放过场动画:暂停游戏逻辑和输入
|
||||
_pauseManager.Push("过场动画", PauseGroup.Gameplay);
|
||||
```
|
||||
|
||||
### 4. 处理器优先级设计
|
||||
|
||||
合理设置处理器优先级,确保正确的执行顺序:
|
||||
|
||||
```csharp
|
||||
// 物理引擎:高优先级(10),最先暂停
|
||||
public class PhysicsPauseHandler : IPauseHandler
|
||||
{
|
||||
public int Priority => 10;
|
||||
}
|
||||
|
||||
// 音频系统:中优先级(20)
|
||||
public class AudioPauseHandler : IPauseHandler
|
||||
{
|
||||
public int Priority => 20;
|
||||
}
|
||||
|
||||
// UI 动画:低优先级(30),最后暂停
|
||||
public class UiAnimationPauseHandler : IPauseHandler
|
||||
{
|
||||
public int Priority => 30;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 避免在处理器中抛出异常
|
||||
|
||||
处理器异常会被捕获并记录,但不会中断其他处理器:
|
||||
|
||||
```csharp
|
||||
public class SafePauseHandler : IPauseHandler
|
||||
{
|
||||
public int Priority => 0;
|
||||
|
||||
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 可能失败的操作
|
||||
RiskyOperation();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录错误但不抛出
|
||||
Console.WriteLine($"暂停处理失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 线程安全考虑
|
||||
|
||||
暂停管理器是线程安全的,但处理器回调在主线程执行:
|
||||
|
||||
```csharp
|
||||
public class ThreadSafeUsage
|
||||
{
|
||||
private IPauseStackManager _pauseManager;
|
||||
|
||||
public void WorkerThread()
|
||||
{
|
||||
// ✅ 可以从任何线程调用
|
||||
Task.Run(() =>
|
||||
{
|
||||
var token = _pauseManager.Push("后台任务");
|
||||
// 执行任务
|
||||
_pauseManager.Pop(token);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. 清理资源
|
||||
|
||||
在组件销毁时注销处理器和事件:
|
||||
|
||||
```csharp
|
||||
public class ProperCleanup : IController
|
||||
{
|
||||
private IPauseStackManager _pauseManager;
|
||||
private IPauseHandler _customHandler;
|
||||
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
_customHandler = new CustomPauseHandler();
|
||||
|
||||
_pauseManager.RegisterHandler(_customHandler);
|
||||
_pauseManager.OnPauseStateChanged += OnPauseChanged;
|
||||
}
|
||||
|
||||
public void Cleanup()
|
||||
{
|
||||
_pauseManager.UnregisterHandler(_customHandler);
|
||||
_pauseManager.OnPauseStateChanged -= OnPauseChanged;
|
||||
}
|
||||
|
||||
private void OnPauseChanged(PauseGroup group, bool isPaused) { }
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 为什么调用 Pop 后游戏还是暂停?
|
||||
|
||||
A: 暂停系统使用栈结构,只有当栈为空时才会恢复。检查是否有其他暂停请求:
|
||||
|
||||
```csharp
|
||||
// 调试暂停状态
|
||||
var depth = _pauseManager.GetPauseDepth();
|
||||
var reasons = _pauseManager.GetPauseReasons();
|
||||
|
||||
Console.WriteLine($"当前暂停深度: {depth}");
|
||||
Console.WriteLine("暂停原因:");
|
||||
foreach (var reason in reasons)
|
||||
{
|
||||
Console.WriteLine($" - {reason}");
|
||||
}
|
||||
```
|
||||
|
||||
### Q2: 如何实现"暂停时显示菜单"?
|
||||
|
||||
A: 使用 Godot 的 `ProcessMode` 或监听暂停事件:
|
||||
|
||||
```csharp
|
||||
public partial class PauseMenu : Control
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
// 方案 1: 设置为 Always 模式
|
||||
ProcessMode = ProcessModeEnum.Always;
|
||||
Visible = false;
|
||||
|
||||
// 方案 2: 监听暂停事件
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
pauseManager.OnPauseStateChanged += (group, isPaused) =>
|
||||
{
|
||||
if (group == PauseGroup.Global)
|
||||
{
|
||||
Visible = isPaused;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Q3: 可以在暂停期间执行某些逻辑吗?
|
||||
|
||||
A: 可以,通过检查暂停状态或使用不同的暂停组:
|
||||
|
||||
```csharp
|
||||
public class SelectiveSystem : AbstractSystem
|
||||
{
|
||||
protected override void OnInit() { }
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
|
||||
// 方案 1: 检查特定组
|
||||
if (!pauseManager.IsPaused(PauseGroup.Gameplay))
|
||||
{
|
||||
UpdateGameplay(deltaTime);
|
||||
}
|
||||
|
||||
// UI 始终更新(不检查暂停)
|
||||
UpdateUI(deltaTime);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Q4: 如何实现"慢动作"效果?
|
||||
|
||||
A: 暂停系统控制是否执行,时间缩放需要使用时间系统:
|
||||
|
||||
```csharp
|
||||
public class SlowMotionController : IController
|
||||
{
|
||||
private ITimeProvider _timeProvider;
|
||||
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public void EnableSlowMotion()
|
||||
{
|
||||
// 使用时间缩放而不是暂停
|
||||
_timeProvider.TimeScale = 0.3f;
|
||||
}
|
||||
|
||||
public void DisableSlowMotion()
|
||||
{
|
||||
_timeProvider.TimeScale = 1.0f;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Q5: 暂停管理器的性能如何?
|
||||
|
||||
A: 暂停管理器使用读写锁优化并发性能:
|
||||
|
||||
- 查询操作(`IsPaused`)使用读锁,支持并发
|
||||
- 修改操作(`Push/Pop`)使用写锁,互斥执行
|
||||
- 事件通知在锁外执行,避免死锁
|
||||
- 适合频繁查询、偶尔修改的场景
|
||||
|
||||
### Q6: 可以动态添加/移除暂停组吗?
|
||||
|
||||
A: 暂停组是枚举类型,不支持动态添加。可以使用自定义组:
|
||||
|
||||
```csharp
|
||||
// 使用预定义的自定义组
|
||||
_pauseManager.Push("特殊效果", PauseGroup.Custom1);
|
||||
_pauseManager.Push("天气系统", PauseGroup.Custom2);
|
||||
_pauseManager.Push("AI 系统", PauseGroup.Custom3);
|
||||
```
|
||||
|
||||
### Q7: 如何处理异步操作中的暂停?
|
||||
|
||||
A: 使用 `PauseScope` 配合 `async/await`:
|
||||
|
||||
```csharp
|
||||
public class AsyncPauseExample : IController
|
||||
{
|
||||
private IPauseStackManager _pauseManager;
|
||||
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public async Task ShowAsyncDialog()
|
||||
{
|
||||
using (_pauseManager.PauseScope("异步对话框"))
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
Console.WriteLine("对话框显示中...");
|
||||
await WaitForUserInput();
|
||||
}
|
||||
// 自动恢复
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 架构集成
|
||||
|
||||
### 在架构中注册
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : Architecture<GameArchitecture>
|
||||
{
|
||||
protected override void OnRegisterUtility()
|
||||
{
|
||||
// 注册暂停管理器
|
||||
RegisterUtility<IPauseStackManager>(new PauseStackManager());
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 注册默认处理器
|
||||
var pauseManager = GetUtility<IPauseStackManager>();
|
||||
|
||||
// Godot 处理器
|
||||
if (Engine.IsEditorHint() == false)
|
||||
{
|
||||
var tree = (GetTree() as SceneTree)!;
|
||||
pauseManager.RegisterHandler(new GodotPauseHandler(tree));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 与其他系统协同
|
||||
|
||||
```csharp
|
||||
// 与事件系统配合
|
||||
public class PauseEventBridge : AbstractSystem
|
||||
{
|
||||
protected override void OnInit()
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
|
||||
pauseManager.OnPauseStateChanged += (group, isPaused) =>
|
||||
{
|
||||
// 发送暂停事件
|
||||
this.SendEvent(new GamePausedEvent
|
||||
{
|
||||
Group = group,
|
||||
IsPaused = isPaused
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 与命令系统配合
|
||||
public class PauseCommand : AbstractCommand
|
||||
{
|
||||
private readonly string _reason;
|
||||
private readonly PauseGroup _group;
|
||||
|
||||
public PauseCommand(string reason, PauseGroup group = PauseGroup.Global)
|
||||
{
|
||||
_reason = reason;
|
||||
_group = group;
|
||||
}
|
||||
|
||||
protected override void OnExecute()
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
pauseManager.Push(_reason, _group);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 相关包
|
||||
|
||||
- [`architecture`](./architecture.md) - 架构核心,提供工具注册
|
||||
- [`utility`](./utility.md) - 工具基类
|
||||
- [`events`](./events.md) - 事件系统,用于状态通知
|
||||
- [`lifecycle`](./lifecycle.md) - 生命周期管理
|
||||
- [`logging`](./logging.md) - 日志系统,用于调试
|
||||
- [Godot 集成](../godot/index.md) - Godot 引擎集成
|
||||
507
docs/zh-CN/core/resource.md
Normal file
507
docs/zh-CN/core/resource.md
Normal file
@ -0,0 +1,507 @@
|
||||
---
|
||||
title: 资源管理系统
|
||||
description: 资源管理系统提供了统一的资源加载、缓存和卸载机制,支持引用计数和多种释放策略。
|
||||
---
|
||||
|
||||
# 资源管理系统
|
||||
|
||||
## 概述
|
||||
|
||||
资源管理系统是 GFramework 中用于管理游戏资源(如纹理、音频、模型等)的核心组件。它提供了统一的资源加载接口,自动缓存机制,以及灵活的资源释放策略,帮助你高效管理游戏资源的生命周期。
|
||||
|
||||
通过资源管理器,你可以避免重复加载相同资源,使用引用计数自动管理资源生命周期,并根据需求选择合适的释放策略。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 统一的资源加载接口(同步/异步)
|
||||
- 自动资源缓存和去重
|
||||
- 引用计数管理
|
||||
- 可插拔的资源加载器
|
||||
- 灵活的释放策略(手动/自动)
|
||||
- 线程安全操作
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 资源管理器
|
||||
|
||||
`ResourceManager` 是资源管理的核心类,负责加载、缓存和卸载资源:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.resource;
|
||||
|
||||
// 获取资源管理器(通常通过架构获取)
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
// 加载资源
|
||||
var texture = resourceManager.Load<Texture>("textures/player.png");
|
||||
```
|
||||
|
||||
### 资源句柄
|
||||
|
||||
`IResourceHandle<T>` 用于管理资源的引用计数,确保资源在使用期间不被释放:
|
||||
|
||||
```csharp
|
||||
// 获取资源句柄(自动增加引用计数)
|
||||
using var handle = resourceManager.GetHandle<Texture>("textures/player.png");
|
||||
|
||||
// 使用资源
|
||||
var texture = handle.Resource;
|
||||
|
||||
// 离开作用域时自动减少引用计数
|
||||
```
|
||||
|
||||
### 资源加载器
|
||||
|
||||
`IResourceLoader<T>` 定义了如何加载特定类型的资源:
|
||||
|
||||
```csharp
|
||||
public interface IResourceLoader<T> where T : class
|
||||
{
|
||||
T Load(string path);
|
||||
Task<T> LoadAsync(string path);
|
||||
void Unload(T resource);
|
||||
}
|
||||
```
|
||||
|
||||
### 释放策略
|
||||
|
||||
`IResourceReleaseStrategy` 决定何时释放资源:
|
||||
|
||||
- **手动释放**(`ManualReleaseStrategy`):引用计数为 0 时不自动释放,需要手动调用 `Unload`
|
||||
- **自动释放**(`AutoReleaseStrategy`):引用计数为 0 时自动释放资源
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 注册资源加载器
|
||||
|
||||
首先需要为每种资源类型注册加载器:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.resource;
|
||||
|
||||
// 实现纹理加载器
|
||||
public class TextureLoader : IResourceLoader<Texture>
|
||||
{
|
||||
public Texture Load(string path)
|
||||
{
|
||||
// 同步加载纹理
|
||||
return LoadTextureFromFile(path);
|
||||
}
|
||||
|
||||
public async Task<Texture> LoadAsync(string path)
|
||||
{
|
||||
// 异步加载纹理
|
||||
return await LoadTextureFromFileAsync(path);
|
||||
}
|
||||
|
||||
public void Unload(Texture resource)
|
||||
{
|
||||
// 释放纹理资源
|
||||
resource?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// 在架构中注册加载器
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void Init()
|
||||
{
|
||||
var resourceManager = new ResourceManager();
|
||||
resourceManager.RegisterLoader(new TextureLoader());
|
||||
RegisterUtility<IResourceManager>(resourceManager);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 同步加载资源
|
||||
|
||||
```csharp
|
||||
// 加载资源
|
||||
var texture = resourceManager.Load<Texture>("textures/player.png");
|
||||
|
||||
if (texture != null)
|
||||
{
|
||||
// 使用纹理
|
||||
sprite.Texture = texture;
|
||||
}
|
||||
```
|
||||
|
||||
### 异步加载资源
|
||||
|
||||
```csharp
|
||||
// 异步加载资源
|
||||
var texture = await resourceManager.LoadAsync<Texture>("textures/player.png");
|
||||
|
||||
if (texture != null)
|
||||
{
|
||||
sprite.Texture = texture;
|
||||
}
|
||||
```
|
||||
|
||||
### 使用资源句柄
|
||||
|
||||
```csharp
|
||||
public class PlayerController
|
||||
{
|
||||
private IResourceHandle<Texture>? _textureHandle;
|
||||
|
||||
public void LoadTexture()
|
||||
{
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
// 获取句柄(增加引用计数)
|
||||
_textureHandle = resourceManager.GetHandle<Texture>("textures/player.png");
|
||||
|
||||
if (_textureHandle?.Resource != null)
|
||||
{
|
||||
sprite.Texture = _textureHandle.Resource;
|
||||
}
|
||||
}
|
||||
|
||||
public void UnloadTexture()
|
||||
{
|
||||
// 释放句柄(减少引用计数)
|
||||
_textureHandle?.Dispose();
|
||||
_textureHandle = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 预加载资源
|
||||
|
||||
在游戏启动或场景切换时预加载资源:
|
||||
|
||||
```csharp
|
||||
public async Task PreloadGameAssets()
|
||||
{
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
// 预加载多个资源
|
||||
await Task.WhenAll(
|
||||
resourceManager.PreloadAsync<Texture>("textures/player.png"),
|
||||
resourceManager.PreloadAsync<Texture>("textures/enemy.png"),
|
||||
resourceManager.PreloadAsync<AudioClip>("audio/bgm.mp3")
|
||||
);
|
||||
|
||||
Console.WriteLine("资源预加载完成");
|
||||
}
|
||||
```
|
||||
|
||||
### 使用自动释放策略
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.resource;
|
||||
|
||||
// 设置自动释放策略
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
resourceManager.SetReleaseStrategy(new AutoReleaseStrategy());
|
||||
|
||||
// 使用资源句柄
|
||||
using (var handle = resourceManager.GetHandle<Texture>("textures/temp.png"))
|
||||
{
|
||||
// 使用资源
|
||||
var texture = handle.Resource;
|
||||
}
|
||||
// 离开作用域后,引用计数为 0,资源自动释放
|
||||
```
|
||||
|
||||
### 批量卸载资源
|
||||
|
||||
```csharp
|
||||
// 卸载特定资源
|
||||
resourceManager.Unload("textures/old_texture.png");
|
||||
|
||||
// 卸载所有资源
|
||||
resourceManager.UnloadAll();
|
||||
```
|
||||
|
||||
### 查询资源状态
|
||||
|
||||
```csharp
|
||||
// 检查资源是否已加载
|
||||
if (resourceManager.IsLoaded("textures/player.png"))
|
||||
{
|
||||
Console.WriteLine("资源已在缓存中");
|
||||
}
|
||||
|
||||
// 获取已加载资源数量
|
||||
Console.WriteLine($"已加载 {resourceManager.LoadedResourceCount} 个资源");
|
||||
|
||||
// 获取所有已加载资源的路径
|
||||
foreach (var path in resourceManager.GetLoadedResourcePaths())
|
||||
{
|
||||
Console.WriteLine($"已加载: {path}");
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义释放策略
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.resource;
|
||||
|
||||
// 实现基于时间的释放策略
|
||||
public class TimeBasedReleaseStrategy : IResourceReleaseStrategy
|
||||
{
|
||||
private readonly Dictionary<string, DateTime> _lastAccessTime = new();
|
||||
private readonly TimeSpan _timeout = TimeSpan.FromMinutes(5);
|
||||
|
||||
public bool ShouldRelease(string path, int refCount)
|
||||
{
|
||||
// 引用计数为 0 且超过 5 分钟未访问
|
||||
if (refCount > 0)
|
||||
return false;
|
||||
|
||||
if (!_lastAccessTime.TryGetValue(path, out var lastAccess))
|
||||
return false;
|
||||
|
||||
return DateTime.Now - lastAccess > _timeout;
|
||||
}
|
||||
|
||||
public void UpdateAccessTime(string path)
|
||||
{
|
||||
_lastAccessTime[path] = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用自定义策略
|
||||
resourceManager.SetReleaseStrategy(new TimeBasedReleaseStrategy());
|
||||
```
|
||||
|
||||
### 资源池模式
|
||||
|
||||
结合对象池实现资源复用:
|
||||
|
||||
```csharp
|
||||
public class BulletPool
|
||||
{
|
||||
private readonly IResourceManager _resourceManager;
|
||||
private readonly Queue<Bullet> _pool = new();
|
||||
private IResourceHandle<Texture>? _textureHandle;
|
||||
|
||||
public BulletPool(IResourceManager resourceManager)
|
||||
{
|
||||
_resourceManager = resourceManager;
|
||||
// 加载并持有纹理句柄
|
||||
_textureHandle = _resourceManager.GetHandle<Texture>("textures/bullet.png");
|
||||
}
|
||||
|
||||
public Bullet Get()
|
||||
{
|
||||
if (_pool.Count > 0)
|
||||
{
|
||||
return _pool.Dequeue();
|
||||
}
|
||||
|
||||
// 创建新子弹,使用缓存的纹理
|
||||
var bullet = new Bullet();
|
||||
bullet.Texture = _textureHandle?.Resource;
|
||||
return bullet;
|
||||
}
|
||||
|
||||
public void Return(Bullet bullet)
|
||||
{
|
||||
bullet.Reset();
|
||||
_pool.Enqueue(bullet);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// 释放纹理句柄
|
||||
_textureHandle?.Dispose();
|
||||
_textureHandle = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 资源依赖管理
|
||||
|
||||
```csharp
|
||||
public class MaterialLoader : IResourceLoader<Material>
|
||||
{
|
||||
private readonly IResourceManager _resourceManager;
|
||||
|
||||
public MaterialLoader(IResourceManager resourceManager)
|
||||
{
|
||||
_resourceManager = resourceManager;
|
||||
}
|
||||
|
||||
public Material Load(string path)
|
||||
{
|
||||
var material = new Material();
|
||||
|
||||
// 加载材质依赖的纹理
|
||||
material.DiffuseTexture = _resourceManager.Load<Texture>($"{path}/diffuse.png");
|
||||
material.NormalTexture = _resourceManager.Load<Texture>($"{path}/normal.png");
|
||||
|
||||
return material;
|
||||
}
|
||||
|
||||
public async Task<Material> LoadAsync(string path)
|
||||
{
|
||||
var material = new Material();
|
||||
|
||||
// 并行加载依赖资源
|
||||
var tasks = new[]
|
||||
{
|
||||
_resourceManager.LoadAsync<Texture>($"{path}/diffuse.png"),
|
||||
_resourceManager.LoadAsync<Texture>($"{path}/normal.png")
|
||||
};
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
material.DiffuseTexture = results[0];
|
||||
material.NormalTexture = results[1];
|
||||
|
||||
return material;
|
||||
}
|
||||
|
||||
public void Unload(Material resource)
|
||||
{
|
||||
// 材质卸载时,纹理由资源管理器自动管理
|
||||
resource?.Dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用资源句柄管理生命周期**:优先使用句柄而不是直接加载
|
||||
```csharp
|
||||
✓ using var handle = resourceManager.GetHandle<Texture>(path);
|
||||
✗ var texture = resourceManager.Load<Texture>(path); // 需要手动管理
|
||||
```
|
||||
|
||||
2. **选择合适的释放策略**:根据游戏需求选择策略
|
||||
- 手动释放:适合长期使用的资源(如 UI 纹理)
|
||||
- 自动释放:适合临时资源(如特效纹理)
|
||||
|
||||
3. **预加载关键资源**:避免游戏中途加载导致卡顿
|
||||
```csharp
|
||||
// 在场景加载时预加载
|
||||
await PreloadSceneAssets();
|
||||
```
|
||||
|
||||
4. **避免重复加载**:使用 `IsLoaded` 检查缓存
|
||||
```csharp
|
||||
if (!resourceManager.IsLoaded(path))
|
||||
{
|
||||
await resourceManager.LoadAsync<Texture>(path);
|
||||
}
|
||||
```
|
||||
|
||||
5. **及时释放不用的资源**:避免内存泄漏
|
||||
```csharp
|
||||
// 场景切换时卸载旧场景资源
|
||||
foreach (var path in oldSceneResources)
|
||||
{
|
||||
resourceManager.Unload(path);
|
||||
}
|
||||
```
|
||||
|
||||
6. **使用 using 语句管理句柄**:确保引用计数正确
|
||||
```csharp
|
||||
✓ using (var handle = resourceManager.GetHandle<Texture>(path))
|
||||
{
|
||||
// 使用资源
|
||||
} // 自动释放
|
||||
|
||||
✗ var handle = resourceManager.GetHandle<Texture>(path);
|
||||
// 忘记调用 Dispose()
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:资源加载失败怎么办?
|
||||
|
||||
**解答**:
|
||||
`Load` 和 `LoadAsync` 方法在失败时返回 `null`,应该检查返回值:
|
||||
|
||||
```csharp
|
||||
var texture = resourceManager.Load<Texture>(path);
|
||||
if (texture == null)
|
||||
{
|
||||
Logger.Error($"Failed to load texture: {path}");
|
||||
// 使用默认纹理
|
||||
texture = defaultTexture;
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何避免重复加载相同资源?
|
||||
|
||||
**解答**:
|
||||
资源管理器自动缓存已加载的资源,多次加载相同路径只会返回缓存的实例:
|
||||
|
||||
```csharp
|
||||
var texture1 = resourceManager.Load<Texture>("player.png");
|
||||
var texture2 = resourceManager.Load<Texture>("player.png");
|
||||
// texture1 和 texture2 是同一个实例
|
||||
```
|
||||
|
||||
### 问题:什么时候使用手动释放 vs 自动释放?
|
||||
|
||||
**解答**:
|
||||
|
||||
- **手动释放**:适合长期使用的资源,如 UI、角色模型
|
||||
- **自动释放**:适合临时资源,如特效、临时纹理
|
||||
|
||||
```csharp
|
||||
// 手动释放:UI 资源长期使用
|
||||
resourceManager.SetReleaseStrategy(new ManualReleaseStrategy());
|
||||
|
||||
// 自动释放:特效资源用完即释放
|
||||
resourceManager.SetReleaseStrategy(new AutoReleaseStrategy());
|
||||
```
|
||||
|
||||
### 问题:资源句柄的引用计数如何工作?
|
||||
|
||||
**解答**:
|
||||
|
||||
- `GetHandle` 增加引用计数
|
||||
- `Dispose` 减少引用计数
|
||||
- 引用计数为 0 时,根据释放策略决定是否卸载
|
||||
|
||||
```csharp
|
||||
// 引用计数: 0
|
||||
var handle1 = resourceManager.GetHandle<Texture>(path); // 引用计数: 1
|
||||
var handle2 = resourceManager.GetHandle<Texture>(path); // 引用计数: 2
|
||||
|
||||
handle1.Dispose(); // 引用计数: 1
|
||||
handle2.Dispose(); // 引用计数: 0(可能被释放)
|
||||
```
|
||||
|
||||
### 问题:如何实现资源热重载?
|
||||
|
||||
**解答**:
|
||||
卸载旧资源后重新加载:
|
||||
|
||||
```csharp
|
||||
public void ReloadResource(string path)
|
||||
{
|
||||
// 卸载旧资源
|
||||
resourceManager.Unload(path);
|
||||
|
||||
// 重新加载
|
||||
var newResource = resourceManager.Load<Texture>(path);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:资源管理器是线程安全的吗?
|
||||
|
||||
**解答**:
|
||||
是的,所有公共方法都是线程安全的,可以在多线程环境中使用:
|
||||
|
||||
```csharp
|
||||
// 在多个线程中并行加载
|
||||
Parallel.For(0, 10, i =>
|
||||
{
|
||||
var texture = resourceManager.Load<Texture>($"texture_{i}.png");
|
||||
});
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [对象池系统](/zh-CN/core/pool) - 结合对象池复用资源
|
||||
- [协程系统](/zh-CN/core/coroutine) - 异步加载资源
|
||||
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 引擎的资源管理
|
||||
- [资源管理最佳实践](/zh-CN/tutorials/resource-management) - 详细教程
|
||||
576
docs/zh-CN/core/state-machine.md
Normal file
576
docs/zh-CN/core/state-machine.md
Normal file
@ -0,0 +1,576 @@
|
||||
---
|
||||
title: 状态机系统
|
||||
description: 状态机系统提供了灵活的状态管理机制,支持状态转换、历史记录和异步操作。
|
||||
---
|
||||
|
||||
# 状态机系统
|
||||
|
||||
## 概述
|
||||
|
||||
状态机系统是 GFramework 中用于管理游戏状态的核心组件。通过状态机,你可以清晰地定义游戏的各种状态(如菜单、游戏中、暂停、游戏结束等),以及状态之间的转换规则,使游戏逻辑更加结构化和易于维护。
|
||||
|
||||
状态机系统支持同步和异步状态操作,提供状态历史记录,并与架构系统深度集成,让你可以在状态中访问所有架构组件。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 类型安全的状态管理
|
||||
- 支持同步和异步状态
|
||||
- 状态转换验证
|
||||
- 状态历史记录和回退
|
||||
- 与架构系统集成
|
||||
- 线程安全操作
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 状态接口
|
||||
|
||||
`IState` 定义了状态的基本行为:
|
||||
|
||||
```csharp
|
||||
public interface IState
|
||||
{
|
||||
void OnEnter(IState? from); // 进入状态
|
||||
void OnExit(IState? to); // 退出状态
|
||||
bool CanTransitionTo(IState target); // 转换验证
|
||||
}
|
||||
```
|
||||
|
||||
### 状态机
|
||||
|
||||
`IStateMachine` 管理状态的注册和切换:
|
||||
|
||||
```csharp
|
||||
public interface IStateMachine
|
||||
{
|
||||
IState? Current { get; } // 当前状态
|
||||
IStateMachine Register(IState state); // 注册状态
|
||||
Task<bool> ChangeToAsync<T>() where T : IState; // 切换状态
|
||||
}
|
||||
```
|
||||
|
||||
### 状态机系统
|
||||
|
||||
`IStateMachineSystem` 结合了状态机和系统的能力:
|
||||
|
||||
```csharp
|
||||
public interface IStateMachineSystem : ISystem, IStateMachine
|
||||
{
|
||||
// 继承 ISystem 和 IStateMachine 的所有功能
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 定义状态
|
||||
|
||||
继承 `ContextAwareStateBase` 创建状态:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.state;
|
||||
|
||||
// 菜单状态
|
||||
public class MenuState : ContextAwareStateBase
|
||||
{
|
||||
public override void OnEnter(IState? from)
|
||||
{
|
||||
Console.WriteLine("进入菜单");
|
||||
// 显示菜单 UI
|
||||
}
|
||||
|
||||
public override void OnExit(IState? to)
|
||||
{
|
||||
Console.WriteLine("退出菜单");
|
||||
// 隐藏菜单 UI
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏状态
|
||||
public class GameplayState : ContextAwareStateBase
|
||||
{
|
||||
public override void OnEnter(IState? from)
|
||||
{
|
||||
Console.WriteLine("开始游戏");
|
||||
// 初始化游戏场景
|
||||
}
|
||||
|
||||
public override void OnExit(IState? to)
|
||||
{
|
||||
Console.WriteLine("结束游戏");
|
||||
// 清理游戏场景
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册和使用状态机
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.state;
|
||||
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void Init()
|
||||
{
|
||||
// 创建状态机系统
|
||||
var stateMachine = new StateMachineSystem();
|
||||
|
||||
// 注册状态
|
||||
stateMachine
|
||||
.Register(new MenuState())
|
||||
.Register(new GameplayState())
|
||||
.Register(new PauseState());
|
||||
|
||||
// 注册到架构
|
||||
RegisterSystem<IStateMachineSystem>(stateMachine);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 切换状态
|
||||
|
||||
```csharp
|
||||
public class GameController : IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public async Task StartGame()
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
|
||||
// 切换到游戏状态
|
||||
var success = await stateMachine.ChangeToAsync<GameplayState>();
|
||||
|
||||
if (success)
|
||||
{
|
||||
Console.WriteLine("成功进入游戏状态");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 状态转换验证
|
||||
|
||||
控制状态之间的转换规则:
|
||||
|
||||
```csharp
|
||||
public class GameplayState : ContextAwareStateBase
|
||||
{
|
||||
public override bool CanTransitionTo(IState target)
|
||||
{
|
||||
// 只能从游戏状态转换到暂停或游戏结束状态
|
||||
return target is PauseState or GameOverState;
|
||||
}
|
||||
|
||||
public override void OnEnter(IState? from)
|
||||
{
|
||||
Console.WriteLine($"从 {from?.GetType().Name ?? "初始"} 进入游戏");
|
||||
}
|
||||
}
|
||||
|
||||
public class PauseState : ContextAwareStateBase
|
||||
{
|
||||
public override bool CanTransitionTo(IState target)
|
||||
{
|
||||
// 暂停状态只能返回游戏状态
|
||||
return target is GameplayState;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 异步状态
|
||||
|
||||
处理需要异步操作的状态:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.state;
|
||||
|
||||
public class LoadingState : AsyncContextAwareStateBase
|
||||
{
|
||||
public override async Task OnEnterAsync(IState? from)
|
||||
{
|
||||
Console.WriteLine("开始加载...");
|
||||
|
||||
// 异步加载资源
|
||||
await LoadResourcesAsync();
|
||||
|
||||
Console.WriteLine("加载完成");
|
||||
|
||||
// 自动切换到下一个状态
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
await stateMachine.ChangeToAsync<GameplayState>();
|
||||
}
|
||||
|
||||
private async Task LoadResourcesAsync()
|
||||
{
|
||||
// 模拟异步加载
|
||||
await Task.Delay(2000);
|
||||
}
|
||||
|
||||
public override async Task OnExitAsync(IState? to)
|
||||
{
|
||||
Console.WriteLine("退出加载状态");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 状态历史和回退
|
||||
|
||||
```csharp
|
||||
public class GameController : IController
|
||||
{
|
||||
public async Task NavigateBack()
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
|
||||
// 回退到上一个状态
|
||||
var success = await stateMachine.GoBackAsync();
|
||||
|
||||
if (success)
|
||||
{
|
||||
Console.WriteLine("已返回上一个状态");
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowHistory()
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
|
||||
// 获取状态历史
|
||||
var history = stateMachine.GetStateHistory();
|
||||
|
||||
Console.WriteLine("状态历史:");
|
||||
foreach (var state in history)
|
||||
{
|
||||
Console.WriteLine($"- {state.GetType().Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在状态中访问架构组件
|
||||
|
||||
```csharp
|
||||
public class GameplayState : ContextAwareStateBase
|
||||
{
|
||||
public override void OnEnter(IState? from)
|
||||
{
|
||||
// 访问 Model
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
playerModel.Reset();
|
||||
|
||||
// 访问 System
|
||||
var audioSystem = this.GetSystem<AudioSystem>();
|
||||
audioSystem.PlayBGM("gameplay");
|
||||
|
||||
// 发送事件
|
||||
this.SendEvent(new GameStartedEvent());
|
||||
}
|
||||
|
||||
public override void OnExit(IState? to)
|
||||
{
|
||||
// 停止音乐
|
||||
var audioSystem = this.GetSystem<AudioSystem>();
|
||||
audioSystem.StopBGM();
|
||||
|
||||
// 发送事件
|
||||
this.SendEvent(new GameEndedEvent());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 状态数据传递
|
||||
|
||||
```csharp
|
||||
// 定义带数据的状态
|
||||
public class GameplayState : ContextAwareStateBase
|
||||
{
|
||||
public int Level { get; set; }
|
||||
public string Difficulty { get; set; } = "Normal";
|
||||
|
||||
public override void OnEnter(IState? from)
|
||||
{
|
||||
Console.WriteLine($"开始关卡 {Level},难度: {Difficulty}");
|
||||
}
|
||||
}
|
||||
|
||||
// 切换状态并设置数据
|
||||
public async Task StartLevel(int level, string difficulty)
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
|
||||
// 获取状态实例并设置数据
|
||||
var gameplayState = stateMachine.GetState<GameplayState>();
|
||||
if (gameplayState != null)
|
||||
{
|
||||
gameplayState.Level = level;
|
||||
gameplayState.Difficulty = difficulty;
|
||||
}
|
||||
|
||||
// 切换状态
|
||||
await stateMachine.ChangeToAsync<GameplayState>();
|
||||
}
|
||||
```
|
||||
|
||||
### 状态事件通知
|
||||
|
||||
```csharp
|
||||
// 定义状态变更事件
|
||||
public class StateChangedEvent
|
||||
{
|
||||
public IState? From { get; set; }
|
||||
public IState To { get; set; }
|
||||
}
|
||||
|
||||
// 自定义状态机系统
|
||||
public class CustomStateMachineSystem : StateMachineSystem
|
||||
{
|
||||
protected override async Task OnStateChangedAsync(IState? from, IState to)
|
||||
{
|
||||
// 发送状态变更事件
|
||||
this.SendEvent(new StateChangedEvent
|
||||
{
|
||||
From = from,
|
||||
To = to
|
||||
});
|
||||
|
||||
await base.OnStateChangedAsync(from, to);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 条件状态转换
|
||||
|
||||
```csharp
|
||||
public class BattleState : ContextAwareStateBase
|
||||
{
|
||||
public override bool CanTransitionTo(IState target)
|
||||
{
|
||||
// 战斗中不能直接退出,必须先结束战斗
|
||||
if (target is MenuState)
|
||||
{
|
||||
var battleModel = this.GetModel<BattleModel>();
|
||||
return battleModel.IsBattleEnded;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试切换状态
|
||||
public async Task TryExitBattle()
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
|
||||
// 检查是否可以切换
|
||||
var canChange = await stateMachine.CanChangeToAsync<MenuState>();
|
||||
|
||||
if (canChange)
|
||||
{
|
||||
await stateMachine.ChangeToAsync<MenuState>();
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("战斗尚未结束,无法退出");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用基类创建状态**:继承 `ContextAwareStateBase` 或 `AsyncContextAwareStateBase`
|
||||
```csharp
|
||||
✓ public class MyState : ContextAwareStateBase { }
|
||||
✗ public class MyState : IState { } // 需要手动实现所有接口
|
||||
```
|
||||
|
||||
2. **在 OnEnter 中初始化,在 OnExit 中清理**:保持状态的独立性
|
||||
```csharp
|
||||
public override void OnEnter(IState? from)
|
||||
{
|
||||
// 初始化状态相关资源
|
||||
LoadUI();
|
||||
StartBackgroundMusic();
|
||||
}
|
||||
|
||||
public override void OnExit(IState? to)
|
||||
{
|
||||
// 清理状态相关资源
|
||||
UnloadUI();
|
||||
StopBackgroundMusic();
|
||||
}
|
||||
```
|
||||
|
||||
3. **使用转换验证控制状态流**:避免非法状态转换
|
||||
```csharp
|
||||
public override bool CanTransitionTo(IState target)
|
||||
{
|
||||
// 定义明确的转换规则
|
||||
return target is AllowedState1 or AllowedState2;
|
||||
}
|
||||
```
|
||||
|
||||
4. **异步操作使用异步状态**:避免阻塞主线程
|
||||
```csharp
|
||||
✓ public class LoadingState : AsyncContextAwareStateBase
|
||||
{
|
||||
public override async Task OnEnterAsync(IState? from)
|
||||
{
|
||||
await LoadDataAsync();
|
||||
}
|
||||
}
|
||||
|
||||
✗ public class LoadingState : ContextAwareStateBase
|
||||
{
|
||||
public override void OnEnter(IState? from)
|
||||
{
|
||||
LoadDataAsync().Wait(); // 阻塞主线程
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. **合理使用状态历史**:避免历史记录过大
|
||||
```csharp
|
||||
// 创建状态机时设置历史大小
|
||||
var stateMachine = new StateMachineSystem(maxHistorySize: 10);
|
||||
```
|
||||
|
||||
6. **状态保持单一职责**:每个状态只负责一个场景或功能
|
||||
```csharp
|
||||
✓ MenuState, GameplayState, PauseState // 职责清晰
|
||||
✗ GameState // 职责不明确,包含太多逻辑
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:状态切换失败怎么办?
|
||||
|
||||
**解答**:
|
||||
`ChangeToAsync` 返回 `false` 表示切换失败,通常是因为 `CanTransitionTo` 返回 `false`:
|
||||
|
||||
```csharp
|
||||
var success = await stateMachine.ChangeToAsync<TargetState>();
|
||||
if (!success)
|
||||
{
|
||||
Console.WriteLine("状态切换被拒绝");
|
||||
// 检查转换规则
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何在状态之间传递数据?
|
||||
|
||||
**解答**:
|
||||
有几种方式:
|
||||
|
||||
1. **通过状态属性**:
|
||||
|
||||
```csharp
|
||||
var state = stateMachine.GetState<GameplayState>();
|
||||
state.Level = 5;
|
||||
await stateMachine.ChangeToAsync<GameplayState>();
|
||||
```
|
||||
|
||||
2. **通过 Model**:
|
||||
|
||||
```csharp
|
||||
// 在切换前设置 Model
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
gameModel.CurrentLevel = 5;
|
||||
|
||||
// 在状态中读取
|
||||
public override void OnEnter(IState? from)
|
||||
{
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
var level = gameModel.CurrentLevel;
|
||||
}
|
||||
```
|
||||
|
||||
3. **通过事件**:
|
||||
|
||||
```csharp
|
||||
this.SendEvent(new LevelSelectedEvent { Level = 5 });
|
||||
await stateMachine.ChangeToAsync<GameplayState>();
|
||||
```
|
||||
|
||||
### 问题:状态机系统和普通状态机有什么区别?
|
||||
|
||||
**解答**:
|
||||
|
||||
- **StateMachine**:纯状态机,不依赖架构
|
||||
- **StateMachineSystem**:集成到架构中,状态可以访问所有架构组件
|
||||
|
||||
```csharp
|
||||
// 使用 StateMachineSystem(推荐)
|
||||
RegisterSystem<IStateMachineSystem>(new StateMachineSystem());
|
||||
|
||||
// 使用 StateMachine(独立使用)
|
||||
var stateMachine = new StateMachine();
|
||||
```
|
||||
|
||||
### 问题:如何处理状态切换动画?
|
||||
|
||||
**解答**:
|
||||
在 `OnExit` 和 `OnEnter` 中使用协程:
|
||||
|
||||
```csharp
|
||||
public class MenuState : AsyncContextAwareStateBase
|
||||
{
|
||||
public override async Task OnExitAsync(IState? to)
|
||||
{
|
||||
// 播放淡出动画
|
||||
await PlayFadeOutAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
public class GameplayState : AsyncContextAwareStateBase
|
||||
{
|
||||
public override async Task OnEnterAsync(IState? from)
|
||||
{
|
||||
// 播放淡入动画
|
||||
await PlayFadeInAnimation();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:可以在状态中切换到其他状态吗?
|
||||
|
||||
**解答**:
|
||||
可以,但要注意避免递归切换:
|
||||
|
||||
```csharp
|
||||
public override async void OnEnter(IState? from)
|
||||
{
|
||||
// 检查条件后自动切换
|
||||
if (ShouldSkip())
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
await stateMachine.ChangeToAsync<NextState>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:状态机是线程安全的吗?
|
||||
|
||||
**解答**:
|
||||
是的,状态机的所有操作都是线程安全的,使用了内部锁机制。
|
||||
|
||||
### 问题:如何实现状态栈(多层状态)?
|
||||
|
||||
**解答**:
|
||||
使用状态历史功能:
|
||||
|
||||
```csharp
|
||||
// 进入子状态
|
||||
await stateMachine.ChangeToAsync<SubMenuState>();
|
||||
|
||||
// 返回上一层
|
||||
await stateMachine.GoBackAsync();
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [生命周期管理](/zh-CN/core/lifecycle) - 状态的初始化和销毁
|
||||
- [事件系统](/zh-CN/core/events) - 状态变更通知
|
||||
- [协程系统](/zh-CN/core/coroutine) - 异步状态操作
|
||||
- [状态机实现教程](/zh-CN/tutorials/state-machine-tutorial) - 完整示例
|
||||
588
docs/zh-CN/game/data.md
Normal file
588
docs/zh-CN/game/data.md
Normal file
@ -0,0 +1,588 @@
|
||||
---
|
||||
title: 数据与存档系统
|
||||
description: 数据与存档系统提供了完整的数据持久化解决方案,支持多槽位存档、版本管理和数据迁移。
|
||||
---
|
||||
|
||||
# 数据与存档系统
|
||||
|
||||
## 概述
|
||||
|
||||
数据与存档系统是 GFramework.Game 中用于管理游戏数据持久化的核心组件。它提供了统一的数据加载和保存接口,支持多槽位存档管理、数据版本控制和自动迁移,让你可以轻松实现游戏存档、设置保存等功能。
|
||||
|
||||
通过数据系统,你可以将游戏数据保存到本地存储,支持多个存档槽位,并在数据结构变化时自动进行版本迁移。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 统一的数据持久化接口
|
||||
- 多槽位存档管理
|
||||
- 数据版本控制和迁移
|
||||
- 异步加载和保存
|
||||
- 批量数据操作
|
||||
- 与存储系统集成
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 数据接口
|
||||
|
||||
`IData` 标记数据类型:
|
||||
|
||||
```csharp
|
||||
public interface IData
|
||||
{
|
||||
// 标记接口,用于标识可持久化的数据
|
||||
}
|
||||
```
|
||||
|
||||
### 数据仓库
|
||||
|
||||
`IDataRepository` 提供通用的数据操作:
|
||||
|
||||
```csharp
|
||||
public interface IDataRepository : IUtility
|
||||
{
|
||||
Task<T> LoadAsync<T>(IDataLocation location) where T : class, IData, new();
|
||||
Task SaveAsync<T>(IDataLocation location, T data) where T : class, IData;
|
||||
Task<bool> ExistsAsync(IDataLocation location);
|
||||
Task DeleteAsync(IDataLocation location);
|
||||
Task SaveAllAsync(IEnumerable<(IDataLocation, IData)> dataList);
|
||||
}
|
||||
```
|
||||
|
||||
### 存档仓库
|
||||
|
||||
`ISaveRepository<T>` 专门用于管理游戏存档:
|
||||
|
||||
```csharp
|
||||
public interface ISaveRepository<TSaveData> : IUtility
|
||||
where TSaveData : class, IData, new()
|
||||
{
|
||||
Task<bool> ExistsAsync(int slot);
|
||||
Task<TSaveData> LoadAsync(int slot);
|
||||
Task SaveAsync(int slot, TSaveData data);
|
||||
Task DeleteAsync(int slot);
|
||||
Task<IReadOnlyList<int>> ListSlotsAsync();
|
||||
}
|
||||
```
|
||||
|
||||
### 版本化数据
|
||||
|
||||
`IVersionedData` 支持数据版本管理:
|
||||
|
||||
```csharp
|
||||
public interface IVersionedData : IData
|
||||
{
|
||||
int Version { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 定义数据类型
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.data;
|
||||
|
||||
// 简单数据
|
||||
public class PlayerData : IData
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Level { get; set; }
|
||||
public int Experience { get; set; }
|
||||
}
|
||||
|
||||
// 版本化数据
|
||||
public class SaveData : IVersionedData
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public PlayerData Player { get; set; }
|
||||
public DateTime SaveTime { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 使用存档仓库
|
||||
|
||||
```csharp
|
||||
public class SaveController : IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public async Task SaveGame(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
// 创建存档数据
|
||||
var saveData = new SaveData
|
||||
{
|
||||
Player = new PlayerData
|
||||
{
|
||||
Name = "Player1",
|
||||
Level = 10,
|
||||
Experience = 1000
|
||||
},
|
||||
SaveTime = DateTime.Now
|
||||
};
|
||||
|
||||
// 保存到指定槽位
|
||||
await saveRepo.SaveAsync(slot, saveData);
|
||||
Console.WriteLine($"游戏已保存到槽位 {slot}");
|
||||
}
|
||||
|
||||
public async Task LoadGame(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
// 检查存档是否存在
|
||||
if (!await saveRepo.ExistsAsync(slot))
|
||||
{
|
||||
Console.WriteLine($"槽位 {slot} 不存在存档");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载存档
|
||||
var saveData = await saveRepo.LoadAsync(slot);
|
||||
Console.WriteLine($"加载存档: {saveData.Player.Name}, 等级 {saveData.Player.Level}");
|
||||
}
|
||||
|
||||
public async Task DeleteSave(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
// 删除存档
|
||||
await saveRepo.DeleteAsync(slot);
|
||||
Console.WriteLine($"已删除槽位 {slot} 的存档");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册存档仓库
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.data;
|
||||
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void Init()
|
||||
{
|
||||
// 获取存储系统
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
// 创建存档配置
|
||||
var saveConfig = new SaveConfiguration
|
||||
{
|
||||
SaveRoot = "saves",
|
||||
SaveSlotPrefix = "slot_",
|
||||
SaveFileName = "save.json"
|
||||
};
|
||||
|
||||
// 注册存档仓库
|
||||
var saveRepo = new SaveRepository<SaveData>(storage, saveConfig);
|
||||
RegisterUtility<ISaveRepository<SaveData>>(saveRepo);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 列出所有存档
|
||||
|
||||
```csharp
|
||||
public async Task ShowSaveList()
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
// 获取所有存档槽位
|
||||
var slots = await saveRepo.ListSlotsAsync();
|
||||
|
||||
Console.WriteLine($"找到 {slots.Count} 个存档:");
|
||||
foreach (var slot in slots)
|
||||
{
|
||||
var saveData = await saveRepo.LoadAsync(slot);
|
||||
Console.WriteLine($"槽位 {slot}: {saveData.Player.Name}, " +
|
||||
$"等级 {saveData.Player.Level}, " +
|
||||
$"保存时间 {saveData.SaveTime}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自动保存
|
||||
|
||||
```csharp
|
||||
public class AutoSaveController : IController
|
||||
{
|
||||
private CancellationTokenSource? _autoSaveCts;
|
||||
|
||||
public void StartAutoSave(int slot, TimeSpan interval)
|
||||
{
|
||||
_autoSaveCts = new CancellationTokenSource();
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (!_autoSaveCts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(interval, _autoSaveCts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
await SaveGame(slot);
|
||||
Console.WriteLine("自动保存完成");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"自动保存失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}, _autoSaveCts.Token);
|
||||
}
|
||||
|
||||
public void StopAutoSave()
|
||||
{
|
||||
_autoSaveCts?.Cancel();
|
||||
_autoSaveCts?.Dispose();
|
||||
_autoSaveCts = null;
|
||||
}
|
||||
|
||||
private async Task SaveGame(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
var saveData = CreateSaveData();
|
||||
await saveRepo.SaveAsync(slot, saveData);
|
||||
}
|
||||
|
||||
private SaveData CreateSaveData()
|
||||
{
|
||||
// 从游戏状态创建存档数据
|
||||
return new SaveData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 数据版本迁移
|
||||
|
||||
```csharp
|
||||
// 版本 1 的数据
|
||||
public class SaveDataV1 : IVersionedData
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public string PlayerName { get; set; }
|
||||
public int Level { get; set; }
|
||||
}
|
||||
|
||||
// 版本 2 的数据(添加了新字段)
|
||||
public class SaveDataV2 : IVersionedData
|
||||
{
|
||||
public int Version { get; set; } = 2;
|
||||
public string PlayerName { get; set; }
|
||||
public int Level { get; set; }
|
||||
public int Experience { get; set; } // 新增字段
|
||||
public DateTime LastPlayTime { get; set; } // 新增字段
|
||||
}
|
||||
|
||||
// 数据迁移器
|
||||
public class SaveDataMigrator
|
||||
{
|
||||
public SaveDataV2 Migrate(SaveDataV1 oldData)
|
||||
{
|
||||
return new SaveDataV2
|
||||
{
|
||||
Version = 2,
|
||||
PlayerName = oldData.PlayerName,
|
||||
Level = oldData.Level,
|
||||
Experience = oldData.Level * 100, // 根据等级计算经验
|
||||
LastPlayTime = DateTime.Now
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 加载时自动迁移
|
||||
public async Task<SaveDataV2> LoadWithMigration(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveDataV2>>();
|
||||
var data = await saveRepo.LoadAsync(slot);
|
||||
|
||||
if (data.Version < 2)
|
||||
{
|
||||
// 需要迁移
|
||||
var oldData = data as SaveDataV1;
|
||||
var migrator = new SaveDataMigrator();
|
||||
var newData = migrator.Migrate(oldData);
|
||||
|
||||
// 保存迁移后的数据
|
||||
await saveRepo.SaveAsync(slot, newData);
|
||||
return newData;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
### 使用数据仓库
|
||||
|
||||
```csharp
|
||||
public class SettingsController : IController
|
||||
{
|
||||
public async Task SaveSettings()
|
||||
{
|
||||
var dataRepo = this.GetUtility<IDataRepository>();
|
||||
|
||||
var settings = new GameSettings
|
||||
{
|
||||
MasterVolume = 0.8f,
|
||||
MusicVolume = 0.6f,
|
||||
SfxVolume = 0.7f
|
||||
};
|
||||
|
||||
// 定义数据位置
|
||||
var location = new DataLocation("settings", "game_settings.json");
|
||||
|
||||
// 保存设置
|
||||
await dataRepo.SaveAsync(location, settings);
|
||||
}
|
||||
|
||||
public async Task<GameSettings> LoadSettings()
|
||||
{
|
||||
var dataRepo = this.GetUtility<IDataRepository>();
|
||||
var location = new DataLocation("settings", "game_settings.json");
|
||||
|
||||
// 检查是否存在
|
||||
if (!await dataRepo.ExistsAsync(location))
|
||||
{
|
||||
return new GameSettings(); // 返回默认设置
|
||||
}
|
||||
|
||||
// 加载设置
|
||||
return await dataRepo.LoadAsync<GameSettings>(location);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 批量保存数据
|
||||
|
||||
```csharp
|
||||
public async Task SaveAllGameData()
|
||||
{
|
||||
var dataRepo = this.GetUtility<IDataRepository>();
|
||||
|
||||
var dataList = new List<(IDataLocation, IData)>
|
||||
{
|
||||
(new DataLocation("player", "profile.json"), playerData),
|
||||
(new DataLocation("inventory", "items.json"), inventoryData),
|
||||
(new DataLocation("quests", "progress.json"), questData)
|
||||
};
|
||||
|
||||
// 批量保存
|
||||
await dataRepo.SaveAllAsync(dataList);
|
||||
Console.WriteLine("所有数据已保存");
|
||||
}
|
||||
```
|
||||
|
||||
### 存档备份
|
||||
|
||||
```csharp
|
||||
public async Task BackupSave(int slot)
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
if (!await saveRepo.ExistsAsync(slot))
|
||||
{
|
||||
Console.WriteLine("存档不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载原存档
|
||||
var saveData = await saveRepo.LoadAsync(slot);
|
||||
|
||||
// 保存到备份槽位
|
||||
int backupSlot = slot + 100;
|
||||
await saveRepo.SaveAsync(backupSlot, saveData);
|
||||
|
||||
Console.WriteLine($"存档已备份到槽位 {backupSlot}");
|
||||
}
|
||||
|
||||
public async Task RestoreBackup(int slot)
|
||||
{
|
||||
int backupSlot = slot + 100;
|
||||
var saveRepo = this.GetUtility<ISaveRepository<SaveData>>();
|
||||
|
||||
if (!await saveRepo.ExistsAsync(backupSlot))
|
||||
{
|
||||
Console.WriteLine("备份不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载备份
|
||||
var backupData = await saveRepo.LoadAsync(backupSlot);
|
||||
|
||||
// 恢复到原槽位
|
||||
await saveRepo.SaveAsync(slot, backupData);
|
||||
|
||||
Console.WriteLine($"已从备份恢复到槽位 {slot}");
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用版本化数据**:为存档数据实现 `IVersionedData`
|
||||
```csharp
|
||||
✓ public class SaveData : IVersionedData { public int Version { get; set; } = 1; }
|
||||
✗ public class SaveData : IData { } // 无法进行版本管理
|
||||
```
|
||||
|
||||
2. **定期自动保存**:避免玩家数据丢失
|
||||
```csharp
|
||||
// 每 5 分钟自动保存
|
||||
StartAutoSave(currentSlot, TimeSpan.FromMinutes(5));
|
||||
```
|
||||
|
||||
3. **保存前验证数据**:确保数据完整性
|
||||
```csharp
|
||||
public async Task SaveGame(int slot)
|
||||
{
|
||||
var saveData = CreateSaveData();
|
||||
|
||||
if (!ValidateSaveData(saveData))
|
||||
{
|
||||
throw new InvalidOperationException("存档数据无效");
|
||||
}
|
||||
|
||||
await saveRepo.SaveAsync(slot, saveData);
|
||||
}
|
||||
```
|
||||
|
||||
4. **处理保存失败**:使用 try-catch 捕获异常
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
await saveRepo.SaveAsync(slot, saveData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"保存失败: {ex.Message}");
|
||||
ShowErrorMessage("保存失败,请重试");
|
||||
}
|
||||
```
|
||||
|
||||
5. **提供多个存档槽位**:让玩家可以管理多个存档
|
||||
```csharp
|
||||
// 支持 10 个存档槽位
|
||||
for (int i = 1; i <= 10; i++)
|
||||
{
|
||||
if (await saveRepo.ExistsAsync(i))
|
||||
{
|
||||
ShowSaveSlot(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. **在关键时刻保存**:场景切换、关卡完成等
|
||||
```csharp
|
||||
public async Task OnLevelComplete()
|
||||
{
|
||||
// 关卡完成时自动保存
|
||||
await SaveGame(currentSlot);
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:如何实现多个存档槽位?
|
||||
|
||||
**解答**:
|
||||
使用 `ISaveRepository<T>` 的槽位参数:
|
||||
|
||||
```csharp
|
||||
// 保存到不同槽位
|
||||
await saveRepo.SaveAsync(1, saveData); // 槽位 1
|
||||
await saveRepo.SaveAsync(2, saveData); // 槽位 2
|
||||
await saveRepo.SaveAsync(3, saveData); // 槽位 3
|
||||
```
|
||||
|
||||
### 问题:如何处理数据版本升级?
|
||||
|
||||
**解答**:
|
||||
实现 `IVersionedData` 并在加载时检查版本:
|
||||
|
||||
```csharp
|
||||
var data = await saveRepo.LoadAsync(slot);
|
||||
if (data.Version < CurrentVersion)
|
||||
{
|
||||
data = MigrateData(data);
|
||||
await saveRepo.SaveAsync(slot, data);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:存档数据保存在哪里?
|
||||
|
||||
**解答**:
|
||||
由存储系统决定,通常在:
|
||||
|
||||
- Windows: `%AppData%/GameName/saves/`
|
||||
- Linux: `~/.local/share/GameName/saves/`
|
||||
- macOS: `~/Library/Application Support/GameName/saves/`
|
||||
|
||||
### 问题:如何实现云存档?
|
||||
|
||||
**解答**:
|
||||
实现自定义的 `IStorage`,将数据保存到云端:
|
||||
|
||||
```csharp
|
||||
public class CloudStorage : IStorage
|
||||
{
|
||||
public async Task WriteAsync(string path, byte[] data)
|
||||
{
|
||||
await UploadToCloud(path, data);
|
||||
}
|
||||
|
||||
public async Task<byte[]> ReadAsync(string path)
|
||||
{
|
||||
return await DownloadFromCloud(path);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何加密存档数据?
|
||||
|
||||
**解答**:
|
||||
在保存和加载时进行加密/解密:
|
||||
|
||||
```csharp
|
||||
public async Task SaveEncrypted(int slot, SaveData data)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(data);
|
||||
var encrypted = Encrypt(json);
|
||||
await storage.WriteAsync(path, encrypted);
|
||||
}
|
||||
|
||||
public async Task<SaveData> LoadEncrypted(int slot)
|
||||
{
|
||||
var encrypted = await storage.ReadAsync(path);
|
||||
var json = Decrypt(encrypted);
|
||||
return JsonSerializer.Deserialize<SaveData>(json);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:存档损坏怎么办?
|
||||
|
||||
**解答**:
|
||||
实现备份和恢复机制:
|
||||
|
||||
```csharp
|
||||
public async Task SaveWithBackup(int slot, SaveData data)
|
||||
{
|
||||
// 先备份旧存档
|
||||
if (await saveRepo.ExistsAsync(slot))
|
||||
{
|
||||
var oldData = await saveRepo.LoadAsync(slot);
|
||||
await saveRepo.SaveAsync(slot + 100, oldData);
|
||||
}
|
||||
|
||||
// 保存新存档
|
||||
await saveRepo.SaveAsync(slot, data);
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [设置系统](/zh-CN/game/setting) - 游戏设置管理
|
||||
- [场景系统](/zh-CN/game/scene) - 场景切换时保存
|
||||
- [存档系统实现教程](/zh-CN/tutorials/save-system) - 完整示例
|
||||
- [Godot 集成](/zh-CN/godot/index) - Godot 中的数据管理
|
||||
652
docs/zh-CN/game/scene.md
Normal file
652
docs/zh-CN/game/scene.md
Normal file
@ -0,0 +1,652 @@
|
||||
---
|
||||
title: 场景系统
|
||||
description: 场景系统提供了完整的场景生命周期管理、路由导航和转换控制功能。
|
||||
---
|
||||
|
||||
# 场景系统
|
||||
|
||||
## 概述
|
||||
|
||||
场景系统是 GFramework.Game 中用于管理游戏场景的核心组件。它提供了场景的加载、卸载、切换、暂停和恢复等完整生命周期管理,以及基于栈的场景导航机制。
|
||||
|
||||
通过场景系统,你可以轻松实现场景之间的平滑切换,管理场景栈(如主菜单 -> 游戏 -> 暂停菜单),并在场景转换时执行自定义逻辑。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 完整的场景生命周期管理
|
||||
- 基于栈的场景导航
|
||||
- 场景转换管道和钩子
|
||||
- 路由守卫(Route Guard)
|
||||
- 场景工厂和行为模式
|
||||
- 异步加载和卸载
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 场景接口
|
||||
|
||||
`IScene` 定义了场景的完整生命周期:
|
||||
|
||||
```csharp
|
||||
public interface IScene
|
||||
{
|
||||
ValueTask OnLoadAsync(ISceneEnterParam? param); // 加载资源
|
||||
ValueTask OnEnterAsync(); // 进入场景
|
||||
ValueTask OnPauseAsync(); // 暂停场景
|
||||
ValueTask OnResumeAsync(); // 恢复场景
|
||||
ValueTask OnExitAsync(); // 退出场景
|
||||
ValueTask OnUnloadAsync(); // 卸载资源
|
||||
}
|
||||
```
|
||||
|
||||
### 场景路由
|
||||
|
||||
`ISceneRouter` 管理场景的导航和切换:
|
||||
|
||||
```csharp
|
||||
public interface ISceneRouter : ISystem
|
||||
{
|
||||
ISceneBehavior? Current { get; } // 当前场景
|
||||
string? CurrentKey { get; } // 当前场景键
|
||||
IEnumerable<ISceneBehavior> Stack { get; } // 场景栈
|
||||
bool IsTransitioning { get; } // 是否正在切换
|
||||
|
||||
ValueTask ReplaceAsync(string sceneKey, ISceneEnterParam? param = null);
|
||||
ValueTask PushAsync(string sceneKey, ISceneEnterParam? param = null);
|
||||
ValueTask PopAsync();
|
||||
ValueTask ClearAsync();
|
||||
}
|
||||
```
|
||||
|
||||
### 场景行为
|
||||
|
||||
`ISceneBehavior` 封装了场景的具体实现和引擎集成:
|
||||
|
||||
```csharp
|
||||
public interface ISceneBehavior
|
||||
{
|
||||
string Key { get; } // 场景唯一标识
|
||||
IScene Scene { get; } // 场景实例
|
||||
ValueTask LoadAsync(ISceneEnterParam? param);
|
||||
ValueTask UnloadAsync();
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 定义场景
|
||||
|
||||
实现 `IScene` 接口创建场景:
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.scene;
|
||||
|
||||
public class MainMenuScene : IScene
|
||||
{
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
{
|
||||
// 加载场景资源
|
||||
Console.WriteLine("加载主菜单资源");
|
||||
await Task.Delay(100); // 模拟加载
|
||||
}
|
||||
|
||||
public async ValueTask OnEnterAsync()
|
||||
{
|
||||
// 进入场景
|
||||
Console.WriteLine("进入主菜单");
|
||||
// 显示 UI、播放音乐等
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnPauseAsync()
|
||||
{
|
||||
// 暂停场景
|
||||
Console.WriteLine("暂停主菜单");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnResumeAsync()
|
||||
{
|
||||
// 恢复场景
|
||||
Console.WriteLine("恢复主菜单");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnExitAsync()
|
||||
{
|
||||
// 退出场景
|
||||
Console.WriteLine("退出主菜单");
|
||||
// 隐藏 UI、停止音乐等
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnUnloadAsync()
|
||||
{
|
||||
// 卸载场景资源
|
||||
Console.WriteLine("卸载主菜单资源");
|
||||
await Task.Delay(50); // 模拟卸载
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册场景
|
||||
|
||||
在场景注册表中注册场景:
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.scene;
|
||||
|
||||
public class GameSceneRegistry : IGameSceneRegistry
|
||||
{
|
||||
private readonly Dictionary<string, Type> _scenes = new();
|
||||
|
||||
public GameSceneRegistry()
|
||||
{
|
||||
// 注册场景
|
||||
Register("MainMenu", typeof(MainMenuScene));
|
||||
Register("Gameplay", typeof(GameplayScene));
|
||||
Register("Pause", typeof(PauseScene));
|
||||
}
|
||||
|
||||
public void Register(string key, Type sceneType)
|
||||
{
|
||||
_scenes[key] = sceneType;
|
||||
}
|
||||
|
||||
public Type? GetSceneType(string key)
|
||||
{
|
||||
return _scenes.TryGetValue(key, out var type) ? type : null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 切换场景
|
||||
|
||||
使用场景路由进行导航:
|
||||
|
||||
```csharp
|
||||
public class GameController : IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public async Task StartGame()
|
||||
{
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
|
||||
// 替换当前场景(清空场景栈)
|
||||
await sceneRouter.ReplaceAsync("Gameplay");
|
||||
}
|
||||
|
||||
public async Task ShowPauseMenu()
|
||||
{
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
|
||||
// 压入新场景(保留当前场景)
|
||||
await sceneRouter.PushAsync("Pause");
|
||||
}
|
||||
|
||||
public async Task ClosePauseMenu()
|
||||
{
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
|
||||
// 弹出当前场景(恢复上一个场景)
|
||||
await sceneRouter.PopAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 场景参数传递
|
||||
|
||||
通过 `ISceneEnterParam` 传递数据:
|
||||
|
||||
```csharp
|
||||
// 定义场景参数
|
||||
public class GameplayEnterParam : ISceneEnterParam
|
||||
{
|
||||
public int Level { get; set; }
|
||||
public string Difficulty { get; set; }
|
||||
}
|
||||
|
||||
// 在场景中接收参数
|
||||
public class GameplayScene : IScene
|
||||
{
|
||||
private int _level;
|
||||
private string _difficulty;
|
||||
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
{
|
||||
if (param is GameplayEnterParam gameplayParam)
|
||||
{
|
||||
_level = gameplayParam.Level;
|
||||
_difficulty = gameplayParam.Difficulty;
|
||||
Console.WriteLine($"加载关卡 {_level},难度: {_difficulty}");
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ... 其他生命周期方法
|
||||
}
|
||||
|
||||
// 切换场景时传递参数
|
||||
await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam
|
||||
{
|
||||
Level = 1,
|
||||
Difficulty = "Normal"
|
||||
});
|
||||
```
|
||||
|
||||
### 路由守卫
|
||||
|
||||
使用路由守卫控制场景切换:
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.scene;
|
||||
|
||||
public class SaveGameGuard : ISceneRouteGuard
|
||||
{
|
||||
public async ValueTask<bool> CanLeaveAsync(
|
||||
ISceneBehavior from,
|
||||
string toKey,
|
||||
ISceneEnterParam? param)
|
||||
{
|
||||
// 离开游戏场景前检查是否需要保存
|
||||
if (from.Key == "Gameplay")
|
||||
{
|
||||
var needsSave = CheckIfNeedsSave();
|
||||
if (needsSave)
|
||||
{
|
||||
await SaveGameAsync();
|
||||
}
|
||||
}
|
||||
|
||||
return true; // 允许离开
|
||||
}
|
||||
|
||||
public async ValueTask<bool> CanEnterAsync(
|
||||
string toKey,
|
||||
ISceneEnterParam? param)
|
||||
{
|
||||
// 进入场景前的验证
|
||||
if (toKey == "Gameplay")
|
||||
{
|
||||
// 检查是否满足进入条件
|
||||
var canEnter = CheckGameplayRequirements();
|
||||
return canEnter;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool CheckIfNeedsSave() => true;
|
||||
private async Task SaveGameAsync() => await Task.Delay(100);
|
||||
private bool CheckGameplayRequirements() => true;
|
||||
}
|
||||
|
||||
// 注册守卫
|
||||
sceneRouter.AddGuard(new SaveGameGuard());
|
||||
```
|
||||
|
||||
### 场景转换处理器
|
||||
|
||||
自定义场景转换逻辑:
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.scene;
|
||||
|
||||
public class FadeTransitionHandler : ISceneTransitionHandler
|
||||
{
|
||||
public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
Console.WriteLine($"准备加载场景: {@event.ToKey}");
|
||||
// 显示加载画面
|
||||
await ShowLoadingScreen();
|
||||
}
|
||||
|
||||
public async ValueTask OnAfterLoadAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
Console.WriteLine($"场景加载完成: {@event.ToKey}");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnBeforeEnterAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
Console.WriteLine($"准备进入场景: {@event.ToKey}");
|
||||
// 播放淡入动画
|
||||
await PlayFadeIn();
|
||||
}
|
||||
|
||||
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
Console.WriteLine($"已进入场景: {@event.ToKey}");
|
||||
// 隐藏加载画面
|
||||
await HideLoadingScreen();
|
||||
}
|
||||
|
||||
public async ValueTask OnBeforeExitAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
Console.WriteLine($"准备退出场景: {@event.FromKey}");
|
||||
// 播放淡出动画
|
||||
await PlayFadeOut();
|
||||
}
|
||||
|
||||
public async ValueTask OnAfterExitAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
Console.WriteLine($"已退出场景: {@event.FromKey}");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task ShowLoadingScreen() => await Task.Delay(100);
|
||||
private async Task HideLoadingScreen() => await Task.Delay(100);
|
||||
private async Task PlayFadeIn() => await Task.Delay(200);
|
||||
private async Task PlayFadeOut() => await Task.Delay(200);
|
||||
}
|
||||
|
||||
// 注册转换处理器
|
||||
sceneRouter.AddTransitionHandler(new FadeTransitionHandler());
|
||||
```
|
||||
|
||||
### 场景栈管理
|
||||
|
||||
```csharp
|
||||
public class SceneNavigationController : IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public async Task NavigateToSettings()
|
||||
{
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
|
||||
// 检查场景是否已在栈中
|
||||
if (sceneRouter.Contains("Settings"))
|
||||
{
|
||||
Console.WriteLine("设置场景已打开");
|
||||
return;
|
||||
}
|
||||
|
||||
// 压入设置场景
|
||||
await sceneRouter.PushAsync("Settings");
|
||||
}
|
||||
|
||||
public void ShowSceneStack()
|
||||
{
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
|
||||
Console.WriteLine("当前场景栈:");
|
||||
foreach (var scene in sceneRouter.Stack)
|
||||
{
|
||||
Console.WriteLine($"- {scene.Key}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ReturnToMainMenu()
|
||||
{
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
|
||||
// 清空所有场景并加载主菜单
|
||||
await sceneRouter.ClearAsync();
|
||||
await sceneRouter.ReplaceAsync("MainMenu");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景加载进度
|
||||
|
||||
```csharp
|
||||
public class GameplayScene : IScene
|
||||
{
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
{
|
||||
var resourceManager = GetResourceManager();
|
||||
|
||||
// 加载多个资源并报告进度
|
||||
var resources = new[]
|
||||
{
|
||||
"textures/player.png",
|
||||
"textures/enemy.png",
|
||||
"audio/bgm.mp3",
|
||||
"models/level.obj"
|
||||
};
|
||||
|
||||
for (int i = 0; i < resources.Length; i++)
|
||||
{
|
||||
await resourceManager.LoadAsync<object>(resources[i]);
|
||||
|
||||
// 报告进度
|
||||
var progress = (i + 1) / (float)resources.Length;
|
||||
ReportProgress(progress);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReportProgress(float progress)
|
||||
{
|
||||
// 发送进度事件
|
||||
Console.WriteLine($"加载进度: {progress * 100:F0}%");
|
||||
}
|
||||
|
||||
// ... 其他生命周期方法
|
||||
}
|
||||
```
|
||||
|
||||
### 场景预加载
|
||||
|
||||
```csharp
|
||||
public class PreloadController : IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public async Task PreloadNextLevel()
|
||||
{
|
||||
var sceneFactory = this.GetUtility<ISceneFactory>();
|
||||
|
||||
// 预加载下一关场景
|
||||
var scene = sceneFactory.Create("Level2");
|
||||
await scene.OnLoadAsync(null);
|
||||
|
||||
Console.WriteLine("下一关预加载完成");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **在 OnLoad 中加载资源,在 OnUnload 中释放**:保持资源管理清晰
|
||||
```csharp
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
{
|
||||
_texture = await LoadTextureAsync("player.png");
|
||||
}
|
||||
|
||||
public async ValueTask OnUnloadAsync()
|
||||
{
|
||||
_texture?.Dispose();
|
||||
_texture = null;
|
||||
}
|
||||
```
|
||||
|
||||
2. **使用 Push/Pop 管理临时场景**:如暂停菜单、设置界面
|
||||
```csharp
|
||||
// 打开暂停菜单(保留游戏场景)
|
||||
await sceneRouter.PushAsync("Pause");
|
||||
|
||||
// 关闭暂停菜单(恢复游戏场景)
|
||||
await sceneRouter.PopAsync();
|
||||
```
|
||||
|
||||
3. **使用 Replace 切换主要场景**:如从菜单到游戏
|
||||
```csharp
|
||||
// 开始游戏(清空场景栈)
|
||||
await sceneRouter.ReplaceAsync("Gameplay");
|
||||
```
|
||||
|
||||
4. **在 OnPause/OnResume 中管理状态**:暂停和恢复游戏逻辑
|
||||
```csharp
|
||||
public async ValueTask OnPauseAsync()
|
||||
{
|
||||
// 暂停游戏逻辑
|
||||
_gameTimer.Pause();
|
||||
_audioSystem.PauseBGM();
|
||||
}
|
||||
|
||||
public async ValueTask OnResumeAsync()
|
||||
{
|
||||
// 恢复游戏逻辑
|
||||
_gameTimer.Resume();
|
||||
_audioSystem.ResumeBGM();
|
||||
}
|
||||
```
|
||||
|
||||
5. **使用路由守卫处理业务逻辑**:如保存检查、权限验证
|
||||
```csharp
|
||||
public async ValueTask<bool> CanLeaveAsync(...)
|
||||
{
|
||||
if (HasUnsavedChanges())
|
||||
{
|
||||
var confirmed = await ShowSaveDialog();
|
||||
if (confirmed)
|
||||
{
|
||||
await SaveAsync();
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
6. **避免在场景切换时阻塞**:使用异步操作
|
||||
```csharp
|
||||
✓ await sceneRouter.ReplaceAsync("Gameplay");
|
||||
✗ sceneRouter.ReplaceAsync("Gameplay").Wait(); // 可能死锁
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:Replace、Push、Pop 有什么区别?
|
||||
|
||||
**解答**:
|
||||
|
||||
- **Replace**:清空场景栈,加载新场景(用于主要场景切换)
|
||||
- **Push**:压入新场景,暂停当前场景(用于临时场景)
|
||||
- **Pop**:弹出当前场景,恢复上一个场景(用于关闭临时场景)
|
||||
|
||||
```csharp
|
||||
// 场景栈示例
|
||||
await sceneRouter.ReplaceAsync("MainMenu"); // [MainMenu]
|
||||
await sceneRouter.PushAsync("Settings"); // [MainMenu, Settings]
|
||||
await sceneRouter.PushAsync("About"); // [MainMenu, Settings, About]
|
||||
await sceneRouter.PopAsync(); // [MainMenu, Settings]
|
||||
await sceneRouter.PopAsync(); // [MainMenu]
|
||||
```
|
||||
|
||||
### 问题:如何在场景之间传递数据?
|
||||
|
||||
**解答**:
|
||||
有几种方式:
|
||||
|
||||
1. **通过场景参数**:
|
||||
|
||||
```csharp
|
||||
await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam
|
||||
{
|
||||
Level = 5
|
||||
});
|
||||
```
|
||||
|
||||
2. **通过 Model**:
|
||||
|
||||
```csharp
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
gameModel.CurrentLevel = 5;
|
||||
await sceneRouter.ReplaceAsync("Gameplay");
|
||||
```
|
||||
|
||||
3. **通过事件**:
|
||||
|
||||
```csharp
|
||||
this.SendEvent(new LevelSelectedEvent { Level = 5 });
|
||||
await sceneRouter.ReplaceAsync("Gameplay");
|
||||
```
|
||||
|
||||
### 问题:场景切换时如何显示加载画面?
|
||||
|
||||
**解答**:
|
||||
使用场景转换处理器:
|
||||
|
||||
```csharp
|
||||
public class LoadingScreenHandler : ISceneTransitionHandler
|
||||
{
|
||||
public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
await ShowLoadingScreen();
|
||||
}
|
||||
|
||||
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
await HideLoadingScreen();
|
||||
}
|
||||
|
||||
// ... 其他方法
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何防止用户在场景切换时操作?
|
||||
|
||||
**解答**:
|
||||
检查 `IsTransitioning` 状态:
|
||||
|
||||
```csharp
|
||||
public async Task ChangeScene(string sceneKey)
|
||||
{
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
|
||||
if (sceneRouter.IsTransitioning)
|
||||
{
|
||||
Console.WriteLine("场景正在切换中,请稍候");
|
||||
return;
|
||||
}
|
||||
|
||||
await sceneRouter.ReplaceAsync(sceneKey);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:场景切换失败怎么办?
|
||||
|
||||
**解答**:
|
||||
使用 try-catch 捕获异常:
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
await sceneRouter.ReplaceAsync("Gameplay");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"场景切换失败: {ex.Message}");
|
||||
// 回退到安全场景
|
||||
await sceneRouter.ReplaceAsync("MainMenu");
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何实现场景预加载?
|
||||
|
||||
**解答**:
|
||||
在后台预先加载场景资源:
|
||||
|
||||
```csharp
|
||||
// 在当前场景中预加载下一个场景
|
||||
var factory = this.GetUtility<ISceneFactory>();
|
||||
var nextScene = factory.Create("NextLevel");
|
||||
await nextScene.OnLoadAsync(null);
|
||||
|
||||
// 稍后快速切换
|
||||
await sceneRouter.ReplaceAsync("NextLevel");
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [UI 系统](/zh-CN/game/ui) - UI 页面管理
|
||||
- [资源管理系统](/zh-CN/core/resource) - 场景资源加载
|
||||
- [状态机系统](/zh-CN/core/state-machine) - 场景状态管理
|
||||
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 引擎集成
|
||||
- [存档系统实现教程](/zh-CN/tutorials/save-system) - 场景切换时保存数据
|
||||
756
docs/zh-CN/game/serialization.md
Normal file
756
docs/zh-CN/game/serialization.md
Normal file
@ -0,0 +1,756 @@
|
||||
---
|
||||
title: 序列化系统
|
||||
description: 序列化系统提供了统一的对象序列化和反序列化接口,支持 JSON 格式和运行时类型处理。
|
||||
---
|
||||
|
||||
# 序列化系统
|
||||
|
||||
## 概述
|
||||
|
||||
序列化系统是 GFramework.Game 中用于对象序列化和反序列化的核心组件。它提供了统一的序列化接口,支持将对象转换为字符串格式(如
|
||||
JSON)进行存储或传输,并能够将字符串数据还原为对象。
|
||||
|
||||
序列化系统与数据存储、配置管理、存档系统等模块深度集成,为游戏数据的持久化提供了基础支持。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 统一的序列化接口
|
||||
- JSON 格式支持
|
||||
- 运行时类型序列化
|
||||
- 泛型和非泛型 API
|
||||
- 与存储系统无缝集成
|
||||
- 类型安全的反序列化
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 序列化器接口
|
||||
|
||||
`ISerializer` 定义了基本的序列化操作:
|
||||
|
||||
```csharp
|
||||
public interface ISerializer : IUtility
|
||||
{
|
||||
// 将对象序列化为字符串
|
||||
string Serialize<T>(T value);
|
||||
|
||||
// 将字符串反序列化为对象
|
||||
T Deserialize<T>(string data);
|
||||
}
|
||||
```
|
||||
|
||||
### 运行时类型序列化器
|
||||
|
||||
`IRuntimeTypeSerializer` 扩展了基本接口,支持运行时类型处理:
|
||||
|
||||
```csharp
|
||||
public interface IRuntimeTypeSerializer : ISerializer
|
||||
{
|
||||
// 使用运行时类型序列化对象
|
||||
string Serialize(object obj, Type type);
|
||||
|
||||
// 使用运行时类型反序列化对象
|
||||
object Deserialize(string data, Type type);
|
||||
}
|
||||
```
|
||||
|
||||
### JSON 序列化器
|
||||
|
||||
`JsonSerializer` 是基于 Newtonsoft.Json 的实现:
|
||||
|
||||
```csharp
|
||||
public sealed class JsonSerializer : IRuntimeTypeSerializer
|
||||
{
|
||||
string Serialize<T>(T value);
|
||||
T Deserialize<T>(string data);
|
||||
string Serialize(object obj, Type type);
|
||||
object Deserialize(string data, Type type);
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 注册序列化器
|
||||
|
||||
在架构中注册序列化器:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.serializer;
|
||||
using GFramework.Game.serializer;
|
||||
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void Init()
|
||||
{
|
||||
// 注册 JSON 序列化器
|
||||
var jsonSerializer = new JsonSerializer();
|
||||
RegisterUtility<ISerializer>(jsonSerializer);
|
||||
RegisterUtility<IRuntimeTypeSerializer>(jsonSerializer);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 序列化对象
|
||||
|
||||
使用泛型 API 序列化对象:
|
||||
|
||||
```csharp
|
||||
public class PlayerData
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Level { get; set; }
|
||||
public int Experience { get; set; }
|
||||
}
|
||||
|
||||
public class SaveController : IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public void SavePlayer()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
|
||||
var player = new PlayerData
|
||||
{
|
||||
Name = "Player1",
|
||||
Level = 10,
|
||||
Experience = 1000
|
||||
};
|
||||
|
||||
// 序列化为 JSON 字符串
|
||||
string json = serializer.Serialize(player);
|
||||
Console.WriteLine(json);
|
||||
// 输出: {"Name":"Player1","Level":10,"Experience":1000}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 反序列化对象
|
||||
|
||||
从字符串还原对象:
|
||||
|
||||
```csharp
|
||||
public void LoadPlayer()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
|
||||
string json = "{\"Name\":\"Player1\",\"Level\":10,\"Experience\":1000}";
|
||||
|
||||
// 反序列化为对象
|
||||
var player = serializer.Deserialize<PlayerData>(json);
|
||||
|
||||
Console.WriteLine($"玩家: {player.Name}, 等级: {player.Level}");
|
||||
}
|
||||
```
|
||||
|
||||
### 运行时类型序列化
|
||||
|
||||
处理不确定类型的对象:
|
||||
|
||||
```csharp
|
||||
public void SerializeRuntimeType()
|
||||
{
|
||||
var serializer = this.GetUtility<IRuntimeTypeSerializer>();
|
||||
|
||||
object data = new PlayerData { Name = "Player1", Level = 10 };
|
||||
Type dataType = data.GetType();
|
||||
|
||||
// 使用运行时类型序列化
|
||||
string json = serializer.Serialize(data, dataType);
|
||||
|
||||
// 使用运行时类型反序列化
|
||||
object restored = serializer.Deserialize(json, dataType);
|
||||
|
||||
var player = restored as PlayerData;
|
||||
Console.WriteLine($"玩家: {player?.Name}");
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 与存储系统集成
|
||||
|
||||
序列化器与存储系统配合使用:
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.storage;
|
||||
using GFramework.Game.storage;
|
||||
|
||||
public class DataManager : IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public async Task SaveData()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
var gameData = new GameData
|
||||
{
|
||||
Score = 1000,
|
||||
Coins = 500
|
||||
};
|
||||
|
||||
// 序列化数据
|
||||
string json = serializer.Serialize(gameData);
|
||||
|
||||
// 写入存储
|
||||
await storage.WriteAsync("game_data", json);
|
||||
}
|
||||
|
||||
public async Task<GameData> LoadData()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
// 从存储读取
|
||||
string json = await storage.ReadAsync<string>("game_data");
|
||||
|
||||
// 反序列化数据
|
||||
return serializer.Deserialize<GameData>(json);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 序列化复杂对象
|
||||
|
||||
处理嵌套和集合类型:
|
||||
|
||||
```csharp
|
||||
public class InventoryData
|
||||
{
|
||||
public List<ItemData> Items { get; set; }
|
||||
public Dictionary<string, int> Resources { get; set; }
|
||||
}
|
||||
|
||||
public class ItemData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int Quantity { get; set; }
|
||||
}
|
||||
|
||||
public void SerializeComplexData()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
|
||||
var inventory = new InventoryData
|
||||
{
|
||||
Items = new List<ItemData>
|
||||
{
|
||||
new ItemData { Id = "sword_01", Name = "铁剑", Quantity = 1 },
|
||||
new ItemData { Id = "potion_hp", Name = "生命药水", Quantity = 5 }
|
||||
},
|
||||
Resources = new Dictionary<string, int>
|
||||
{
|
||||
{ "gold", 1000 },
|
||||
{ "wood", 500 }
|
||||
}
|
||||
};
|
||||
|
||||
// 序列化复杂对象
|
||||
string json = serializer.Serialize(inventory);
|
||||
|
||||
// 反序列化
|
||||
var restored = serializer.Deserialize<InventoryData>(json);
|
||||
|
||||
Console.WriteLine($"物品数量: {restored.Items.Count}");
|
||||
Console.WriteLine($"金币: {restored.Resources["gold"]}");
|
||||
}
|
||||
```
|
||||
|
||||
### 处理多态类型
|
||||
|
||||
序列化继承层次结构:
|
||||
|
||||
```csharp
|
||||
public abstract class EntityData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Type { get; set; }
|
||||
}
|
||||
|
||||
public class PlayerEntityData : EntityData
|
||||
{
|
||||
public int Level { get; set; }
|
||||
public int Experience { get; set; }
|
||||
}
|
||||
|
||||
public class EnemyEntityData : EntityData
|
||||
{
|
||||
public int Health { get; set; }
|
||||
public int Damage { get; set; }
|
||||
}
|
||||
|
||||
public void SerializePolymorphic()
|
||||
{
|
||||
var serializer = this.GetUtility<IRuntimeTypeSerializer>();
|
||||
|
||||
// 创建不同类型的实体
|
||||
EntityData player = new PlayerEntityData
|
||||
{
|
||||
Id = "player_1",
|
||||
Type = "Player",
|
||||
Level = 10,
|
||||
Experience = 1000
|
||||
};
|
||||
|
||||
EntityData enemy = new EnemyEntityData
|
||||
{
|
||||
Id = "enemy_1",
|
||||
Type = "Enemy",
|
||||
Health = 100,
|
||||
Damage = 20
|
||||
};
|
||||
|
||||
// 使用运行时类型序列化
|
||||
string playerJson = serializer.Serialize(player, player.GetType());
|
||||
string enemyJson = serializer.Serialize(enemy, enemy.GetType());
|
||||
|
||||
// 根据类型反序列化
|
||||
var restoredPlayer = serializer.Deserialize(playerJson, typeof(PlayerEntityData));
|
||||
var restoredEnemy = serializer.Deserialize(enemyJson, typeof(EnemyEntityData));
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义序列化逻辑
|
||||
|
||||
虽然 GFramework 使用 Newtonsoft.Json,但你可以通过特性控制序列化行为:
|
||||
|
||||
```csharp
|
||||
using Newtonsoft.Json;
|
||||
|
||||
public class CustomData
|
||||
{
|
||||
// 忽略此属性
|
||||
[JsonIgnore]
|
||||
public string InternalId { get; set; }
|
||||
|
||||
// 使用不同的属性名
|
||||
[JsonProperty("player_name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
// 仅在值不为 null 时序列化
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string? OptionalField { get; set; }
|
||||
|
||||
// 格式化日期
|
||||
[JsonProperty("created_at")]
|
||||
[JsonConverter(typeof(IsoDateTimeConverter))]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 批量序列化
|
||||
|
||||
处理多个对象的序列化:
|
||||
|
||||
```csharp
|
||||
public async Task SaveMultipleData()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
var dataList = new Dictionary<string, object>
|
||||
{
|
||||
{ "player", new PlayerData { Name = "Player1", Level = 10 } },
|
||||
{ "inventory", new InventoryData { Items = new List<ItemData>() } },
|
||||
{ "settings", new SettingsData { Volume = 0.8f } }
|
||||
};
|
||||
|
||||
// 批量序列化和保存
|
||||
foreach (var (key, data) in dataList)
|
||||
{
|
||||
string json = serializer.Serialize(data);
|
||||
await storage.WriteAsync(key, json);
|
||||
}
|
||||
|
||||
Console.WriteLine($"已保存 {dataList.Count} 个数据文件");
|
||||
}
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
处理序列化和反序列化错误:
|
||||
|
||||
```csharp
|
||||
public void SafeDeserialize()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
|
||||
string json = "{\"Name\":\"Player1\",\"Level\":\"invalid\"}"; // 错误的数据
|
||||
|
||||
try
|
||||
{
|
||||
var player = serializer.Deserialize<PlayerData>(json);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
Console.WriteLine($"反序列化失败: {ex.Message}");
|
||||
// 返回默认值或重新尝试
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Console.WriteLine($"JSON 格式错误: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerData DeserializeWithFallback(string json)
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
|
||||
try
|
||||
{
|
||||
return serializer.Deserialize<PlayerData>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 返回默认数据
|
||||
return new PlayerData
|
||||
{
|
||||
Name = "DefaultPlayer",
|
||||
Level = 1,
|
||||
Experience = 0
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 版本兼容性
|
||||
|
||||
处理数据结构变化:
|
||||
|
||||
```csharp
|
||||
// 旧版本数据
|
||||
public class PlayerDataV1
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Level { get; set; }
|
||||
}
|
||||
|
||||
// 新版本数据(添加了新字段)
|
||||
public class PlayerDataV2
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Level { get; set; }
|
||||
public int Experience { get; set; } = 0; // 新增字段,提供默认值
|
||||
public DateTime LastLogin { get; set; } = DateTime.Now; // 新增字段
|
||||
}
|
||||
|
||||
public PlayerDataV2 LoadWithMigration(string json)
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
|
||||
try
|
||||
{
|
||||
// 尝试加载新版本
|
||||
return serializer.Deserialize<PlayerDataV2>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 如果失败,尝试加载旧版本并迁移
|
||||
var oldData = serializer.Deserialize<PlayerDataV1>(json);
|
||||
return new PlayerDataV2
|
||||
{
|
||||
Name = oldData.Name,
|
||||
Level = oldData.Level,
|
||||
Experience = oldData.Level * 100, // 根据等级计算经验
|
||||
LastLogin = DateTime.Now
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用接口而非具体类型**:依赖 `ISerializer` 接口
|
||||
```csharp
|
||||
✓ var serializer = this.GetUtility<ISerializer>();
|
||||
✗ var serializer = new JsonSerializer(); // 避免直接实例化
|
||||
```
|
||||
|
||||
2. **为数据类提供默认值**:确保反序列化的健壮性
|
||||
```csharp
|
||||
public class GameData
|
||||
{
|
||||
public string Name { get; set; } = "Default";
|
||||
public int Score { get; set; } = 0;
|
||||
public List<string> Items { get; set; } = new();
|
||||
}
|
||||
```
|
||||
|
||||
3. **处理反序列化异常**:避免程序崩溃
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
var data = serializer.Deserialize<GameData>(json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"反序列化失败: {ex.Message}");
|
||||
return GetDefaultData();
|
||||
}
|
||||
```
|
||||
|
||||
4. **避免序列化敏感数据**:使用 `[JsonIgnore]` 标记
|
||||
```csharp
|
||||
public class UserData
|
||||
{
|
||||
public string Username { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string Password { get; set; } // 不序列化密码
|
||||
}
|
||||
```
|
||||
|
||||
5. **使用运行时类型处理多态**:保持类型信息
|
||||
```csharp
|
||||
var serializer = this.GetUtility<IRuntimeTypeSerializer>();
|
||||
string json = serializer.Serialize(obj, obj.GetType());
|
||||
```
|
||||
|
||||
6. **验证反序列化的数据**:确保数据完整性
|
||||
```csharp
|
||||
var data = serializer.Deserialize<GameData>(json);
|
||||
if (string.IsNullOrEmpty(data.Name) || data.Score < 0)
|
||||
{
|
||||
throw new InvalidDataException("数据验证失败");
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 减少序列化开销
|
||||
|
||||
```csharp
|
||||
// 避免频繁序列化大对象
|
||||
public class CachedSerializer
|
||||
{
|
||||
private string? _cachedJson;
|
||||
private GameData? _cachedData;
|
||||
|
||||
public string GetJson(GameData data)
|
||||
{
|
||||
if (_cachedData == data && _cachedJson != null)
|
||||
{
|
||||
return _cachedJson;
|
||||
}
|
||||
|
||||
var serializer = GetSerializer();
|
||||
_cachedJson = serializer.Serialize(data);
|
||||
_cachedData = data;
|
||||
return _cachedJson;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 异步序列化
|
||||
|
||||
```csharp
|
||||
public async Task SaveDataAsync()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
var data = GetLargeData();
|
||||
|
||||
// 在后台线程序列化
|
||||
string json = await Task.Run(() => serializer.Serialize(data));
|
||||
|
||||
// 异步写入存储
|
||||
await storage.WriteAsync("large_data", json);
|
||||
}
|
||||
```
|
||||
|
||||
### 分块序列化
|
||||
|
||||
```csharp
|
||||
public async Task SaveLargeDataset()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
var largeDataset = GetLargeDataset();
|
||||
|
||||
// 分块保存
|
||||
const int chunkSize = 100;
|
||||
for (int i = 0; i < largeDataset.Count; i += chunkSize)
|
||||
{
|
||||
var chunk = largeDataset.Skip(i).Take(chunkSize).ToList();
|
||||
string json = serializer.Serialize(chunk);
|
||||
await storage.WriteAsync($"data_chunk_{i / chunkSize}", json);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:如何序列化循环引用的对象?
|
||||
|
||||
**解答**:
|
||||
Newtonsoft.Json 默认不支持循环引用,需要配置:
|
||||
|
||||
```csharp
|
||||
// 注意:GFramework 的 JsonSerializer 使用默认设置
|
||||
// 如需处理循环引用,避免创建循环引用的数据结构
|
||||
// 或使用 [JsonIgnore] 打破循环
|
||||
|
||||
public class Node
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public List<Node> Children { get; set; }
|
||||
|
||||
[JsonIgnore] // 忽略父节点引用,避免循环
|
||||
public Node? Parent { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:序列化后的 JSON 太大怎么办?
|
||||
|
||||
**解答**:
|
||||
使用压缩或分块存储:
|
||||
|
||||
```csharp
|
||||
public async Task SaveCompressed()
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
var data = GetLargeData();
|
||||
string json = serializer.Serialize(data);
|
||||
|
||||
// 压缩 JSON
|
||||
byte[] compressed = Compress(json);
|
||||
|
||||
// 保存压缩数据
|
||||
await storage.WriteAsync("data_compressed", compressed);
|
||||
}
|
||||
|
||||
private byte[] Compress(string text)
|
||||
{
|
||||
using var output = new MemoryStream();
|
||||
using (var gzip = new GZipStream(output, CompressionMode.Compress))
|
||||
using (var writer = new StreamWriter(gzip))
|
||||
{
|
||||
writer.Write(text);
|
||||
}
|
||||
return output.ToArray();
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何处理不同平台的序列化差异?
|
||||
|
||||
**解答**:
|
||||
使用平台无关的数据类型:
|
||||
|
||||
```csharp
|
||||
public class CrossPlatformData
|
||||
{
|
||||
// 使用 string 而非 DateTime(避免时区问题)
|
||||
public string CreatedAt { get; set; } = DateTime.UtcNow.ToString("O");
|
||||
|
||||
// 使用 double 而非 float(精度一致)
|
||||
public double Score { get; set; }
|
||||
|
||||
// 明确指定编码
|
||||
public string Text { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:反序列化失败时如何恢复?
|
||||
|
||||
**解答**:
|
||||
实现备份和恢复机制:
|
||||
|
||||
```csharp
|
||||
public async Task<GameData> LoadWithBackup(string key)
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
try
|
||||
{
|
||||
// 尝试加载主数据
|
||||
string json = await storage.ReadAsync<string>(key);
|
||||
return serializer.Deserialize<GameData>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 尝试加载备份
|
||||
try
|
||||
{
|
||||
string backupJson = await storage.ReadAsync<string>($"{key}_backup");
|
||||
return serializer.Deserialize<GameData>(backupJson);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 返回默认数据
|
||||
return new GameData();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何加密序列化的数据?
|
||||
|
||||
**解答**:
|
||||
在序列化后加密:
|
||||
|
||||
```csharp
|
||||
public async Task SaveEncrypted(string key, GameData data)
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
// 序列化
|
||||
string json = serializer.Serialize(data);
|
||||
|
||||
// 加密
|
||||
byte[] encrypted = EncryptString(json);
|
||||
|
||||
// 保存
|
||||
await storage.WriteAsync(key, encrypted);
|
||||
}
|
||||
|
||||
public async Task<GameData> LoadEncrypted(string key)
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var storage = this.GetUtility<IStorage>();
|
||||
|
||||
// 读取
|
||||
byte[] encrypted = await storage.ReadAsync<byte[]>(key);
|
||||
|
||||
// 解密
|
||||
string json = DecryptToString(encrypted);
|
||||
|
||||
// 反序列化
|
||||
return serializer.Deserialize<GameData>(json);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:序列化器是线程安全的吗?
|
||||
|
||||
**解答**:
|
||||
`JsonSerializer` 本身是线程安全的,但建议通过架构的 Utility 系统访问:
|
||||
|
||||
```csharp
|
||||
// 线程安全的访问方式
|
||||
public async Task ParallelSave()
|
||||
{
|
||||
var tasks = Enumerable.Range(0, 10).Select(async i =>
|
||||
{
|
||||
var serializer = this.GetUtility<ISerializer>();
|
||||
var data = new GameData { Score = i };
|
||||
string json = serializer.Serialize(data);
|
||||
await SaveToStorage($"data_{i}", json);
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [数据与存档系统](/zh-CN/game/data) - 数据持久化
|
||||
- [存储系统](/zh-CN/game/storage) - 文件存储
|
||||
- [设置系统](/zh-CN/game/setting) - 设置数据序列化
|
||||
- [Utility 系统](/zh-CN/core/utility) - 工具类注册
|
||||
735
docs/zh-CN/game/storage.md
Normal file
735
docs/zh-CN/game/storage.md
Normal file
@ -0,0 +1,735 @@
|
||||
---
|
||||
title: 存储系统详解
|
||||
description: 存储系统提供了灵活的文件存储和作用域隔离功能,支持跨平台数据持久化。
|
||||
---
|
||||
|
||||
# 存储系统详解
|
||||
|
||||
## 概述
|
||||
|
||||
存储系统是 GFramework.Game 中用于管理文件存储的核心组件。它提供了统一的存储接口,支持键值对存储、作用域隔离、目录操作等功能,让你可以轻松实现游戏数据的持久化。
|
||||
|
||||
存储系统采用装饰器模式设计,通过 `IStorage` 接口定义统一的存储操作,`FileStorage` 提供基于文件系统的实现,`ScopedStorage`
|
||||
提供作用域隔离功能。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 统一的键值对存储接口
|
||||
- 基于文件系统的持久化
|
||||
- 作用域隔离和命名空间管理
|
||||
- 线程安全的并发访问
|
||||
- 支持同步和异步操作
|
||||
- 目录和文件列举功能
|
||||
- 路径安全防护
|
||||
- 跨平台支持(包括 Godot)
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 存储接口
|
||||
|
||||
`IStorage` 定义了统一的存储操作:
|
||||
|
||||
```csharp
|
||||
public interface IStorage : IUtility
|
||||
{
|
||||
// 检查键是否存在
|
||||
bool Exists(string key);
|
||||
Task<bool> ExistsAsync(string key);
|
||||
|
||||
// 读取数据
|
||||
T Read<T>(string key);
|
||||
T Read<T>(string key, T defaultValue);
|
||||
Task<T> ReadAsync<T>(string key);
|
||||
|
||||
// 写入数据
|
||||
void Write<T>(string key, T value);
|
||||
Task WriteAsync<T>(string key, T value);
|
||||
|
||||
// 删除数据
|
||||
void Delete(string key);
|
||||
Task DeleteAsync(string key);
|
||||
|
||||
// 目录操作
|
||||
Task<IReadOnlyList<string>> ListDirectoriesAsync(string path = "");
|
||||
Task<IReadOnlyList<string>> ListFilesAsync(string path = "");
|
||||
Task<bool> DirectoryExistsAsync(string path);
|
||||
Task CreateDirectoryAsync(string path);
|
||||
}
|
||||
```
|
||||
|
||||
### 文件存储
|
||||
|
||||
`FileStorage` 是基于文件系统的存储实现:
|
||||
|
||||
- 将数据序列化后保存为文件
|
||||
- 支持自定义文件扩展名(默认 `.dat`)
|
||||
- 使用细粒度锁保证线程安全
|
||||
- 自动创建目录结构
|
||||
- 防止路径遍历攻击
|
||||
|
||||
### 作用域存储
|
||||
|
||||
`ScopedStorage` 提供命名空间隔离:
|
||||
|
||||
- 为所有键添加前缀
|
||||
- 支持嵌套作用域
|
||||
- 透明包装底层存储
|
||||
- 实现逻辑分组
|
||||
|
||||
### 存储类型
|
||||
|
||||
`StorageKinds` 枚举定义了不同的存储方式:
|
||||
|
||||
```csharp
|
||||
[Flags]
|
||||
public enum StorageKinds
|
||||
{
|
||||
None = 0,
|
||||
Local = 1 << 0, // 本地文件系统
|
||||
Memory = 1 << 1, // 内存存储
|
||||
Remote = 1 << 2, // 远程存储
|
||||
Database = 1 << 3 // 数据库存储
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 创建文件存储
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.storage;
|
||||
using GFramework.Game.serializer;
|
||||
|
||||
// 创建序列化器
|
||||
var serializer = new JsonSerializer();
|
||||
|
||||
// 创Windows 示例)
|
||||
var storage = new FileStorage(@"C:\MyGame\Data", serializer);
|
||||
|
||||
// 或使用自定义扩展名
|
||||
var storage = new FileStorage(@"C:\MyGame\Data", serializer, ".json");
|
||||
```
|
||||
|
||||
### 写入和读取数据
|
||||
|
||||
```csharp
|
||||
// 写入简单类型
|
||||
storage.Write("player_score", 1000);
|
||||
storage.Write("player_name", "Alice");
|
||||
|
||||
// 写入复杂对象
|
||||
var settings = new GameSettings
|
||||
{
|
||||
Volume = 0.8f,
|
||||
Difficulty = "Hard",
|
||||
Language = "zh-CN"
|
||||
};
|
||||
storage.Write("settings", settings);
|
||||
|
||||
// 读取数据
|
||||
int score = storage.Read<int>("player_score");
|
||||
string name = storage.Read<string>("player_name");
|
||||
var loadedSettings = storage.Read<GameSettings>("settings");
|
||||
|
||||
// 读取数据(带默认值)
|
||||
int highScore = storage.Read("high_score", 0);
|
||||
```
|
||||
|
||||
### 异步操作
|
||||
|
||||
```csharp
|
||||
// 异步写入
|
||||
await storage.WriteAsync("player_level", 10);
|
||||
|
||||
// 异步读取
|
||||
int level = await storage.ReadAsync<int>("player_level");
|
||||
|
||||
// 异步检查存在
|
||||
bool exists = await storage.ExistsAsync("player_level");
|
||||
|
||||
// 异步删除
|
||||
await storage.DeleteAsync("player_level");
|
||||
```
|
||||
|
||||
### 检查和删除
|
||||
|
||||
```csharp
|
||||
// 检查键是否存在
|
||||
if (storage.Exists("player_score"))
|
||||
{
|
||||
Console.WriteLine("存档存在");
|
||||
}
|
||||
|
||||
// 删除数据
|
||||
storage.Delete("player_score");
|
||||
|
||||
// 异步检查
|
||||
bool exists = await storage.ExistsAsync("player_score");
|
||||
```
|
||||
|
||||
### 使用层级键
|
||||
|
||||
```csharp
|
||||
// 使用 / 分隔符创建层级结构
|
||||
storage.Write("player/profile/name", "Alice");
|
||||
storage.Write("player/profile/level", 10);
|
||||
storage.Write("player/inventory/gold", 1000);
|
||||
|
||||
// 文件结构:
|
||||
// Data/
|
||||
// player/
|
||||
// profile/
|
||||
// name.dat
|
||||
// level.dat
|
||||
// inventory/
|
||||
// gold.dat
|
||||
|
||||
// 读取层级数据
|
||||
string name = storage.Read<string>("player/profile/name");
|
||||
int gold = storage.Read<int>("player/inventory/gold");
|
||||
```
|
||||
|
||||
## 作用域存储
|
||||
|
||||
### 创建作用域存储
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.storage;
|
||||
|
||||
// 基于文件存储创建作用域存储
|
||||
var baseStorage = new FileStorage(@"C:\MyGame\Data", serializer);
|
||||
var playerStorage = new ScopedStorage(baseStorage, "player");
|
||||
|
||||
// 所有操作都会添加 "player/" 前缀
|
||||
playerStorage.Write("name", "Alice"); // 实际存储为 "player/name.dat"
|
||||
playerStorage.Write("level", 10); // 实际存储为 "player/level.dat"
|
||||
|
||||
// 读取时也使用相同的前缀
|
||||
string name = playerStorage.Read<string>("name"); // 从 "player/name.dat" 读取
|
||||
```
|
||||
|
||||
### 嵌套作用域
|
||||
|
||||
```csharp
|
||||
// 创建嵌套作用域
|
||||
var settingsStorage = new ScopedStorage(baseStorage, "settings");
|
||||
var graphicsStorage = new ScopedStorage(settingsStorage, "graphics");
|
||||
|
||||
// 前缀变为 "settings/graphics/"
|
||||
graphicsStorage.Write("resolution", "1920x1080");
|
||||
// 实际存储为 "settings/graphics/resolution.dat"
|
||||
|
||||
// 或使用 Scope 方法
|
||||
var audioStorage = settingsStorage.Scope("audio");
|
||||
audioStorage.Write("volume", 0.8f);
|
||||
// 实际存储为 "settings/audio/volume.dat"
|
||||
```
|
||||
|
||||
### 多作用域隔离
|
||||
|
||||
```csharp
|
||||
// 创建不同作用域的存储
|
||||
var playerStorage = new ScopedStorage(baseStorage, "player");
|
||||
var gameStorage = new ScopedStorage(baseStorage, "game");
|
||||
var settingsStorage = new ScopedStorage(baseStorage, "settings");
|
||||
|
||||
// 在不同作用域中使用相同的键不会冲突
|
||||
playerStorage.Write("level", 5); // player/level.dat
|
||||
gameStorage.Write("level", "forest_area_1"); // game/level.dat
|
||||
settingsStorage.Write("level", "high"); // settings/level.dat
|
||||
|
||||
// 读取时各自独立
|
||||
int playerLevel = playerStorage.Read<int>("level"); // 5
|
||||
string gameLevel = gameStorage.Read<string>("level"); // "forest_area_1"
|
||||
string settingsLevel = settingsStorage.Read<string>("level"); // "high"
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 目录操作
|
||||
|
||||
```csharp
|
||||
// 列举子目录
|
||||
var directories = await storage.ListDirectoriesAsync("player");
|
||||
foreach (var dir in directories)
|
||||
{
|
||||
Console.WriteLine($"目录: {dir}");
|
||||
}
|
||||
|
||||
// 列举文件
|
||||
var files = await storage.ListFilesAsync("player/inventory");
|
||||
foreach (var file in files)
|
||||
{
|
||||
Console.WriteLine($"文件: {file}");
|
||||
}
|
||||
|
||||
// 检查目录是否存在
|
||||
bool exists = await storage.DirectoryExistsAsync("player/quests");
|
||||
|
||||
// 创建目录
|
||||
await storage.CreateDirectoryAsync("player/achievements");
|
||||
```
|
||||
|
||||
### 批量操作
|
||||
|
||||
```csharp
|
||||
public async Task SaveAllPlayerData(PlayerData player)
|
||||
{
|
||||
var playerStorage = new ScopedStorage(baseStorage, $"player_{player.Id}");
|
||||
|
||||
// 批量写入
|
||||
var tasks = new List<Task>
|
||||
{
|
||||
playerStorage.WriteAsync("profile", player.Profile),
|
||||
playerStorage.WriteAsync("inventory", player.Inventory),
|
||||
playerStorage.WriteAsync("quests", player.Quests),
|
||||
playerStorage.WriteAsync("achievements", player.Achievements)
|
||||
};
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
Console.WriteLine("所有玩家数据已保存");
|
||||
}
|
||||
|
||||
public async Task<PlayerData> LoadAllPlayerData(int playerId)
|
||||
{
|
||||
var playerStorage = new ScopedStorage(baseStorage, $"player_{playerId}");
|
||||
|
||||
// 批量读取
|
||||
var tasks = new[]
|
||||
{
|
||||
playerStorage.ReadAsync<Profile>("profile"),
|
||||
playerStorage.ReadAsync<Inventory>("inventory"),
|
||||
playerStorage.ReadAsync<QuestData>("quests"),
|
||||
playerStorage.ReadAsync<Achievements>("achievements")
|
||||
};
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
return new PlayerData
|
||||
{
|
||||
Id = playerId,
|
||||
Profile = tasks[0].Result,
|
||||
Inventory = tasks[1].Result,
|
||||
Quests = tasks[2].Result,
|
||||
Achievements = tasks[3].Result
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 存储迁移
|
||||
|
||||
```csharp
|
||||
public async Task MigrateStorage(IStorage oldStorage, IStorage newStorage, string path = "")
|
||||
{
|
||||
// 列举所有文件
|
||||
var files = await oldStorage.ListFilesAsync(path);
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var key = string.IsNullOrEmpty(path) ? file : $"{path}/{file}";
|
||||
|
||||
// 读取旧数据
|
||||
var data = await oldStorage.ReadAsync<object>(key);
|
||||
|
||||
// 写入新存储
|
||||
await newStorage.WriteAsync(key, data);
|
||||
|
||||
Console.WriteLine($"已迁移: {key}");
|
||||
}
|
||||
|
||||
// 递归处理子目录
|
||||
var directories = await oldStorage.ListDirectoriesAsync(path);
|
||||
foreach (var dir in directories)
|
||||
{
|
||||
var subPath = string.IsNullOrEmpty(path) ? dir : $"{path}/{dir}";
|
||||
await MigrateStorage(oldStorage, newStorage, subPath);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 存储备份
|
||||
|
||||
```csharp
|
||||
public class StorageBackupSystem
|
||||
{
|
||||
private readonly IStorage _storage;
|
||||
private readonly string _backupPrefix = "backup";
|
||||
|
||||
public async Task CreateBackup(string sourcePath)
|
||||
{
|
||||
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
var backupPath = $"{_backupPrefix}/{timestamp}";
|
||||
|
||||
await CopyDirectory(sourcePath, backupPath);
|
||||
Console.WriteLine($"备份已创建: {backupPath}");
|
||||
}
|
||||
|
||||
public async Task RestoreBackup(string backupName, string targetPath)
|
||||
{
|
||||
var backupPath = $"{_backupPrefix}/{backupName}";
|
||||
|
||||
if (!await _storage.DirectoryExistsAsync(backupPath))
|
||||
{
|
||||
throw new DirectoryNotFoundException($"备份不存在: {backupName}");
|
||||
}
|
||||
|
||||
await CopyDirectory(backupPath, targetPath);
|
||||
Console.WriteLine($"已从备份恢复: {backupName}");
|
||||
}
|
||||
|
||||
private async Task CopyDirectory(string source, string target)
|
||||
{
|
||||
var files = await _storage.ListFilesAsync(source);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var sourceKey = $"{source}/{file}";
|
||||
var targetKey = $"{target}/{file}";
|
||||
var data = await _storage.ReadAsync<object>(sourceKey);
|
||||
await _storage.WriteAsync(targetKey, data);
|
||||
}
|
||||
|
||||
var directories = await _storage.ListDirectoriesAsync(source);
|
||||
foreach (var dir in directories)
|
||||
{
|
||||
await CopyDirectory($"{source}/{dir}", $"{target}/{dir}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 缓存层
|
||||
|
||||
```csharp
|
||||
public class CachedStorage : IStorage
|
||||
{
|
||||
private readonly IStorage _innerStorage;
|
||||
private readonly ConcurrentDictionary<string, object> _cache = new();
|
||||
|
||||
public CachedStorage(IStorage innerStorage)
|
||||
{
|
||||
_innerStorage = innerStorage;
|
||||
}
|
||||
|
||||
public T Read<T>(string key)
|
||||
{
|
||||
// 先从缓存读取
|
||||
if (_cache.TryGetValue(key, out var cached))
|
||||
{
|
||||
return (T)cached;
|
||||
}
|
||||
|
||||
// 从存储读取并缓存
|
||||
var value = _innerStorage.Read<T>(key);
|
||||
_cache[key] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
public void Write<T>(string key, T value)
|
||||
{
|
||||
// 写入存储
|
||||
_innerStorage.Write(key, value);
|
||||
|
||||
// 更新缓存
|
||||
_cache[key] = value;
|
||||
}
|
||||
|
||||
public void Delete(string key)
|
||||
{
|
||||
_innerStorage.Delete(key);
|
||||
_cache.TryRemove(key, out _);
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
{
|
||||
_cache.Clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Godot 集成
|
||||
|
||||
### 使用 Godot 文件存储
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.storage;
|
||||
|
||||
// 创建 Godot 文件存储
|
||||
var storage = new GodotFileStorage(serializer);
|
||||
|
||||
// 使用 user:// 路径(用户数据目录)
|
||||
storage.Write("user://saves/slot1.dat", saveData);
|
||||
var data = storage.Read<SaveData>("user://saves/slot1.dat");
|
||||
|
||||
// 使用 res:// 路径(资源目录,只读)
|
||||
var config = storage.Read<Config>("res://config/default.json");
|
||||
|
||||
// 普通文件路径也支持
|
||||
storage.Write("/tmp/temp_data.dat", tempData);
|
||||
```
|
||||
|
||||
### Godot 路径说明
|
||||
|
||||
```csharp
|
||||
// user:// - 用户数据目录
|
||||
// Windows: %APPDATA%/Godot/app_userdata/[project_name]
|
||||
// Linux: ~/.local/share/godot/app_userdata/[project_name]
|
||||
// macOS: ~/Library/Application Support/Godot/app_userdata/[project_name]
|
||||
storage.Write("user://save.dat", data);
|
||||
|
||||
// res:// - 项目资源目录(只读)
|
||||
var config = storage.Read<Config>("res://data/config.json");
|
||||
|
||||
// 绝对路径
|
||||
storage.Write("/home/user/game/data.dat", data);
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用作用域隔离不同类型的数据**
|
||||
```csharp
|
||||
✓ var playerStorage = new ScopedStorage(baseStorage, "player");
|
||||
✓ var settingsStorage = new ScopedStorage(baseStorage, "settings");
|
||||
✗ storage.Write("player_name", name); // 不使用作用域
|
||||
```
|
||||
|
||||
2. **使用异步操作避免阻塞**
|
||||
```csharp
|
||||
✓ await storage.WriteAsync("data", value);
|
||||
✗ storage.Write("data", value); // 在 UI 线程中同步操作
|
||||
```
|
||||
|
||||
3. **读取时提供默认值**
|
||||
```csharp
|
||||
✓ int score = storage.Read("score", 0);
|
||||
✗ int score = storage.Read<int>("score"); // 键不存在时抛异常
|
||||
```
|
||||
|
||||
4. **使用层级键组织数据**
|
||||
```csharp
|
||||
✓ storage.Write("player/inventory/gold", 1000);
|
||||
✗ storage.Write("player_inventory_gold", 1000);
|
||||
```
|
||||
|
||||
5. **处理存储异常**
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
await storage.WriteAsync("data", value);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.Error($"存储失败: {ex.Message}");
|
||||
ShowErrorMessage("保存失败,请检查磁盘空间");
|
||||
}
|
||||
```
|
||||
|
||||
6. **定期清理过期数据**
|
||||
```csharp
|
||||
public async Task CleanupOldData(TimeSpan maxAge)
|
||||
{
|
||||
var files = await storage.ListFilesAsync("temp");
|
||||
foreach (var file in files)
|
||||
{
|
||||
var data = await storage.ReadAsync<TimestampedData>($"temp/{file}");
|
||||
if (DateTime.Now - data.Timestamp > maxAge)
|
||||
{
|
||||
await storage.DeleteAsync($"temp/{file}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
7. **使用合适的序列化器**
|
||||
```csharp
|
||||
// JSON - 可读性好,适合配置文件
|
||||
var jsonStorage = new FileStorage(path, new JsonSerializer(), ".json");
|
||||
|
||||
// 二进制 - 性能好,适合大量数据
|
||||
var binaryStorage = new FileStorage(path, new BinarySerializer(), ".dat");
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:如何实现跨平台存储路径?
|
||||
|
||||
**解答**:
|
||||
使用 `Environment.GetFolderPath` 获取平台特定路径:
|
||||
|
||||
```csharp
|
||||
public static string GetStoragePath()
|
||||
{
|
||||
var appData = Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.ApplicationData);
|
||||
return Path.Combine(appData, "MyGame", "Data");
|
||||
}
|
||||
|
||||
var storage = new FileStorage(GetStoragePath(), serializer);
|
||||
```
|
||||
|
||||
### 问题:存储系统是否线程安全?
|
||||
|
||||
**解答**:
|
||||
是的,`FileStorage` 使用细粒度锁机制保证线程安全:
|
||||
|
||||
```csharp
|
||||
// 不同键的操作可以并发执行
|
||||
Task.Run(() => storage.Write("key1", value1));
|
||||
Task.Run(() => storage.Write("key2", value2));
|
||||
|
||||
// 相同键的操作会串行化
|
||||
Task.Run(() => storage.Write("key", value1));
|
||||
Task.Run(() => storage.Write("key", value2)); // 等待第一个完成
|
||||
```
|
||||
|
||||
### 问题:如何实现存储加密?
|
||||
|
||||
**解答**:
|
||||
创建加密存储包装器:
|
||||
|
||||
```csharp
|
||||
public class EncryptedStorage : IStorage
|
||||
{
|
||||
private readonly IStorage _innerStorage;
|
||||
private readonly IEncryption _encryption;
|
||||
|
||||
public void Write<T>(string key, T value)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(value);
|
||||
var encrypted = _encryption.Encrypt(json);
|
||||
_innerStorage.Write(key, encrypted);
|
||||
}
|
||||
|
||||
public T Read<T>(string key)
|
||||
{
|
||||
var encrypted = _innerStorage.Read<byte[]>(key);
|
||||
var json = _encryption.Decrypt(encrypted);
|
||||
return JsonSerializer.Deserialize<T>(json);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何限制存储大小?
|
||||
|
||||
**解答**:
|
||||
实现配额管理:
|
||||
|
||||
```csharp
|
||||
public class QuotaStorage : IStorage
|
||||
{
|
||||
private readonly IStorage _innerStorage;
|
||||
private readonly long _maxSize;
|
||||
private long _currentSize;
|
||||
|
||||
public void Write<T>(string key, T value)
|
||||
{
|
||||
var data = Serialize(value);
|
||||
var size = data.Length;
|
||||
|
||||
if (_currentSize + size > _maxSize)
|
||||
{
|
||||
throw new InvalidOperationException("存储配额已满");
|
||||
}
|
||||
|
||||
_innerStorage.Write(key, value);
|
||||
_currentSize += size;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何实现存储压缩?
|
||||
|
||||
**解答**:
|
||||
使用压缩序列化器:
|
||||
|
||||
```csharp
|
||||
public class CompressedSerializer : ISerializer
|
||||
{
|
||||
private readonly ISerializer _innerSerializer;
|
||||
|
||||
public string Serialize<T>(T value)
|
||||
{
|
||||
var json = _innerSerializer.Serialize(value);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
var compressed = Compress(bytes);
|
||||
return Convert.ToBase64String(compressed);
|
||||
}
|
||||
|
||||
public T Deserialize<T>(string data)
|
||||
{
|
||||
var compressed = Convert.FromBase64String(data);
|
||||
var bytes = Decompress(compressed);
|
||||
var json = Encoding.UTF8.GetString(bytes);
|
||||
return _innerSerializer.Deserialize<T>(json);
|
||||
}
|
||||
|
||||
private byte[] Compress(byte[] data)
|
||||
{
|
||||
using var output = new MemoryStream();
|
||||
using (var gzip = new GZipStream(output, CompressionMode.Compress))
|
||||
{
|
||||
gzip.Write(data, 0, data.Length);
|
||||
}
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private byte[] Decompress(byte[] data)
|
||||
{
|
||||
using var input = new MemoryStream(data);
|
||||
using var gzip = new GZipStream(input, CompressionMode.Decompress);
|
||||
using var output = new MemoryStream();
|
||||
gzip.CopyTo(output);
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何监控存储操作?
|
||||
|
||||
**解答**:
|
||||
实现日志存储包装器:
|
||||
|
||||
```csharp
|
||||
public class LoggingStorage : IStorage
|
||||
{
|
||||
private readonly IStorage _innerStorage;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public void Write<T>(string key, T value)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
_innerStorage.Write(key, value);
|
||||
_logger.Info($"写入成功: {key}, 耗时: {stopwatch.ElapsedMilliseconds}ms");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"写入失败: {key}, 错误: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public T Read<T>(string key)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var value = _innerStorage.Read<T>(key);
|
||||
_logger.Info($"读取成功: {key}, 耗时: {stopwatch.ElapsedMilliseconds}ms");
|
||||
return value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"读取失败: {key}, 错误: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [数据与存档系统](/zh-CN/game/data) - 数据持久化
|
||||
- [序列化系统](/zh-CN/game/serialization) - 数据序列化
|
||||
- [Godot 集成](/zh-CN/godot/index) - Godot 中的存储
|
||||
- [存档系统教程](/zh-CN/tutorials/save-system) - 完整示例
|
||||
496
docs/zh-CN/game/ui.md
Normal file
496
docs/zh-CN/game/ui.md
Normal file
@ -0,0 +1,496 @@
|
||||
---
|
||||
title: UI 系统
|
||||
description: UI 系统提供了完整的 UI 页面管理、路由导航和多层级显示功能。
|
||||
---
|
||||
|
||||
# UI 系统
|
||||
|
||||
## 概述
|
||||
|
||||
UI 系统是 GFramework.Game 中用于管理游戏 UI 界面的核心组件。它提供了 UI 页面的生命周期管理、基于栈的导航机制,以及多层级的
|
||||
UI 显示系统(Page、Overlay、Modal、Toast、Topmost)。
|
||||
|
||||
通过 UI 系统,你可以轻松实现 UI 页面之间的切换,管理 UI 栈(如主菜单 -> 设置 -> 关于),以及在不同层级显示各种类型的
|
||||
UI(对话框、提示、加载界面等)。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 完整的 UI 生命周期管理
|
||||
- 基于栈的 UI 导航
|
||||
- 多层级 UI 显示(5 个层级)
|
||||
- UI 转换管道和钩子
|
||||
- 路由守卫(Route Guard)
|
||||
- UI 工厂和行为模式
|
||||
|
||||
## 核心概念
|
||||
|
||||
### UI 页面接口
|
||||
|
||||
`IUiPage` 定义了 UI 页面的生命周期:
|
||||
|
||||
```csharp
|
||||
public interface IUiPage
|
||||
{
|
||||
void OnEnter(IUiPageEnterParam? param); // 进入页面
|
||||
void OnExit(); // 退出页面
|
||||
void OnPause(); // 暂停页面
|
||||
void OnResume(); // 恢复页面
|
||||
void OnShow(); // 显示页面
|
||||
void OnHide(); // 隐藏页面
|
||||
}
|
||||
```
|
||||
|
||||
### UI 路由
|
||||
|
||||
`IUiRouter` 管理 UI 的导航和切换:
|
||||
|
||||
```csharp
|
||||
public interface IUiRouter : ISystem
|
||||
{
|
||||
int Count { get; } // UI 栈深度
|
||||
IUiPageBehavior? Peek(); // 栈顶 UI
|
||||
|
||||
ValueTask PushAsync(string uiKey, IUiPageEnterParam? param = null);
|
||||
ValueTask PopAsync(UiPopPolicy policy = UiPopPolicy.Destroy);
|
||||
ValueTask ReplaceAsync(string uiKey, IUiPageEnterParam? param = null);
|
||||
ValueTask ClearAsync();
|
||||
}
|
||||
```
|
||||
|
||||
### UI 层级
|
||||
|
||||
UI 系统支持 5 个显示层级:
|
||||
|
||||
```csharp
|
||||
public enum UiLayer
|
||||
{
|
||||
Page, // 页面层(栈管理,不可重入)
|
||||
Overlay, // 浮层(可重入,对话框等)
|
||||
Modal, // 模态层(可重入,带遮罩)
|
||||
Toast, // 提示层(可重入,轻量提示)
|
||||
Topmost // 顶层(不可重入,系统级)
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 定义 UI 页面
|
||||
|
||||
实现 `IUiPage` 接口创建 UI 页面:
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.ui;
|
||||
|
||||
public class MainMenuPage : IUiPage
|
||||
{
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
Console.WriteLine("进入主菜单");
|
||||
// 初始化 UI、绑定事件
|
||||
}
|
||||
|
||||
public void OnExit()
|
||||
{
|
||||
Console.WriteLine("退出主菜单");
|
||||
// 清理资源、解绑事件
|
||||
}
|
||||
|
||||
public void OnPause()
|
||||
{
|
||||
Console.WriteLine("暂停主菜单");
|
||||
// 暂停动画、停止交互
|
||||
}
|
||||
|
||||
public void OnResume()
|
||||
{
|
||||
Console.WriteLine("恢复主菜单");
|
||||
// 恢复动画、启用交互
|
||||
}
|
||||
|
||||
public void OnShow()
|
||||
{
|
||||
Console.WriteLine("显示主菜单");
|
||||
// 显示 UI 元素
|
||||
}
|
||||
|
||||
public void OnHide()
|
||||
{
|
||||
Console.WriteLine("隐藏主菜单");
|
||||
// 隐藏 UI 元素
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 切换 UI 页面
|
||||
|
||||
使用 UI 路由进行导航:
|
||||
|
||||
```csharp
|
||||
public class UiController : IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public async Task ShowSettings()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
// 压入设置页面(保留当前页面)
|
||||
await uiRouter.PushAsync("Settings");
|
||||
}
|
||||
|
||||
public async Task CloseSettings()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
// 弹出当前页面(返回上一页)
|
||||
await uiRouter.PopAsync();
|
||||
}
|
||||
|
||||
public async Task ShowMainMenu()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
// 替换所有页面(清空 UI 栈)
|
||||
await uiRouter.ReplaceAsync("MainMenu");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 显示不同层级的 UI
|
||||
|
||||
```csharp
|
||||
public class UiController : IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public void ShowDialog()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
// 在 Modal 层显示对话框
|
||||
var handle = uiRouter.Show("ConfirmDialog", UiLayer.Modal);
|
||||
}
|
||||
|
||||
public void ShowToast(string message)
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
// 在 Toast 层显示提示
|
||||
var handle = uiRouter.Show("ToastMessage", UiLayer.Toast,
|
||||
new ToastParam { Message = message });
|
||||
}
|
||||
|
||||
public void ShowLoading()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
// 在 Topmost 层显示加载界面
|
||||
var handle = uiRouter.Show("LoadingScreen", UiLayer.Topmost);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### UI 参数传递
|
||||
|
||||
```csharp
|
||||
// 定义 UI 参数
|
||||
public class SettingsEnterParam : IUiPageEnterParam
|
||||
{
|
||||
public string Category { get; set; }
|
||||
}
|
||||
|
||||
// 在 UI 中接收参数
|
||||
public class SettingsPage : IUiPage
|
||||
{
|
||||
private string _category;
|
||||
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
if (param is SettingsEnterParam settingsParam)
|
||||
{
|
||||
_category = settingsParam.Category;
|
||||
Console.WriteLine($"打开设置分类: {_category}");
|
||||
}
|
||||
}
|
||||
|
||||
// ... 其他生命周期方法
|
||||
}
|
||||
|
||||
// 传递参数
|
||||
await uiRouter.PushAsync("Settings", new SettingsEnterParam
|
||||
{
|
||||
Category = "Audio"
|
||||
});
|
||||
```
|
||||
|
||||
### 路由守卫
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.ui;
|
||||
|
||||
public class UnsavedChangesGuard : IUiRouteGuard
|
||||
{
|
||||
public async ValueTask<bool> CanLeaveAsync(
|
||||
IUiPageBehavior from,
|
||||
string toKey,
|
||||
IUiPageEnterParam? param)
|
||||
{
|
||||
// 检查是否有未保存的更改
|
||||
if (from.Key == "Settings" && HasUnsavedChanges())
|
||||
{
|
||||
var confirmed = await ShowConfirmDialog();
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> CanEnterAsync(
|
||||
string toKey,
|
||||
IUiPageEnterParam? param)
|
||||
{
|
||||
// 进入前的验证
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool HasUnsavedChanges() => true;
|
||||
private async Task<bool> ShowConfirmDialog() => await Task.FromResult(true);
|
||||
}
|
||||
|
||||
// 注册守卫
|
||||
uiRouter.AddGuard(new UnsavedChangesGuard());
|
||||
```
|
||||
|
||||
### UI 转换处理器
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.ui;
|
||||
|
||||
public class FadeTransitionHandler : IUiTransitionHandler
|
||||
{
|
||||
public async ValueTask OnBeforeEnterAsync(UiTransitionEvent @event)
|
||||
{
|
||||
Console.WriteLine($"准备进入 UI: {@event.ToKey}");
|
||||
await PlayFadeIn();
|
||||
}
|
||||
|
||||
public async ValueTask OnAfterEnterAsync(UiTransitionEvent @event)
|
||||
{
|
||||
Console.WriteLine($"已进入 UI: {@event.ToKey}");
|
||||
}
|
||||
|
||||
public async ValueTask OnBeforeExitAsync(UiTransitionEvent @event)
|
||||
{
|
||||
Console.WriteLine($"准备退出 UI: {@event.FromKey}");
|
||||
await PlayFadeOut();
|
||||
}
|
||||
|
||||
public async ValueTask OnAfterExitAsync(UiTransitionEvent @event)
|
||||
{
|
||||
Console.WriteLine($"已退出 UI: {@event.FromKey}");
|
||||
}
|
||||
|
||||
private async Task PlayFadeIn() => await Task.Delay(200);
|
||||
private async Task PlayFadeOut() => await Task.Delay(200);
|
||||
}
|
||||
|
||||
// 注册转换处理器
|
||||
uiRouter.RegisterHandler(new FadeTransitionHandler());
|
||||
```
|
||||
|
||||
### UI 句柄管理
|
||||
|
||||
```csharp
|
||||
public class DialogController : IController
|
||||
{
|
||||
private UiHandle? _dialogHandle;
|
||||
|
||||
public void ShowDialog()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
// 显示对话框并保存句柄
|
||||
_dialogHandle = uiRouter.Show("ConfirmDialog", UiLayer.Modal);
|
||||
}
|
||||
|
||||
public void CloseDialog()
|
||||
{
|
||||
if (_dialogHandle.HasValue)
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
// 使用句柄关闭对话框
|
||||
uiRouter.Hide(_dialogHandle.Value, UiLayer.Modal, destroy: true);
|
||||
_dialogHandle = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UI 栈管理
|
||||
|
||||
```csharp
|
||||
public class NavigationController : IController
|
||||
{
|
||||
public void ShowUiStack()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
Console.WriteLine($"UI 栈深度: {uiRouter.Count}");
|
||||
|
||||
var current = uiRouter.Peek();
|
||||
if (current != null)
|
||||
{
|
||||
Console.WriteLine($"当前 UI: {current.Key}");
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSettingsOpen()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
return uiRouter.Contains("Settings");
|
||||
}
|
||||
|
||||
public bool IsTopPage(string uiKey)
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
return uiRouter.IsTop(uiKey);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 多层级 UI 管理
|
||||
|
||||
```csharp
|
||||
public class LayerController : IController
|
||||
{
|
||||
public void ShowMultipleToasts()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
// Toast 层支持重入,可以同时显示多个
|
||||
uiRouter.Show("Toast1", UiLayer.Toast);
|
||||
uiRouter.Show("Toast2", UiLayer.Toast);
|
||||
uiRouter.Show("Toast3", UiLayer.Toast);
|
||||
}
|
||||
|
||||
public void ClearAllToasts()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
// 清空 Toast 层的所有 UI
|
||||
uiRouter.ClearLayer(UiLayer.Toast, destroy: true);
|
||||
}
|
||||
|
||||
public void HideAllDialogs()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
// 隐藏 Modal 层的所有对话框
|
||||
uiRouter.HideByKey("ConfirmDialog", UiLayer.Modal, hideAll: true);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用合适的层级**:根据 UI 类型选择正确的层级
|
||||
```csharp
|
||||
✓ Page: 主要页面(主菜单、设置、游戏界面)
|
||||
✓ Overlay: 浮层(信息面板、小窗口)
|
||||
✓ Modal: 模态对话框(确认框、输入框)
|
||||
✓ Toast: 轻量提示(消息、通知)
|
||||
✓ Topmost: 系统级(加载界面、全屏遮罩)
|
||||
```
|
||||
|
||||
2. **使用 Push/Pop 管理临时 UI**:如设置、帮助页面
|
||||
```csharp
|
||||
// 打开设置(保留当前页面)
|
||||
await uiRouter.PushAsync("Settings");
|
||||
|
||||
// 关闭设置(返回上一页)
|
||||
await uiRouter.PopAsync();
|
||||
```
|
||||
|
||||
3. **使用 Replace 切换主要页面**:如从菜单到游戏
|
||||
```csharp
|
||||
// 开始游戏(清空 UI 栈)
|
||||
await uiRouter.ReplaceAsync("Gameplay");
|
||||
```
|
||||
|
||||
4. **在 OnEnter/OnExit 中管理资源**:保持资源管理清晰
|
||||
```csharp
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
// 加载资源、绑定事件
|
||||
BindEvents();
|
||||
}
|
||||
|
||||
public void OnExit()
|
||||
{
|
||||
// 清理资源、解绑事件
|
||||
UnbindEvents();
|
||||
}
|
||||
```
|
||||
|
||||
5. **使用句柄管理非栈 UI**:对于 Overlay、Modal、Toast 层
|
||||
```csharp
|
||||
// 保存句柄
|
||||
var handle = uiRouter.Show("Dialog", UiLayer.Modal);
|
||||
|
||||
// 使用句柄关闭
|
||||
uiRouter.Hide(handle, UiLayer.Modal, destroy: true);
|
||||
```
|
||||
|
||||
6. **避免在 UI 切换时阻塞**:使用异步操作
|
||||
```csharp
|
||||
✓ await uiRouter.PushAsync("Settings");
|
||||
✗ uiRouter.PushAsync("Settings").Wait(); // 可能死锁
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:Push、Pop、Replace 有什么区别?
|
||||
|
||||
**解答**:
|
||||
|
||||
- **Push**:压入新 UI,暂停当前 UI(用于临时页面)
|
||||
- **Pop**:弹出当前 UI,恢复上一个 UI(用于关闭临时页面)
|
||||
- **Replace**:清空 UI 栈,加载新 UI(用于主要页面切换)
|
||||
|
||||
### 问题:什么时候使用不同的 UI 层级?
|
||||
|
||||
**解答**:
|
||||
|
||||
- **Page**:主要页面,使用栈管理
|
||||
- **Overlay**:浮层,可叠加显示
|
||||
- **Modal**:模态对话框,阻挡下层交互
|
||||
- **Toast**:轻量提示,不阻挡交互
|
||||
- **Topmost**:系统级,最高优先级
|
||||
|
||||
### 问题:如何在 UI 之间传递数据?
|
||||
|
||||
**解答**:
|
||||
|
||||
1. 通过 UI 参数
|
||||
2. 通过 Model
|
||||
3. 通过事件
|
||||
|
||||
### 问题:UI 切换时如何显示过渡动画?
|
||||
|
||||
**解答**:
|
||||
使用 UI 转换处理器在 `OnBeforeEnter`/`OnAfterExit` 中播放动画。
|
||||
|
||||
### 问题:如何防止用户在 UI 切换时操作?
|
||||
|
||||
**解答**:
|
||||
在转换处理器中显示遮罩或禁用输入。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [场景系统](/zh-CN/game/scene) - 场景管理
|
||||
- [Godot UI 系统](/zh-CN/godot/ui) - Godot 引擎集成
|
||||
- [事件系统](/zh-CN/core/events) - UI 事件通信
|
||||
- [状态机系统](/zh-CN/core/state-machine) - UI 状态管理
|
||||
590
docs/zh-CN/godot/architecture.md
Normal file
590
docs/zh-CN/godot/architecture.md
Normal file
@ -0,0 +1,590 @@
|
||||
---
|
||||
title: Godot 架构集成
|
||||
description: Godot 架构集成提供了 GFramework 与 Godot 引擎的无缝连接,实现生命周期同步和模块化开发。
|
||||
---
|
||||
|
||||
# Godot 架构集成
|
||||
|
||||
## 概述
|
||||
|
||||
Godot 架构集成是 GFramework.Godot 中连接框架与 Godot 引擎的核心组件。它提供了架构与 Godot 场景树的生命周期绑定、模块化扩展系统,以及与
|
||||
Godot 节点系统的深度集成。
|
||||
|
||||
通过 Godot 架构集成,你可以在 Godot 项目中使用 GFramework 的所有功能,同时保持与 Godot 引擎的完美兼容。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 架构与 Godot 生命周期自动同步
|
||||
- 模块化的 Godot 扩展系统
|
||||
- 架构锚点节点管理
|
||||
- 自动资源清理
|
||||
- 热重载支持
|
||||
- 与 Godot 场景树深度集成
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 抽象架构
|
||||
|
||||
`AbstractArchitecture` 是 Godot 项目中架构的基类:
|
||||
|
||||
```csharp
|
||||
public abstract class AbstractArchitecture : Architecture
|
||||
{
|
||||
protected Node ArchitectureRoot { get; }
|
||||
protected abstract void InstallModules();
|
||||
protected Task InstallGodotModule<TModule>(TModule module);
|
||||
}
|
||||
```
|
||||
|
||||
### 架构锚点
|
||||
|
||||
`ArchitectureAnchor` 是连接架构与 Godot 场景树的桥梁:
|
||||
|
||||
```csharp
|
||||
public partial class ArchitectureAnchor : Node
|
||||
{
|
||||
public void Bind(Action onExit);
|
||||
public override void _ExitTree();
|
||||
}
|
||||
```
|
||||
|
||||
### Godot 模块
|
||||
|
||||
`IGodotModule` 定义了 Godot 特定的模块接口:
|
||||
|
||||
```csharp
|
||||
public interface IGodotModule : IArchitectureModule
|
||||
{
|
||||
Node Node { get; }
|
||||
void OnPhase(ArchitecturePhase phase, IArchitecture architecture);
|
||||
void OnAttach(Architecture architecture);
|
||||
void OnDetach();
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 创建 Godot 架构
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.architecture;
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
// 单例实例
|
||||
public static GameArchitecture Interface { get; private set; }
|
||||
|
||||
public GameArchitecture()
|
||||
{
|
||||
Interface = this;
|
||||
}
|
||||
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 注册 Model
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterModel(new GameModel());
|
||||
|
||||
// 注册 System
|
||||
RegisterSystem(new GameplaySystem());
|
||||
RegisterSystem(new AudioSystem());
|
||||
|
||||
// 注册 Utility
|
||||
RegisterUtility(new StorageUtility());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在 Godot 场景中初始化架构
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.architecture;
|
||||
|
||||
public partial class GameRoot : Node
|
||||
{
|
||||
private GameArchitecture _architecture;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 创建并初始化架构
|
||||
_architecture = new GameArchitecture();
|
||||
_architecture.InitializeAsync().AsTask().Wait();
|
||||
|
||||
GD.Print("架构已初始化");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用架构锚点
|
||||
|
||||
架构锚点会自动创建并绑定到场景树:
|
||||
|
||||
```csharp
|
||||
// 架构会自动创建锚点节点
|
||||
// 节点名称格式: __GFramework__GameArchitecture__[HashCode]__ArchitectureAnchor__
|
||||
|
||||
// 当场景树销毁时,锚点会自动触发架构清理
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 创建 Godot 模块
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.architecture;
|
||||
using Godot;
|
||||
|
||||
public class CoroutineModule : AbstractGodotModule
|
||||
{
|
||||
private Node _coroutineNode;
|
||||
|
||||
public override Node Node => _coroutineNode;
|
||||
|
||||
public CoroutineModule()
|
||||
{
|
||||
_coroutineNode = new Node { Name = "CoroutineScheduler" };
|
||||
}
|
||||
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
// 注册协程调度器
|
||||
var scheduler = new CoroutineScheduler(new GodotTimeSource());
|
||||
architecture.RegisterSystem<ICoroutineScheduler>(scheduler);
|
||||
|
||||
GD.Print("协程模块已安装");
|
||||
}
|
||||
|
||||
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
|
||||
{
|
||||
if (phase == ArchitecturePhase.Ready)
|
||||
{
|
||||
GD.Print("协程模块已就绪");
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDetach()
|
||||
{
|
||||
GD.Print("协程模块已分离");
|
||||
_coroutineNode?.QueueFree();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 安装 Godot 模块
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 安装核心模块
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterSystem(new GameplaySystem());
|
||||
|
||||
// 安装 Godot 模块
|
||||
InstallGodotModule(new CoroutineModule()).Wait();
|
||||
InstallGodotModule(new SceneModule()).Wait();
|
||||
InstallGodotModule(new UiModule()).Wait();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 访问架构根节点
|
||||
|
||||
```csharp
|
||||
public class SceneModule : AbstractGodotModule
|
||||
{
|
||||
private Node _sceneRoot;
|
||||
|
||||
public override Node Node => _sceneRoot;
|
||||
|
||||
public SceneModule()
|
||||
{
|
||||
_sceneRoot = new Node { Name = "SceneRoot" };
|
||||
}
|
||||
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
// 访问架构根节点
|
||||
if (architecture is AbstractArchitecture godotArch)
|
||||
{
|
||||
var root = godotArch.ArchitectureRoot;
|
||||
root.AddChild(_sceneRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 监听架构阶段
|
||||
|
||||
```csharp
|
||||
public class AnalyticsModule : AbstractGodotModule
|
||||
{
|
||||
private Node _analyticsNode;
|
||||
|
||||
public override Node Node => _analyticsNode;
|
||||
|
||||
public AnalyticsModule()
|
||||
{
|
||||
_analyticsNode = new Node { Name = "Analytics" };
|
||||
}
|
||||
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
// 安装分析系统
|
||||
}
|
||||
|
||||
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
|
||||
{
|
||||
switch (phase)
|
||||
{
|
||||
case ArchitecturePhase.Initializing:
|
||||
GD.Print("架构正在初始化");
|
||||
break;
|
||||
|
||||
case ArchitecturePhase.Ready:
|
||||
GD.Print("架构已就绪,开始追踪");
|
||||
StartTracking();
|
||||
break;
|
||||
|
||||
case ArchitecturePhase.Destroying:
|
||||
GD.Prin构正在销毁,停止追踪");
|
||||
StopTracking();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void StartTracking() { }
|
||||
private void StopTracking() { }
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义架构配置
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Core.Abstractions.environment;
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
public GameArchitecture() : base(
|
||||
configuration: CreateConfiguration(),
|
||||
environment: CreateEnvironment()
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
private static IArchitectureConfiguration CreateConfiguration()
|
||||
{
|
||||
return new ArchitectureConfiguration
|
||||
{
|
||||
EnableLogging
|
||||
LogLevel = LogLevel.Debug
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnvironment CreateEnvironment()
|
||||
{
|
||||
return new DefaultEnvironment
|
||||
{
|
||||
IsDevelopment = OS.IsDebugBuild()
|
||||
};
|
||||
}
|
||||
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 根据环境配置安装模块
|
||||
if (Environment.IsDevelopment)
|
||||
{
|
||||
InstallGodotModule(new DebugModule()).Wait();
|
||||
}
|
||||
|
||||
// 安装核心模块
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterSystem(new GameplaySystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 热重载支持
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
private static bool _initialized;
|
||||
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
// 防止热重载时重复初始化
|
||||
if (_initialized)
|
||||
{
|
||||
GD.Print("架构已初始化,跳过重复初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnInitialize();
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
protected override async ValueTask OnDestroyAsync()
|
||||
{
|
||||
await base.OnDestroyAsync();
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在节点中使用架构
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class Player : CharacterBody2D
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
// 通过扩展方法访问架构组件
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
var gameplaySystem = this.GetSystem<GameplaySystem>();
|
||||
|
||||
// 发送事件
|
||||
this.SendEvent(new PlayerSpawnedEvent());
|
||||
|
||||
// 执行命令
|
||||
this.SendCommand(new InitPlayerCommand());
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
// 在 Process 中使用架构组件
|
||||
var inputSystem = this.GetSystem<InputSystem>();
|
||||
var movement = inputSystem.GetMovementInput();
|
||||
|
||||
Velocity = movement * 200;
|
||||
MoveAndSlide();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 多架构支持
|
||||
|
||||
```csharp
|
||||
// 游戏架构
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
public static GameArchitecture Interface { get; private set; }
|
||||
|
||||
public GameArchitecture()
|
||||
{
|
||||
Interface = this;
|
||||
}
|
||||
|
||||
protected override void InstallModules()
|
||||
{
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterSystem(new GameplaySystem());
|
||||
}
|
||||
}
|
||||
|
||||
// UI 架构
|
||||
public class UiArchitecture : AbstractArchitecture
|
||||
{
|
||||
public static UiArchitecture Interface { get; private set; }
|
||||
|
||||
public UiArchitecture()
|
||||
{
|
||||
Interface = this;
|
||||
}
|
||||
|
||||
protected override void InstallModules()
|
||||
{
|
||||
RegisterModel(new UiModel());
|
||||
RegisterSystem(new UiSystem());
|
||||
}
|
||||
}
|
||||
|
||||
// 在不同节点中使用不同架构
|
||||
public partial class GameNode : Node, IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
}
|
||||
|
||||
public partial class UiNode : Control, IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => UiArchitecture.Interface;
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用单例模式**:为架构提供全局访问点
|
||||
```csharp
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
public static GameArchitecture Interface { get; private set; }
|
||||
|
||||
public GameArchitecture()
|
||||
{
|
||||
Interface = this;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **在根节点初始化架构**:确保架构在所有节点之前就绪
|
||||
```csharp
|
||||
public partial class GameRoot : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
new GameArchitecture().InitializeAsync().AsTask().Wait();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **使用 Godot 模块组织功能**:将相关功能封装为模块
|
||||
```csharp
|
||||
InstallGodotModule(new CoroutineModule()).Wait();
|
||||
InstallGodotModule(new SceneModule()).Wait();
|
||||
InstallGodotModule(new UiModule()).Wait();
|
||||
```
|
||||
|
||||
4. **利用架构阶段钩子**:在适当的时机执行逻辑
|
||||
```csharp
|
||||
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
|
||||
{
|
||||
if (phase == ArchitecturePhase.Ready)
|
||||
{
|
||||
// 架构就绪后的初始化
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. **正确清理资源**:在 OnDetach 中释放 Godot 节点
|
||||
```csharp
|
||||
public override void OnDetach()
|
||||
{
|
||||
_node?.QueueFree();
|
||||
_node = null;
|
||||
}
|
||||
```
|
||||
|
||||
6. **避免在构造函数中访问架构**:使用 _Ready 或 OnPhase
|
||||
```csharp
|
||||
✗ public Player()
|
||||
{
|
||||
var model = this.GetModel<PlayerModel>(); // 架构可能未就绪
|
||||
}
|
||||
|
||||
✓ public override void _Ready()
|
||||
{
|
||||
var model = this.GetModel<PlayerModel>(); // 安全
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:架构什么时候初始化?
|
||||
|
||||
**解答**:
|
||||
在根节点的 `_Ready` 方法中初始化:
|
||||
|
||||
```csharp
|
||||
public partial class GameRoot : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
new GameArchitecture().InitializeAsync().AsTask().Wait();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何在节点中访问架构?
|
||||
|
||||
**解答**:
|
||||
实现 `IController` 接口或使用扩展方法:
|
||||
|
||||
```csharp
|
||||
// 方式 1: 实现 IController
|
||||
public partial class Player : Node, IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
var model = this.GetModel<PlayerModel>();
|
||||
}
|
||||
}
|
||||
|
||||
// 方式 2: 直接使用单例
|
||||
public partial class Enemy : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
var model = GameArchitecture.Interface.GetModel<EnemyModel>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:架构锚点节点是什么?
|
||||
|
||||
**解答**:
|
||||
架构锚点是一个隐藏的节点,用于将架构绑定到 Godot 场景树。当场景树销毁时,锚点会自动触发架构清理。
|
||||
|
||||
### 问题:如何支持热重载?
|
||||
|
||||
**解答**:
|
||||
使用静态标志防止重复初始化:
|
||||
|
||||
```csharp
|
||||
private static bool _initialized;
|
||||
|
||||
protected override void OnInitialize()
|
||||
{
|
||||
if (_initialized) return;
|
||||
base.OnInitialize();
|
||||
_initialized = true;
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:可以有多个架构吗?
|
||||
|
||||
**解答**:
|
||||
可以,但通常一个游戏只需要一个主架构。如果需要多个架构,为每个架构提供独立的单例:
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
public static GameArchitecture Interface { get; private set; }
|
||||
}
|
||||
|
||||
public class UiArchitecture : AbstractArchitecture
|
||||
{
|
||||
public static UiArchitecture Interface { get; private set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:Godot 模块和普通模块有什么区别?
|
||||
|
||||
**解答**:
|
||||
|
||||
- **普通模块**:纯 C# 逻辑,不依赖 Godot
|
||||
- **Godot 模块**:包含 Godot 节点,与场景树集成
|
||||
|
||||
```csharp
|
||||
// 普通模块
|
||||
InstallModule(new CoreModule());
|
||||
|
||||
// Godot 模块
|
||||
InstallGodotModule(new SceneModule()).Wait();
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [架构组件](/zh-CN/core/architecture) - 核心架构系统
|
||||
- [生命周期管理](/zh-CN/core/lifecycle) - 组件生命周期
|
||||
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景集成
|
||||
- [Godot UI 系统](/zh-CN/godot/ui) - Godot UI 集成
|
||||
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
|
||||
628
docs/zh-CN/godot/logging.md
Normal file
628
docs/zh-CN/godot/logging.md
Normal file
@ -0,0 +1,628 @@
|
||||
---
|
||||
title: Godot 日志系统
|
||||
description: Godot 日志系统提供了 GFramework 日志功能与 Godot 引擎控制台的完整集成。
|
||||
---
|
||||
|
||||
# Godot 日志系统
|
||||
|
||||
## 概述
|
||||
|
||||
Godot 日志系统是 GFramework.Godot 中连接框架日志功能与 Godot 引擎控制台的核心组件。它提供了与 Godot
|
||||
控制台的深度集成,支持彩色输出、多级别日志记录,以及与 GFramework 日志系统的无缝对接。
|
||||
|
||||
通过 Godot 日志系统,你可以在 Godot 项目中使用统一的日志接口,日志会自动输出到 Godot 编辑器控制台,并根据日志级别使用不同的颜色和输出方式。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 与 Godot 控制台深度集成
|
||||
- 支持彩色日志输出
|
||||
- 多级别日志记录(Trace、Debug、Info、Warning、Error、Fatal)
|
||||
- 日志缓存机制
|
||||
- 时间戳和格式化支持
|
||||
- 异常信息记录
|
||||
|
||||
## 核心概念
|
||||
|
||||
### GodotLogger
|
||||
|
||||
`GodotLogger` 是 Godot 平台的日志记录器实现,继承自 `AbstractLogger`:
|
||||
|
||||
```csharp
|
||||
public sealed class GodotLogger : AbstractLogger
|
||||
{
|
||||
public GodotLogger(string? name = null, LogLevel minLevel = LogLevel.Info);
|
||||
protected override void Write(LogLevel level, string message, Exception? exception);
|
||||
}
|
||||
```
|
||||
|
||||
### GodotLoggerFactory
|
||||
|
||||
`GodotLoggerFactory` 用于创建 Godot 日志记录器实例:
|
||||
|
||||
```csharp
|
||||
public class GodotLoggerFactory : ILoggerFactory
|
||||
{
|
||||
public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info);
|
||||
}
|
||||
```
|
||||
|
||||
### GodotLoggerFactoryProvider
|
||||
|
||||
`GodotLoggerFactoryProvider` 提供日志工厂实例,并支持日志缓存:
|
||||
|
||||
```csharp
|
||||
public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
|
||||
{
|
||||
public LogLevel MinLevel { get; set; }
|
||||
public ILogger CreateLogger(string name);
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 配置 Godot 日志系统
|
||||
|
||||
在架构初始化时配置日志提供程序:
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.architecture;
|
||||
using GFramework.Godot.logging;
|
||||
using GFramework.Core.logging;
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
public static GameArchitecture Interface { get; private set; }
|
||||
|
||||
public GameArchitecture()
|
||||
{
|
||||
Interface = this;
|
||||
|
||||
// 配置 Godot 日志系统
|
||||
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Debug // 设置最小日志级别
|
||||
};
|
||||
}
|
||||
|
||||
protected override void InstallModules()
|
||||
{
|
||||
var logger = LoggerFactoryResolver.Provider.CreateLogger("GameArchitecture");
|
||||
logger.Info("游戏架构初始化开始");
|
||||
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterSystem(new GameplaySystem());
|
||||
|
||||
logger.Info("游戏架构初始化完成");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 创建和使用日志记录器
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Core.logging;
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
|
||||
public partial class Player : CharacterBody2D
|
||||
{
|
||||
private ILogger _logger;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 创建日志记录器
|
||||
_logger = LoggerFactoryResolver.Provider.CreateLogger("Player");
|
||||
|
||||
_logger.Info("玩家初始化");
|
||||
_logger.Debug("玩家位置: {0}", Position);
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (_logger.IsDebugEnabled())
|
||||
{
|
||||
_logger.Debug("玩家速度: {0}", Velocity);
|
||||
}
|
||||
}
|
||||
|
||||
private void TakeDamage(float damage)
|
||||
{
|
||||
_logger.Warn("玩家受到伤害: {0}", damage);
|
||||
}
|
||||
|
||||
private void OnError()
|
||||
{
|
||||
_logger.Error("玩家状态异常");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 记录不同级别的日志
|
||||
|
||||
```csharp
|
||||
var logger = LoggerFactoryResolver.Provider.CreateLogger("GameSystem");
|
||||
|
||||
// Trace - 最详细的跟踪信息(灰色)
|
||||
logger.Trace("执行函数: UpdatePlayerPosition");
|
||||
|
||||
// Debug - 调试信息(青色)
|
||||
logger.Debug("当前帧率: {0}", Engine.GetFramesPerSecond());
|
||||
|
||||
// Info - 一般信息(白色)
|
||||
logger.Info("游戏开始");
|
||||
|
||||
// Warning - 警告信息(黄色)
|
||||
logger.Warn("资源加载缓慢: {0}ms", loadTime);
|
||||
|
||||
// Error - 错误信息(红色)
|
||||
logger.Error("无法加载配置文件");
|
||||
|
||||
// Fatal - 致命错误(红色,使用 PushError)
|
||||
logger.Fatal("游戏崩溃");
|
||||
```
|
||||
|
||||
### 记录异常信息
|
||||
|
||||
```csharp
|
||||
var logger = LoggerFactoryResolver.Provider.CreateLogger("SaveSystem");
|
||||
|
||||
try
|
||||
{
|
||||
SaveGame();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录异常信息
|
||||
logger.Error("保存游戏失败", ex);
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 在 System 中使用日志
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Core.logging;
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
|
||||
public class CombatSystem : AbstractSystem
|
||||
{
|
||||
private ILogger _logger;
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_logger = LoggerFactoryResolver.Provider.CreateLogger("CombatSystem");
|
||||
_logger.Info("战斗系统初始化完成");
|
||||
}
|
||||
|
||||
public void ProcessCombat(Entity attacker, Entity target, float damage)
|
||||
{
|
||||
_logger.Debug("战斗处理: {0} 攻击 {1}, 伤害: {2}",
|
||||
attacker.Name, target.Name, damage);
|
||||
|
||||
if (damage > 100)
|
||||
{
|
||||
_logger.Warn("高伤害攻击: {0}", damage);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
_logger.Info("战斗系统已销毁");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在 Model 中使用日志
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.model;
|
||||
using GFramework.Core.logging;
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
|
||||
public class PlayerModel : AbstractModel
|
||||
{
|
||||
private ILogger _logger;
|
||||
private int _health;
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_logger = LoggerFactoryResolver.Provider.CreateLogger("PlayerModel");
|
||||
_logger.Info("玩家模型初始化");
|
||||
|
||||
_health = 100;
|
||||
}
|
||||
|
||||
public void SetHealth(int value)
|
||||
{
|
||||
var oldHealth = _health;
|
||||
_health = value;
|
||||
|
||||
_logger.Debug("玩家生命值变化: {0} -> {1}", oldHealth, _health);
|
||||
|
||||
if (_health <= 0)
|
||||
{
|
||||
_logger.Warn("玩家生命值归零");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 条件日志记录
|
||||
|
||||
```csharp
|
||||
var logger = LoggerFactoryResolver.Provider.CreateLogger("PerformanceMonitor");
|
||||
|
||||
// 检查日志级别是否启用,避免不必要的字符串格式化
|
||||
if (logger.IsDebugEnabled())
|
||||
{
|
||||
var stats = CalculateComplexStats(); // 耗时操作
|
||||
logger.Debug("性能统计: {0}", stats);
|
||||
}
|
||||
|
||||
// 简化写法
|
||||
if (logger.IsTraceEnabled())
|
||||
{
|
||||
logger.Trace("详细的执行流程信息");
|
||||
}
|
||||
```
|
||||
|
||||
### 分类日志记录
|
||||
|
||||
```csharp
|
||||
// 为不同模块创建独立的日志记录器
|
||||
var networkLogger = LoggerFactoryResolver.Provider.CreateLogger("Network");
|
||||
var databaseLogger = LoggerFactoryResolver.Provider.CreateLogger("Database");
|
||||
var aiLogger = LoggerFactoryResolver.Provider.CreateLogger("AI");
|
||||
|
||||
networkLogger.Info("连接到服务器");
|
||||
databaseLogger.Debug("查询用户数据");
|
||||
aiLogger.Trace("AI 决策树遍历");
|
||||
```
|
||||
|
||||
### 自定义日志级别
|
||||
|
||||
```csharp
|
||||
// 在开发环境使用 Debug 级别
|
||||
#if DEBUG
|
||||
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Debug
|
||||
};
|
||||
#else
|
||||
// 在生产环境使用 Info 级别
|
||||
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Info
|
||||
};
|
||||
#endif
|
||||
```
|
||||
|
||||
### 在 Godot 模块中使用日志
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.architecture;
|
||||
using GFramework.Core.logging;
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
using Godot;
|
||||
|
||||
public class SceneModule : AbstractGodotModule
|
||||
{
|
||||
private ILogger _logger;
|
||||
private Node _sceneRoot;
|
||||
|
||||
public override Node Node => _sceneRoot;
|
||||
|
||||
public SceneModule()
|
||||
{
|
||||
_sceneRoot = new Node { Name = "SceneRoot" };
|
||||
_logger = LoggerFactoryResolver.Provider.CreateLogger("SceneModule");
|
||||
}
|
||||
|
||||
public override void Install(IArchitecture architecture)
|
||||
{
|
||||
_logger.Info("场景模块安装开始");
|
||||
|
||||
// 安装场景系统
|
||||
var sceneSystem = new SceneSystem();
|
||||
architecture.RegisterSystem<ISceneSystem>(sceneSystem);
|
||||
|
||||
_logger.Info("场景模块安装完成");
|
||||
}
|
||||
|
||||
public override void OnPhase(ArchitecturePhase phase, IArchitecture architecture)
|
||||
{
|
||||
_logger.Debug("场景模块阶段: {0}", phase);
|
||||
|
||||
if (phase == ArchitecturePhase.Ready)
|
||||
{
|
||||
_logger.Info("场景模块已就绪");
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDetach()
|
||||
{
|
||||
_logger.Info("场景模块已分离");
|
||||
_sceneRoot?.QueueFree();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 日志输出格式
|
||||
|
||||
### 输出格式说明
|
||||
|
||||
Godot 日志系统使用以下格式输出日志:
|
||||
|
||||
```
|
||||
[时间戳] 日志级别 [日志器名称] 日志消息
|
||||
```
|
||||
|
||||
**示例输出**:
|
||||
|
||||
```
|
||||
[2025-01-09 10:30:45.123] INFO [GameArchitecture] 游戏架构初始化开始
|
||||
[2025-01-09 10:30:45.456] DEBUG [Player] 玩家位置: (100, 200)
|
||||
[2025-01-09 10:30:46.789] WARNING [CombatSystem] 高伤害攻击: 150
|
||||
[2025-01-09 10:30:47.012] ERROR [SaveSystem] 保存游戏失败
|
||||
```
|
||||
|
||||
### 日志级别与 Godot 输出方法
|
||||
|
||||
| 日志级别 | Godot 方法 | 颜色 | 说明 |
|
||||
|-------------|------------------|----|----------|
|
||||
| **Trace** | `GD.PrintRich` | 灰色 | 最详细的跟踪信息 |
|
||||
| **Debug** | `GD.PrintRich` | 青色 | 调试信息 |
|
||||
| **Info** | `GD.Print` | 白色 | 一般信息 |
|
||||
| **Warning** | `GD.PushWarning` | 黄色 | 警告信息 |
|
||||
| **Error** | `GD.PrintErr` | 红色 | 错误信息 |
|
||||
| **Fatal** | `GD.PushError` | 红色 | 致命错误 |
|
||||
|
||||
### 异常信息格式
|
||||
|
||||
当记录异常时,异常信息会附加到日志消息后:
|
||||
|
||||
```
|
||||
[2025-01-09 10:30:47.012] ERROR [SaveSystem] 保存游戏失败
|
||||
System.IO.IOException: 文件访问被拒绝
|
||||
at SaveSystem.SaveGame() in SaveSystem.cs:line 42
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **在架构初始化时配置日志系统**:
|
||||
```csharp
|
||||
public GameArchitecture()
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Debug
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
2. **为每个类创建独立的日志记录器**:
|
||||
```csharp
|
||||
private ILogger _logger;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_logger = LoggerFactoryResolver.Provider.CreateLogger(GetType().Name);
|
||||
}
|
||||
```
|
||||
|
||||
3. **使用合适的日志级别**:
|
||||
- `Trace`:详细的执行流程,仅在深度调试时使用
|
||||
- `Debug`:调试信息,开发阶段使用
|
||||
- `Info`:重要的业务流程和状态变化
|
||||
- `Warning`:潜在问题但不影响功能
|
||||
- `Error`:错误但程序可以继续运行
|
||||
- `Fatal`:严重错误,程序无法继续
|
||||
|
||||
4. **检查日志级别避免性能损失**:
|
||||
```csharp
|
||||
if (_logger.IsDebugEnabled())
|
||||
{
|
||||
var expensiveData = CalculateExpensiveData();
|
||||
_logger.Debug("数据: {0}", expensiveData);
|
||||
}
|
||||
```
|
||||
|
||||
5. **提供有意义的上下文信息**:
|
||||
```csharp
|
||||
// ✗ 不好
|
||||
logger.Error("错误");
|
||||
|
||||
// ✓ 好
|
||||
logger.Error("加载场景失败: SceneKey={0}, Path={1}", sceneKey, scenePath);
|
||||
```
|
||||
|
||||
6. **记录异常时提供上下文**:
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
LoadScene(sceneKey);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"加载场景失败: {sceneKey}", ex);
|
||||
}
|
||||
```
|
||||
|
||||
7. **使用分类日志记录器**:
|
||||
```csharp
|
||||
var networkLogger = LoggerFactoryResolver.Provider.CreateLogger("Network");
|
||||
var aiLogger = LoggerFactoryResolver.Provider.CreateLogger("AI");
|
||||
```
|
||||
|
||||
8. **在生命周期方法中记录关键事件**:
|
||||
```csharp
|
||||
protected override void OnInit()
|
||||
{
|
||||
_logger.Info("系统初始化完成");
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
_logger.Info("系统已销毁");
|
||||
}
|
||||
```
|
||||
|
||||
## 性能考虑
|
||||
|
||||
1. **日志缓存**:
|
||||
- `GodotLoggerFactoryProvider` 使用 `CachedLoggerFactory` 缓存日志记录器实例
|
||||
- 相同名称和级别的日志记录器会被复用
|
||||
|
||||
2. **级别检查**:
|
||||
- 日志方法会自动检查日志级别
|
||||
- 低于最小级别的日志不会被处理
|
||||
|
||||
3. **字符串格式化**:
|
||||
- 使用参数化日志避免不必要的字符串拼接
|
||||
```csharp
|
||||
// ✗ 不好 - 总是执行字符串拼接
|
||||
logger.Debug("位置: " + position.ToString());
|
||||
|
||||
// ✓ 好 - 只在 Debug 启用时格式化
|
||||
logger.Debug("位置: {0}", position);
|
||||
```
|
||||
|
||||
4. **条件日志**:
|
||||
- 对于耗时的数据计算,先检查日志级别
|
||||
```csharp
|
||||
if (logger.IsDebugEnabled())
|
||||
{
|
||||
var stats = CalculateComplexStats();
|
||||
logger.Debug("统计: {0}", stats);
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:如何配置 Godot 日志系统?
|
||||
|
||||
**解答**:
|
||||
在架构构造函数中配置日志提供程序:
|
||||
|
||||
```csharp
|
||||
public GameArchitecture()
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Debug
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:日志没有输出到 Godot 控制台?
|
||||
|
||||
**解答**:
|
||||
检查以下几点:
|
||||
|
||||
1. 确认已配置 `GodotLoggerFactoryProvider`
|
||||
2. 检查日志级别是否低于最小级别
|
||||
3. 确认使用了正确的日志记录器
|
||||
|
||||
```csharp
|
||||
// 确认配置
|
||||
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Trace // 设置为最低级别测试
|
||||
};
|
||||
|
||||
// 创建日志记录器
|
||||
var logger = LoggerFactoryResolver.Provider.CreateLogger("Test");
|
||||
logger.Info("测试日志"); // 应该能看到输出
|
||||
```
|
||||
|
||||
### 问题:如何在不同环境使用不同的日志级别?
|
||||
|
||||
**解答**:
|
||||
使用条件编译或环境检测:
|
||||
|
||||
```csharp
|
||||
public GameArchitecture()
|
||||
{
|
||||
var minLevel = OS.IsDebugBuild() ? LogLevel.Debug : LogLevel.Info;
|
||||
|
||||
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = minLevel
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何禁用某个模块的日志?
|
||||
|
||||
**解答**:
|
||||
为该模块创建一个高级别的日志记录器:
|
||||
|
||||
```csharp
|
||||
// 只记录 Error 及以上级别
|
||||
var logger = new GodotLogger("VerboseModule", LogLevel.Error);
|
||||
```
|
||||
|
||||
### 问题:日志输出影响性能怎么办?
|
||||
|
||||
**解答**:
|
||||
|
||||
1. 提高最小日志级别
|
||||
2. 使用条件日志
|
||||
3. 避免在高频调用的方法中记录日志
|
||||
|
||||
```csharp
|
||||
// 提高日志级别
|
||||
LoggerFactoryResolver.Provider = new GodotLoggerFactoryProvider
|
||||
{
|
||||
MinLevel = LogLevel.Warning // 只记录警告及以上
|
||||
};
|
||||
|
||||
// 使用条件日志
|
||||
if (_logger.IsDebugEnabled())
|
||||
{
|
||||
_logger.Debug("高频数据: {0}", data);
|
||||
}
|
||||
|
||||
// 避免在 _Process 中频繁记录
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
// ✗ 不好 - 每帧都记录
|
||||
// _logger.Debug("帧更新");
|
||||
|
||||
// ✓ 好 - 只在特定条件下记录
|
||||
if (someErrorCondition)
|
||||
{
|
||||
_logger.Error("检测到错误");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何记录结构化日志?
|
||||
|
||||
**解答**:
|
||||
使用参数化日志或 `IStructuredLogger` 接口:
|
||||
|
||||
```csharp
|
||||
// 参数化日志
|
||||
logger.Info("玩家登录: UserId={0}, UserName={1}, Level={2}",
|
||||
userId, userName, level);
|
||||
|
||||
// 使用结构化日志(如果实现了 IStructuredLogger)
|
||||
if (logger is IStructuredLogger structuredLogger)
|
||||
{
|
||||
structuredLogger.Log(LogLevel.Info, "玩家登录",
|
||||
("UserId", userId),
|
||||
("UserName", userName),
|
||||
("Level", level));
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [核心日志系统](/zh-CN/core/logging) - GFramework 核心日志功能
|
||||
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构系统
|
||||
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
|
||||
- [最佳实践](/zh-CN/best-practices/architecture-patterns) - 架构最佳实践
|
||||
736
docs/zh-CN/godot/pause.md
Normal file
736
docs/zh-CN/godot/pause.md
Normal file
@ -0,0 +1,736 @@
|
||||
---
|
||||
title: Godot 暂停处理
|
||||
description: Godot 暂停处理系统提供了 GFramework 暂停管理与 Godot SceneTree 暂停的完整集成。
|
||||
---
|
||||
|
||||
# Godot 暂停处理
|
||||
|
||||
## 概述
|
||||
|
||||
Godot 暂停处理系统是 GFramework.Godot 中连接框架暂停管理与 Godot 引擎暂停机制的核心组件。它提供了暂停栈管理、分组暂停、嵌套暂停等功能,让你可以在
|
||||
Godot 项目中使用 GFramework 的暂停系统。
|
||||
|
||||
通过 Godot 暂停处理系统,你可以实现精细的暂停控制,支持游戏逻辑暂停、UI 暂停、动画暂停等多种场景,同时保持与 Godot SceneTree
|
||||
暂停机制的完美兼容。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 暂停栈管理(支持嵌套暂停)
|
||||
- 分组暂停(Global、Gameplay、Animation、Audio 等)
|
||||
- 与 Godot SceneTree.Paused 集成
|
||||
- 暂停处理器机制
|
||||
- 暂停作用域(支持 using 语法)
|
||||
- 线程安全的暂停管理
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 暂停栈管理器
|
||||
|
||||
`IPauseStackManager` 管理游戏中的暂停状态:
|
||||
|
||||
```csharp
|
||||
public interface IPauseStackManager : IContextUtility
|
||||
{
|
||||
// 推入暂停请求
|
||||
PauseToken Push(string reason, PauseGroup group = PauseGroup.Global);
|
||||
|
||||
// 弹出暂停请求
|
||||
bool Pop(PauseToken token);
|
||||
|
||||
// 查询是否暂停
|
||||
bool IsPaused(PauseGroup group = PauseGroup.Global);
|
||||
|
||||
// 获取暂停深度
|
||||
int GetPauseDepth(PauseGroup group = PauseGroup.Global);
|
||||
|
||||
// 暂停状态变化事件
|
||||
event Action<PauseGroup, bool>? OnPauseStateChanged;
|
||||
}
|
||||
```
|
||||
|
||||
### 暂停组
|
||||
|
||||
`PauseGroup` 定义不同的暂停作用域:
|
||||
|
||||
```csharp
|
||||
public enum PauseGroup
|
||||
{
|
||||
Global = 0, // 全局暂停(影响所有系统)
|
||||
Gameplay = 1, // 游戏逻辑暂停(不影响 UI)
|
||||
Animation = 2, // 动画暂停
|
||||
Audio = 3, // 音频暂停
|
||||
Custom1 = 10, // 自定义组 1
|
||||
Custom2 = 11, // 自定义组 2
|
||||
Custom3 = 12 // 自定义组 3
|
||||
}
|
||||
```
|
||||
|
||||
### 暂停令牌
|
||||
|
||||
`PauseToken` 唯一标识一个暂停请求:
|
||||
|
||||
```csharp
|
||||
public readonly struct PauseToken
|
||||
{
|
||||
public Guid Id { get; }
|
||||
public bool IsValid { get; }
|
||||
}
|
||||
```
|
||||
|
||||
### Godot 暂停处理器
|
||||
|
||||
`GodotPauseHandler` 响应暂停栈状态变化,控制 SceneTree.Paused:
|
||||
|
||||
```csharp
|
||||
public class GodotPauseHandler : IPauseHandler
|
||||
{
|
||||
public int Priority => 0;
|
||||
|
||||
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||
{
|
||||
// 只有 Global 组影响 Godot 的全局暂停
|
||||
if (group == PauseGroup.Global)
|
||||
{
|
||||
_tree.Paused = isPaused;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 设置暂停系统
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.architecture;
|
||||
using GFramework.Godot.pause;
|
||||
using GFramework.Core.pause;
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 注册暂停栈管理器
|
||||
var pauseManager = new PauseStackManager();
|
||||
RegisterUtility<IPauseStackManager>(pauseManager);
|
||||
|
||||
// 注册 Godot 暂停处理器
|
||||
var pauseHandler = new GodotPauseHandler(GetTree());
|
||||
pauseManager.RegisterHandler(pauseHandler);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 基本暂停和恢复
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class PauseMenu : Control
|
||||
{
|
||||
private PauseToken _pauseToken;
|
||||
|
||||
public void ShowPauseMenu()
|
||||
{
|
||||
// 暂停游戏
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
_pauseToken = pauseManager.Push("Pause menu opened");
|
||||
|
||||
Show();
|
||||
}
|
||||
|
||||
public void HidePauseMenu()
|
||||
{
|
||||
// 恢复游戏
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
pauseManager.Pop(_pauseToken);
|
||||
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用暂停作用域
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class DialogBox : Control
|
||||
{
|
||||
public async void ShowDialog()
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
|
||||
// 使用 using 语法自动管理暂停
|
||||
using (pauseManager.PauseScope("Dialog shown"))
|
||||
{
|
||||
Show();
|
||||
await ToSignal(GetTree().CreateTimer(3.0f), "timeout");
|
||||
Hide();
|
||||
} // 自动恢复
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 查询暂停状态
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class GameController : Node
|
||||
{
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
|
||||
// 检查是否暂停
|
||||
if (pauseManager.IsPaused())
|
||||
{
|
||||
GD.Print("游戏已暂停");
|
||||
return;
|
||||
}
|
||||
|
||||
// 游戏逻辑
|
||||
UpdateGame(delta);
|
||||
}
|
||||
|
||||
private void UpdateGame(double delta)
|
||||
{
|
||||
// 游戏更新逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 分组暂停
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class GameManager : Node
|
||||
{
|
||||
private PauseToken _gameplayPauseToken;
|
||||
private PauseToken _animationPauseToken;
|
||||
|
||||
// 只暂停游戏逻辑,UI 仍然可以交互
|
||||
public void PauseGameplay()
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
_gameplayPauseToken = pauseManager.Push("Gameplay paused", PauseGroup.Gameplay);
|
||||
}
|
||||
|
||||
public void ResumeGameplay()
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
pauseManager.Pop(_gameplayPauseToken);
|
||||
}
|
||||
|
||||
// 只暂停动画
|
||||
public void PauseAnimations()
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
_animationPauseToken = pauseManager.Push("Animations paused", PauseGroup.Animation);
|
||||
}
|
||||
|
||||
public void ResumeAnimations()
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
pauseManager.Pop(_animationPauseToken);
|
||||
}
|
||||
|
||||
// 检查特定组的暂停状态
|
||||
public bool IsGameplayPaused()
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
return pauseManager.IsPaused(PauseGroup.Gameplay);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 嵌套暂停
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class GameScene : Node
|
||||
{
|
||||
public async void ShowNestedDialogs()
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
|
||||
// 第一层暂停
|
||||
using (pauseManager.PauseScope("First dialog"))
|
||||
{
|
||||
GD.Print($"暂停深度: {pauseManager.GetPauseDepth()}"); // 输出: 1
|
||||
ShowDialog("第一个对话框");
|
||||
await ToSignal(GetTree().CreateTimer(2.0f), "timeout");
|
||||
|
||||
// 第二层暂停
|
||||
using (pauseManager.PauseScope("Second dialog"))
|
||||
{
|
||||
GD.Print($"暂停深度: {pauseManager.GetPauseDepth()}"); // 输出: 2
|
||||
ShowDialog("第二个对话框");
|
||||
await ToSignal(GetTree().CreateTimer(2.0f), "timeout");
|
||||
}
|
||||
|
||||
GD.Print($"暂停深度: {pauseManager.GetPauseDepth()}"); // 输出: 1
|
||||
}
|
||||
|
||||
GD.Print($"暂停深度: {pauseManager.GetPauseDepth()}"); // 输出: 0
|
||||
}
|
||||
|
||||
private void ShowDialog(string message)
|
||||
{
|
||||
GD.Print(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义暂停处理器
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.pause;
|
||||
using Godot;
|
||||
|
||||
// 自定义动画暂停处理器
|
||||
public class AnimationPauseHandler : IPauseHandler
|
||||
{
|
||||
private readonly AnimationPlayer _animationPlayer;
|
||||
|
||||
public AnimationPauseHandler(AnimationPlayer animationPlayer)
|
||||
{
|
||||
_animationPlayer = animationPlayer;
|
||||
}
|
||||
|
||||
public int Priority => 10;
|
||||
|
||||
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||
{
|
||||
// 只响应 Animation 组
|
||||
if (group == PauseGroup.Animation)
|
||||
{
|
||||
if (isPaused)
|
||||
{
|
||||
_animationPlayer.Pause();
|
||||
GD.Print("动画已暂停");
|
||||
}
|
||||
else
|
||||
{
|
||||
_animationPlayer.Play();
|
||||
GD.Print("动画已恢复");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 注册自定义处理器
|
||||
public partial class GameController : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
var animationPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
|
||||
|
||||
var animationHandler = new AnimationPauseHandler(animationPlayer);
|
||||
pauseManager.RegisterHandler(animationHandler);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 音频暂停处理器
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.pause;
|
||||
using Godot;
|
||||
|
||||
public class AudioPauseHandler : IPauseHandler
|
||||
{
|
||||
private readonly AudioStreamPlayer _musicPlayer;
|
||||
private readonly AudioStreamPlayer _sfxPlayer;
|
||||
|
||||
public AudioPauseHandler(AudioStreamPlayer musicPlayer, AudioStreamPlayer sfxPlayer)
|
||||
{
|
||||
_musicPlayer = musicPlayer;
|
||||
_sfxPlayer = sfxPlayer;
|
||||
}
|
||||
|
||||
public int Priority => 20;
|
||||
|
||||
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||
{
|
||||
if (group == PauseGroup.Audio || group == PauseGroup.Global)
|
||||
{
|
||||
if (isPaused)
|
||||
{
|
||||
_musicPlayer.StreamPaused = true;
|
||||
_sfxPlayer.StreamPaused = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_musicPlayer.StreamPaused = false;
|
||||
_sfxPlayer.StreamPaused = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 节点暂停模式控制
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class GameNode : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
// 设置节点在暂停时的行为
|
||||
|
||||
// 暂停时停止处理
|
||||
ProcessMode = ProcessModeEnum.Pausable;
|
||||
|
||||
// 暂停时继续处理(用于 UI)
|
||||
// ProcessMode = ProcessModeEnum.Always;
|
||||
|
||||
// 暂停时停止,且子节点也停止
|
||||
// ProcessMode = ProcessModeEnum.Inherit;
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
// 当 SceneTree.Paused = true 且 ProcessMode = Pausable 时
|
||||
// 此方法不会被调用
|
||||
UpdateGameLogic(delta);
|
||||
}
|
||||
|
||||
private void UpdateGameLogic(double delta)
|
||||
{
|
||||
// 游戏逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UI 在暂停时继续工作
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class PauseMenuUI : Control
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
// UI 在游戏暂停时仍然可以交互
|
||||
ProcessMode = ProcessModeEnum.Always;
|
||||
|
||||
GetNode<Button>("ResumeButton").Pressed += OnResumePressed;
|
||||
GetNode<Button>("QuitButton").Pressed += OnQuitPressed;
|
||||
}
|
||||
|
||||
private void OnResumePressed()
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
|
||||
// 获取所有暂停原因
|
||||
var reasons = pauseManager.GetPauseReasons();
|
||||
GD.Print($"当前暂停原因: {string.Join(", ", reasons)}");
|
||||
|
||||
// 清空所有暂停
|
||||
pauseManager.ClearAll();
|
||||
|
||||
Hide();
|
||||
}
|
||||
|
||||
private void OnQuitPressed()
|
||||
{
|
||||
GetTree().Quit();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 监听暂停状态变化
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class PauseIndicator : Label
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
|
||||
// 订阅暂停状态变化事件
|
||||
pauseManager.OnPauseStateChanged += OnPauseStateChanged;
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
pauseManager.OnPauseStateChanged -= OnPauseStateChanged;
|
||||
}
|
||||
|
||||
private void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||
{
|
||||
if (group == PauseGroup.Global)
|
||||
{
|
||||
Text = isPaused ? "游戏已暂停" : "游戏运行中";
|
||||
Visible = isPaused;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 调试暂停状态
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class PauseDebugger : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
pauseManager.OnPauseStateChanged += OnPauseStateChanged;
|
||||
}
|
||||
|
||||
private void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
|
||||
GD.Print($"=== 暂停状态变化 ===");
|
||||
GD.Print($"组: {group}");
|
||||
GD.Print($"状态: {(isPaused ? "暂停" : "恢复")}");
|
||||
GD.Print($"深度: {pauseManager.GetPauseDepth(group)}");
|
||||
|
||||
var reasons = pauseManager.GetPauseReasons(group);
|
||||
if (reasons.Count > 0)
|
||||
{
|
||||
GD.Print($"原因:");
|
||||
foreach (var reason in reasons)
|
||||
{
|
||||
GD.Print($" - {reason}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void _Input(InputEvent @event)
|
||||
{
|
||||
// 按 F12 显示所有暂停状态
|
||||
if (@event is InputEventKey keyEvent && keyEvent.Pressed && keyEvent.Keycode == Key.F12)
|
||||
{
|
||||
PrintAllPauseStates();
|
||||
}
|
||||
}
|
||||
|
||||
private void PrintAllPauseStates()
|
||||
{
|
||||
var pauseManager = this.GetUtility<IPauseStackManager>();
|
||||
|
||||
GD.Print("=== 所有暂停状态 ===");
|
||||
|
||||
foreach (PauseGroup group in Enum.GetValues(typeof(PauseGroup)))
|
||||
{
|
||||
var isPaused = pauseManager.IsPaused(group);
|
||||
var depth = pauseManager.GetPauseDepth(group);
|
||||
|
||||
if (depth > 0)
|
||||
{
|
||||
GD.Print($"{group}: 暂停 (深度: {depth})");
|
||||
var reasons = pauseManager.GetPauseReasons(group);
|
||||
foreach (var reason in reasons)
|
||||
{
|
||||
GD.Print($" - {reason}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用暂停作用域管理生命周期**:避免忘记恢复
|
||||
```csharp
|
||||
✓ using (pauseManager.PauseScope("Dialog")) { ... }
|
||||
✗ var token = pauseManager.Push("Dialog"); // 可能忘记 Pop
|
||||
```
|
||||
|
||||
2. **为暂停提供清晰的原因**:便于调试
|
||||
```csharp
|
||||
✓ pauseManager.Push("Inventory opened");
|
||||
✗ pauseManager.Push("pause"); // 原因不明确
|
||||
```
|
||||
|
||||
3. **使用正确的暂停组**:避免影响不该暂停的系统
|
||||
```csharp
|
||||
✓ pauseManager.Push("Menu", PauseGroup.Gameplay); // 只暂停游戏逻辑
|
||||
✗ pauseManager.Push("Menu", PauseGroup.Global); // 暂停所有系统包括 UI
|
||||
```
|
||||
|
||||
4. **UI 节点设置 ProcessMode.Always**:确保 UI 在暂停时可用
|
||||
```csharp
|
||||
public override void _Ready()
|
||||
{
|
||||
ProcessMode = ProcessModeEnum.Always;
|
||||
}
|
||||
```
|
||||
|
||||
5. **游戏逻辑节点设置 ProcessMode.Pausable**:确保暂停时停止
|
||||
```csharp
|
||||
public override void _Ready()
|
||||
{
|
||||
ProcessMode = ProcessModeEnum.Pausable;
|
||||
}
|
||||
```
|
||||
|
||||
6. **保存暂停令牌以便恢复**:确保能正确恢复暂停
|
||||
```csharp
|
||||
private PauseToken _pauseToken;
|
||||
|
||||
public void Pause()
|
||||
{
|
||||
_pauseToken = pauseManager.Push("Paused");
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
pauseManager.Pop(_pauseToken);
|
||||
}
|
||||
```
|
||||
|
||||
7. **使用事件监听暂停状态**:实现响应式 UI
|
||||
```csharp
|
||||
pauseManager.OnPauseStateChanged += (group, isPaused) =>
|
||||
{
|
||||
UpdateUI(isPaused);
|
||||
};
|
||||
```
|
||||
|
||||
8. **清理时注销事件监听**:避免内存泄漏
|
||||
```csharp
|
||||
public override void _ExitTree()
|
||||
{
|
||||
pauseManager.OnPauseStateChanged -= OnPauseStateChanged;
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:如何暂停游戏但保持 UI 可交互?
|
||||
|
||||
**解答**:
|
||||
使用 `PauseGroup.Gameplay` 而不是 `PauseGroup.Global`:
|
||||
|
||||
```csharp
|
||||
// 只暂停游戏逻辑
|
||||
pauseManager.Push("Menu opened", PauseGroup.Gameplay);
|
||||
|
||||
// UI 节点设置为 Always
|
||||
public override void _Ready()
|
||||
{
|
||||
ProcessMode = ProcessModeEnum.Always;
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:嵌套暂停如何工作?
|
||||
|
||||
**解答**:
|
||||
暂停栈支持嵌套,需要所有 Pop 才能完全恢复:
|
||||
|
||||
```csharp
|
||||
var token1 = pauseManager.Push("First"); // 深度: 1, 暂停
|
||||
var token2 = pauseManager.Push("Second"); // 深度: 2, 仍然暂停
|
||||
|
||||
pauseManager.Pop(token1); // 深度: 1, 仍然暂停
|
||||
pauseManager.Pop(token2); // 深度: 0, 恢复
|
||||
```
|
||||
|
||||
### 问题:如何实现自定义暂停行为?
|
||||
|
||||
**解答**:
|
||||
实现 `IPauseHandler` 接口并注册:
|
||||
|
||||
```csharp
|
||||
public class CustomPauseHandler : IPauseHandler
|
||||
{
|
||||
public int Priority => 0;
|
||||
|
||||
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||
{
|
||||
// 自定义暂停逻辑
|
||||
}
|
||||
}
|
||||
|
||||
pauseManager.RegisterHandler(new CustomPauseHandler());
|
||||
```
|
||||
|
||||
### 问题:暂停处理器的优先级如何工作?
|
||||
|
||||
**解答**:
|
||||
数值越小优先级越高,按优先级顺序调用:
|
||||
|
||||
```csharp
|
||||
handler1.Priority = 0; // 最先调用
|
||||
handler2.Priority = 10; // 其次调用
|
||||
handler3.Priority = 20; // 最后调用
|
||||
```
|
||||
|
||||
### 问题:如何清空所有暂停?
|
||||
|
||||
**解答**:
|
||||
使用 `ClearAll()` 或 `ClearGroup()`:
|
||||
|
||||
```csharp
|
||||
// 清空所有组的暂停
|
||||
pauseManager.ClearAll();
|
||||
|
||||
// 只清空特定组
|
||||
pauseManager.ClearGroup(PauseGroup.Gameplay);
|
||||
```
|
||||
|
||||
### 问题:暂停系统是线程安全的吗?
|
||||
|
||||
**解答**:
|
||||
是的,`PauseStackManager` 使用 `ReaderWriterLockSlim` 确保线程安全:
|
||||
|
||||
```csharp
|
||||
// 可以在多个线程中安全调用
|
||||
Task.Run(() => pauseManager.Push("Thread 1"));
|
||||
Task.Run(() => pauseManager.Push("Thread 2"));
|
||||
```
|
||||
|
||||
### 问题:如何调试暂停问题?
|
||||
|
||||
**解答**:
|
||||
使用暂停状态查询方法:
|
||||
|
||||
```csharp
|
||||
// 检查是否暂停
|
||||
bool isPaused = pauseManager.IsPaused(PauseGroup.Global);
|
||||
|
||||
// 获取暂停深度
|
||||
int depth = pauseManager.GetPauseDepth(PauseGroup.Global);
|
||||
|
||||
// 获取所有暂停原因
|
||||
var reasons = pauseManager.GetPauseReasons(PauseGroup.Global);
|
||||
foreach (var reason in reasons)
|
||||
{
|
||||
GD.Print($"暂停原因: {reason}");
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
|
||||
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景集成
|
||||
- [Godot UI 系统](/zh-CN/godot/ui) - Godot UI 集成
|
||||
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
|
||||
684
docs/zh-CN/godot/pool.md
Normal file
684
docs/zh-CN/godot/pool.md
Normal file
@ -0,0 +1,684 @@
|
||||
---
|
||||
title: Godot 节点池系统
|
||||
description: Godot 节点池系统提供了高性能的节点复用机制,减少频繁创建和销毁节点带来的性能开销。
|
||||
---
|
||||
|
||||
# Godot 节点池系统
|
||||
|
||||
## 概述
|
||||
|
||||
Godot 节点池系统是 GFramework.Godot 中用于管理和复用 Godot
|
||||
节点的高性能组件。通过对象池模式,它可以显著减少频繁创建和销毁节点带来的性能开销,特别适用于需要大量动态生成节点的场景,如子弹、特效、敌人等。
|
||||
|
||||
节点池系统基于 GFramework 核心的对象池系统,专门针对 Godot 节点进行了优化,提供了完整的生命周期管理和统计功能。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 节点复用机制,减少 GC 压力
|
||||
- 自动生命周期管理
|
||||
- 池容量限制和预热功能
|
||||
- 详细的统计信息
|
||||
- 类型安全的泛型设计
|
||||
- 与 Godot PackedScene 无缝集成
|
||||
|
||||
**性能优势**:
|
||||
|
||||
- 减少内存分配和垃圾回收
|
||||
- 降低节点实例化开销
|
||||
- 提高游戏运行时性能
|
||||
- 优化大量对象场景的帧率
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 节点池
|
||||
|
||||
节点池是一个存储可复用节点的容器。当需要节点时从池中获取,使用完毕后归还到池中,而不是销毁。这种复用机制可以显著提升性能。
|
||||
|
||||
### 可池化节点
|
||||
|
||||
实现 `IPoolableNode` 接口的节点可以被对象池管理。接口定义了节点在池中的生命周期回调:
|
||||
|
||||
```csharp
|
||||
public interface IPoolableNode : IPoolableObject
|
||||
{
|
||||
// 从池中获取时调用
|
||||
void OnAcquire();
|
||||
|
||||
// 归还到池中时调用
|
||||
void OnRelease();
|
||||
|
||||
// 池被销毁时调用
|
||||
void OnPoolDestroy();
|
||||
|
||||
// 转换为 Node 类型
|
||||
Node AsNode();
|
||||
}
|
||||
```
|
||||
|
||||
### 节点复用
|
||||
|
||||
节点复用是指重复使用已创建的节点实例,而不是每次都创建新实例。这可以:
|
||||
|
||||
- 减少内存分配
|
||||
- 降低 GC 压力
|
||||
- 提高实例化速度
|
||||
- 优化运行时性能
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 创建可池化节点
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.pool;
|
||||
|
||||
public partial class Bullet : Node2D, IPoolableNode
|
||||
{
|
||||
private Vector2 _velocity;
|
||||
private float _lifetime;
|
||||
|
||||
public void OnAcquire()
|
||||
{
|
||||
// 从池中获取时重置状态
|
||||
_lifetime = 5.0f;
|
||||
Show();
|
||||
SetProcess(true);
|
||||
}
|
||||
|
||||
public void OnRelease()
|
||||
{
|
||||
// 归还到池中时清理状态
|
||||
Hide();
|
||||
SetProcess(false);
|
||||
_velocity = Vector2.Zero;
|
||||
}
|
||||
|
||||
public void OnPoolDestroy()
|
||||
{
|
||||
// 池被销毁时的清理工作
|
||||
QueueFree();
|
||||
}
|
||||
|
||||
public Node AsNode()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
public void Initialize(Vector2 position, Vector2 velocity)
|
||||
{
|
||||
Position = position;
|
||||
_velocity = velocity;
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
Position += _velocity * (float)delta;
|
||||
_lifetime -= (float)delta;
|
||||
|
||||
if (_lifetime <= 0)
|
||||
{
|
||||
// 归还到池中
|
||||
ReturnToPool();
|
||||
}
|
||||
}
|
||||
|
||||
private void ReturnToPool()
|
||||
{
|
||||
// 通过池系统归还
|
||||
var poolSystem = this.GetSystem<BulletPoolSystem>();
|
||||
poolSystem.Release("Bullet", this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 创建节点池系统
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.pool;
|
||||
|
||||
public class BulletPoolSystem : AbstractNodePoolSystem<string, Bullet>
|
||||
{
|
||||
protected override PackedScene LoadScene(string key)
|
||||
{
|
||||
// 根据键加载对应的场景
|
||||
return key switch
|
||||
{
|
||||
"Bullet" => GD.Load<PackedScene>("res://prefabs/Bullet.tscn"),
|
||||
"EnemyBullet" => GD.Load<PackedScene>("res://prefabs/EnemyBullet.tscn"),
|
||||
_ => throw new ArgumentException($"Unknown bullet type: {key}")
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 预热池,提前创建一些对象
|
||||
Prewarm("Bullet", 50);
|
||||
Prewarm("EnemyBullet", 30);
|
||||
|
||||
// 设置最大容量
|
||||
SetMaxCapacity("Bullet", 100);
|
||||
SetMaxCapacity("EnemyBullet", 50);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册节点池系统
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.architecture;
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 注册节点池系统
|
||||
RegisterSystem<BulletPoolSystem>(new BulletPoolSystem());
|
||||
RegisterSystem<EffectPoolSystem>(new EffectPoolSystem());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用节点池
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class Player : Node2D
|
||||
{
|
||||
private BulletPoolSystem _bulletPool;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_bulletPool = this.GetSystem<BulletPoolSystem>();
|
||||
}
|
||||
|
||||
public void Shoot()
|
||||
{
|
||||
// 从池中获取子弹
|
||||
var bullet = _bulletPool.Acquire("Bullet");
|
||||
|
||||
// 初始化子弹
|
||||
bullet.Initialize(GlobalPosition, Vector2.Right.Rotated(Rotation) * 500);
|
||||
|
||||
// 添加到场景树
|
||||
GetParent().AddChild(bullet.AsNode());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 多类型节点池
|
||||
|
||||
```csharp
|
||||
public class EffectPoolSystem : AbstractNodePoolSystem<string, PoolableEffect>
|
||||
{
|
||||
protected override PackedScene LoadScene(string key)
|
||||
{
|
||||
return key switch
|
||||
{
|
||||
"Explosion" => GD.Load<PackedScene>("res://effects/Explosion.tscn"),
|
||||
"Hit" => GD.Load<PackedScene>("res://effects/Hit.tscn"),
|
||||
"Smoke" => GD.Load<PackedScene>("res://effects/Smoke.tscn"),
|
||||
"Spark" => GD.Load<PackedScene>("res://effects/Spark.tscn"),
|
||||
_ => throw new ArgumentException($"Unknown effect type: {key}")
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 为不同类型的特效设置不同的池配置
|
||||
Prewarm("Explosion", 10);
|
||||
SetMaxCapacity("Explosion", 20);
|
||||
|
||||
Prewarm("Hit", 20);
|
||||
SetMaxCapacity("Hit", 50);
|
||||
|
||||
Prewarm("Smoke", 15);
|
||||
SetMaxCapacity("Smoke", 30);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用特效池
|
||||
public partial class Enemy : Node2D
|
||||
{
|
||||
public void Die()
|
||||
{
|
||||
var effectPool = this.GetSystem<EffectPoolSystem>();
|
||||
var explosion = effectPool.Acquire("Explosion");
|
||||
|
||||
explosion.AsNode().GlobalPosition = GlobalPosition;
|
||||
GetParent().AddChild(explosion.AsNode());
|
||||
|
||||
QueueFree();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 自动归还的节点
|
||||
|
||||
```csharp
|
||||
public partial class PoolableEffect : Node2D, IPoolableNode
|
||||
{
|
||||
private AnimationPlayer _animationPlayer;
|
||||
private EffectPoolSystem _poolSystem;
|
||||
private string _effectKey;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_animationPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
|
||||
_poolSystem = this.GetSystem<EffectPoolSystem>();
|
||||
|
||||
// 动画播放完毕后自动归还
|
||||
_animationPlayer.AnimationFinished += OnAnimationFinished;
|
||||
}
|
||||
|
||||
public void OnAcquire()
|
||||
{
|
||||
Show();
|
||||
_animationPlayer.Play("default");
|
||||
}
|
||||
|
||||
public void OnRelease()
|
||||
{
|
||||
Hide();
|
||||
_animationPlayer.Stop();
|
||||
}
|
||||
|
||||
public void OnPoolDestroy()
|
||||
{
|
||||
_animationPlayer.AnimationFinished -= OnAnimationFinished;
|
||||
QueueFree();
|
||||
}
|
||||
|
||||
public Node AsNode() => this;
|
||||
|
||||
public void SetEffectKey(string key)
|
||||
{
|
||||
_effectKey = key;
|
||||
}
|
||||
|
||||
private void OnAnimationFinished(StringName animName)
|
||||
{
|
||||
// 动画播放完毕,自动归还到池中
|
||||
_poolSystem.Release(_effectKey, this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 池容量管理
|
||||
|
||||
```csharp
|
||||
public class DynamicPoolSystem : AbstractNodePoolSystem<string, PoolableEnemy>
|
||||
{
|
||||
protected override PackedScene LoadScene(string key)
|
||||
{
|
||||
return GD.Load<PackedScene>($"res://enemies/{key}.tscn");
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 初始配置
|
||||
SetMaxCapacity("Slime", 50);
|
||||
SetMaxCapacity("Goblin", 30);
|
||||
SetMaxCapacity("Boss", 5);
|
||||
}
|
||||
|
||||
// 动态调整池容量
|
||||
public void AdjustPoolCapacity(string key, int newCapacity)
|
||||
{
|
||||
var currentSize = GetPoolSize(key);
|
||||
var activeCount = GetActiveCount(key);
|
||||
|
||||
GD.Print($"池 '{key}' 当前状态:");
|
||||
GD.Print($" 可用: {currentSize}");
|
||||
GD.Print($" 活跃: {activeCount}");
|
||||
GD.Print($" 新容量: {newCapacity}");
|
||||
|
||||
SetMaxCapacity(key, newCapacity);
|
||||
}
|
||||
|
||||
// 根据游戏阶段预热
|
||||
public void PrewarmForStage(int stage)
|
||||
{
|
||||
switch (stage)
|
||||
{
|
||||
case 1:
|
||||
Prewarm("Slime", 20);
|
||||
break;
|
||||
case 2:
|
||||
Prewarm("Slime", 30);
|
||||
Prewarm("Goblin", 15);
|
||||
break;
|
||||
case 3:
|
||||
Prewarm("Goblin", 25);
|
||||
Prewarm("Boss", 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 池统计和监控
|
||||
|
||||
```csharp
|
||||
public partial class PoolMonitor : Control
|
||||
{
|
||||
private BulletPoolSystem _bulletPool;
|
||||
private Label _statsLabel;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_bulletPool = this.GetSystem<BulletPoolSystem>();
|
||||
_statsLabel = GetNode<Label>("StatsLabel");
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
// 获取统计信息
|
||||
var stats = _bulletPool.GetStatistics("Bullet");
|
||||
|
||||
// 显示统计信息
|
||||
_statsLabel.Text = $@"
|
||||
子弹池统计:
|
||||
可用对象: {stats.AvailableCount}
|
||||
活跃对象: {stats.ActiveCount}
|
||||
最大容量: {stats.MaxCapacity}
|
||||
总创建数: {stats.TotalCreated}
|
||||
总获取数: {stats.TotalAcquired}
|
||||
总释放数: {stats.TotalReleased}
|
||||
总销毁数: {stats.TotalDestroyed}
|
||||
复用率: {CalculateReuseRate(stats):P2}
|
||||
";
|
||||
}
|
||||
|
||||
private float CalculateReuseRate(PoolStatistics stats)
|
||||
{
|
||||
if (stats.TotalAcquired == 0) return 0;
|
||||
return 1.0f - (float)stats.TotalCreated / stats.TotalAcquired;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 条件释放和清理
|
||||
|
||||
```csharp
|
||||
public class SmartPoolSystem : AbstractNodePoolSystem<string, PoolableNode>
|
||||
{
|
||||
protected override PackedScene LoadScene(string key)
|
||||
{
|
||||
return GD.Load<PackedScene>($"res://poolable/{key}.tscn");
|
||||
}
|
||||
|
||||
// 清理超出屏幕的对象
|
||||
public void CleanupOffscreenObjects(Rect2 screenRect)
|
||||
{
|
||||
foreach (var pool in Pools)
|
||||
{
|
||||
var stats = GetStatistics(pool.Key);
|
||||
GD.Print($"清理前 '{pool.Key}': 活跃={stats.ActiveCount}");
|
||||
}
|
||||
}
|
||||
|
||||
// 根据内存压力调整池大小
|
||||
public void AdjustForMemoryPressure(float memoryUsage)
|
||||
{
|
||||
if (memoryUsage > 0.8f)
|
||||
{
|
||||
// 内存压力大,减小池容量
|
||||
foreach (var pool in Pools)
|
||||
{
|
||||
var currentCapacity = GetStatistics(pool.Key).MaxCapacity;
|
||||
SetMaxCapacity(pool.Key, Math.Max(10, currentCapacity / 2));
|
||||
}
|
||||
|
||||
GD.Print("内存压力大,减小池容量");
|
||||
}
|
||||
else if (memoryUsage < 0.5f)
|
||||
{
|
||||
// 内存充足,增加池容量
|
||||
foreach (var pool in Pools)
|
||||
{
|
||||
var currentCapacity = GetStatistics(pool.Key).MaxCapacity;
|
||||
SetMaxCapacity(pool.Key, currentCapacity * 2);
|
||||
}
|
||||
|
||||
GD.Print("内存充足,增加池容量");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **在 OnAcquire 中重置状态**:确保对象从池中获取时处于干净状态
|
||||
```csharp
|
||||
public void OnAcquire()
|
||||
{
|
||||
// 重置所有状态
|
||||
Position = Vector2.Zero;
|
||||
Rotation = 0;
|
||||
Scale = Vector2.One;
|
||||
Modulate = Colors.White;
|
||||
Show();
|
||||
}
|
||||
```
|
||||
|
||||
2. **在 OnRelease 中清理资源**:避免内存泄漏
|
||||
```csharp
|
||||
public void OnRelease()
|
||||
{
|
||||
// 清理引用
|
||||
_target = null;
|
||||
_callbacks.Clear();
|
||||
|
||||
// 停止所有动画和计时器
|
||||
_animationPlayer.Stop();
|
||||
_timer.Stop();
|
||||
|
||||
Hide();
|
||||
}
|
||||
```
|
||||
|
||||
3. **合理设置池容量**:根据实际需求设置最大容量
|
||||
```csharp
|
||||
// 根据游戏设计设置合理的容量
|
||||
SetMaxCapacity("Bullet", 100); // 屏幕上最多100个子弹
|
||||
SetMaxCapacity("Enemy", 50); // 同时最多50个敌人
|
||||
```
|
||||
|
||||
4. **使用预热优化启动性能**:在游戏开始前预创建对象
|
||||
```csharp
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 在加载界面预热池
|
||||
Prewarm("Bullet", 50);
|
||||
Prewarm("Effect", 30);
|
||||
}
|
||||
```
|
||||
|
||||
5. **及时归还对象**:使用完毕后立即归还到池中
|
||||
```csharp
|
||||
✓ poolSystem.Release("Bullet", bullet); // 使用完立即归还
|
||||
✗ // 忘记归还,导致池耗尽
|
||||
```
|
||||
|
||||
6. **监控池统计信息**:定期检查池的使用情况
|
||||
```csharp
|
||||
var stats = poolSystem.GetStatistics("Bullet");
|
||||
if (stats.ActiveCount > stats.MaxCapacity * 0.9f)
|
||||
{
|
||||
GD.PrintErr("警告:子弹池接近容量上限");
|
||||
}
|
||||
```
|
||||
|
||||
7. **避免在池中存储过大的对象**:大对象应该按需创建
|
||||
```csharp
|
||||
✓ 小对象:子弹、特效、UI元素
|
||||
✗ 大对象:完整的关卡、大型模型
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:什么时候应该使用节点池?
|
||||
|
||||
**解答**:
|
||||
以下场景适合使用节点池:
|
||||
|
||||
- 频繁创建和销毁的对象(子弹、特效)
|
||||
- 数量较多的对象(敌人、道具)
|
||||
- 生命周期短的对象(粒子、UI提示)
|
||||
- 性能敏感的场景(移动平台、大量对象)
|
||||
|
||||
不适合使用节点池的场景:
|
||||
|
||||
- 只创建一次的对象(玩家、UI界面)
|
||||
- 数量很少的对象(Boss、关键NPC)
|
||||
- 状态复杂难以重置的对象
|
||||
|
||||
### 问题:如何确定合适的池容量?
|
||||
|
||||
**解答**:
|
||||
根据游戏实际情况设置:
|
||||
|
||||
```csharp
|
||||
// 1. 测量峰值使用量
|
||||
var stats = poolSystem.GetStatistics("Bullet");
|
||||
GD.Print($"峰值活跃数: {stats.ActiveCount}");
|
||||
|
||||
// 2. 设置容量为峰值的 1.2-1.5 倍
|
||||
SetMaxCapacity("Bullet", (int)(peakCount * 1.3f));
|
||||
|
||||
// 3. 监控并调整
|
||||
if (stats.TotalDestroyed > stats.TotalCreated * 0.1f)
|
||||
{
|
||||
// 销毁过多,容量可能太小
|
||||
SetMaxCapacity("Bullet", stats.MaxCapacity * 2);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:对象没有正确归还到池中怎么办?
|
||||
|
||||
**解答**:
|
||||
检查以下几点:
|
||||
|
||||
```csharp
|
||||
// 1. 确保调用了 Release
|
||||
poolSystem.Release("Bullet", bullet);
|
||||
|
||||
// 2. 检查是否使用了正确的键
|
||||
✓ poolSystem.Release("Bullet", bullet);
|
||||
✗ poolSystem.Release("Enemy", bullet); // 错误的键
|
||||
|
||||
// 3. 避免重复释放
|
||||
if (!_isReleased)
|
||||
{
|
||||
poolSystem.Release("Bullet", this);
|
||||
_isReleased = true;
|
||||
}
|
||||
|
||||
// 4. 使用统计信息诊断
|
||||
var stats = poolSystem.GetStatistics("Bullet");
|
||||
if (stats.ActiveCount != stats.TotalAcquired - stats.TotalReleased)
|
||||
{
|
||||
GD.PrintErr("检测到对象泄漏");
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:池中的对象状态没有正确重置?
|
||||
|
||||
**解答**:
|
||||
在 OnAcquire 中完整重置所有状态:
|
||||
|
||||
```csharp
|
||||
public void OnAcquire()
|
||||
{
|
||||
// 重置变换
|
||||
Position = Vector2.Zero;
|
||||
Rotation = 0;
|
||||
Scale = Vector2.One;
|
||||
|
||||
// 重置视觉
|
||||
Modulate = Colors.White;
|
||||
Visible = true;
|
||||
|
||||
// 重置物理
|
||||
if (this is RigidBody2D rb)
|
||||
{
|
||||
rb.LinearVelocity = Vector2.Zero;
|
||||
rb.AngularVelocity = 0;
|
||||
}
|
||||
|
||||
// 重置逻辑状态
|
||||
_health = _maxHealth;
|
||||
_isActive = true;
|
||||
|
||||
// 重启动画
|
||||
_animationPlayer.Play("idle");
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何处理节点的父子关系?
|
||||
|
||||
**解答**:
|
||||
在归还前移除父节点:
|
||||
|
||||
```csharp
|
||||
public void ReturnToPool()
|
||||
{
|
||||
// 从场景树中移除
|
||||
if (GetParent() != null)
|
||||
{
|
||||
GetParent().RemoveChild(this);
|
||||
}
|
||||
|
||||
// 归还到池中
|
||||
var poolSystem = this.GetSystem<BulletPoolSystem>();
|
||||
poolSystem.Release("Bullet", this);
|
||||
}
|
||||
|
||||
// 获取时重新添加到场景树
|
||||
var bullet = poolSystem.Acquire("Bullet");
|
||||
GetParent().AddChild(bullet.AsNode());
|
||||
```
|
||||
|
||||
### 问题:池系统对性能的提升有多大?
|
||||
|
||||
**解答**:
|
||||
性能提升取决于具体场景:
|
||||
|
||||
```csharp
|
||||
// 测试代码
|
||||
var stopwatch = new Stopwatch();
|
||||
|
||||
// 不使用池
|
||||
stopwatch.Start();
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
var bullet = scene.Instantiate<Bullet>();
|
||||
bullet.QueueFree();
|
||||
}
|
||||
stopwatch.Stop();
|
||||
GD.Print($"不使用池: {stopwatch.ElapsedMilliseconds}ms");
|
||||
|
||||
// 使用池
|
||||
stopwatch.Restart();
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
var bullet = poolSystem.Acquire("Bullet");
|
||||
poolSystem.Release("Bullet", bullet);
|
||||
}
|
||||
stopwatch.Stop();
|
||||
GD.Print($"使用池: {stopwatch.ElapsedMilliseconds}ms");
|
||||
|
||||
// 典型结果:使用池可以提升 3-10 倍性能
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [对象池系统](/zh-CN/core/pool) - 核心对象池实现
|
||||
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
|
||||
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景管理
|
||||
- [性能优化](/zh-CN/core/pool) - 性能优化最佳实践
|
||||
637
docs/zh-CN/godot/resource.md
Normal file
637
docs/zh-CN/godot/resource.md
Normal file
@ -0,0 +1,637 @@
|
||||
---
|
||||
title: Godot 资源仓储系统
|
||||
description: Godot 资源仓储系统提供了 Godot Resource 的集中管理和高效加载功能。
|
||||
---
|
||||
|
||||
# Godot 资源仓储系统
|
||||
|
||||
## 概述
|
||||
|
||||
Godot 资源仓储系统是 GFramework.Godot 中用于管理 Godot Resource
|
||||
资源的核心组件。它提供了基于键值对的资源存储、批量加载、路径扫描等功能,让你可以高效地组织和访问游戏中的各类资源。
|
||||
|
||||
通过资源仓储系统,你可以将 Godot 的 `.tres` 和 `.res` 资源文件集中管理,支持按键快速查找、批量预加载、递归扫描目录等功能,简化资源管理流程。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 基于键值对的资源管理
|
||||
- 支持 Godot Resource 类型
|
||||
- 路径扫描和批量加载
|
||||
- 递归目录遍历
|
||||
- 类型安全的资源访问
|
||||
- 与 GFramework 架构集成
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 资源仓储接口
|
||||
|
||||
`IResourceRepository<TKey, TResource>` 定义了资源仓储的基本操作:
|
||||
|
||||
```csharp
|
||||
public interface IResourceRepository<in TKey, TResource> : IRepository<TKey, TResource>
|
||||
where TResource : Resource
|
||||
{
|
||||
void LoadFromPath(IEnumerable<string> paths);
|
||||
void LoadFromPath(params string[] paths);
|
||||
void LoadFromPathRecursive(IEnumerable<string> paths);
|
||||
void LoadFromPathRecursive(params string[] paths);
|
||||
}
|
||||
```
|
||||
|
||||
### 资源仓储实现
|
||||
|
||||
`GodotResourceRepository<TKey, TResource>` 提供了完整的实现:
|
||||
|
||||
```csharp
|
||||
public class GodotResourceRepository<TKey, TResource>
|
||||
: IResourceRepository<TKey, TResource>
|
||||
where TResource : Resource, IHasKey<TKey>
|
||||
where TKey : notnull
|
||||
{
|
||||
public void Add(TKey key, TResource value);
|
||||
public TResource Get(TKey key);
|
||||
public bool TryGet(TKey key, out TResource value);
|
||||
public IReadOnlyCollection<TResource> GetAll();
|
||||
public bool Contains(TKey key);
|
||||
public void Remove(TKey key);
|
||||
public void Clear();
|
||||
}
|
||||
```
|
||||
|
||||
### 资源键接口
|
||||
|
||||
资源必须实现 `IHasKey<TKey>` 接口:
|
||||
|
||||
```csharp
|
||||
public interface IHasKey<out TKey>
|
||||
{
|
||||
TKey Key { get; }
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 定义资源类型
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Core.Abstractions.bases;
|
||||
|
||||
// 定义资源数据类
|
||||
[GlobalClass]
|
||||
public partial class ItemData : Resource, IHasKey<string>
|
||||
{
|
||||
[Export]
|
||||
public string Id { get; set; }
|
||||
|
||||
[Export]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Export]
|
||||
public string Description { get; set; }
|
||||
|
||||
[Export]
|
||||
public Texture2D Icon { get; set; }
|
||||
|
||||
[Export]
|
||||
public int MaxStack { get; set; } = 99;
|
||||
|
||||
// 实现 IHasKey 接口
|
||||
public string Key => Id;
|
||||
}
|
||||
```
|
||||
|
||||
### 创建资源仓储
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.data;
|
||||
|
||||
public class ItemRepository : GodotResourceRepository<string, ItemData>
|
||||
{
|
||||
public ItemRepository()
|
||||
{
|
||||
// 从指定路径加载所有物品资源
|
||||
LoadFromPath("res://data/items");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册到架构
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.architecture;
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 注册物品仓储
|
||||
var itemRepo = new ItemRepository();
|
||||
RegisterUtility<IResourceRepository<string, ItemData>>(itemRepo);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用资源仓储
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class InventoryController : Node
|
||||
{
|
||||
private IResourceRepository<string, ItemData> _itemRepo;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 获取资源仓储
|
||||
_itemRepo = this.GetUtility<IResourceRepository<string, ItemData>>();
|
||||
|
||||
// 使用资源
|
||||
ShowItemInfo("sword_001");
|
||||
}
|
||||
|
||||
private void ShowItemInfo(string itemId)
|
||||
{
|
||||
// 获取物品数据
|
||||
if (_itemRepo.TryGet(itemId, out var itemData))
|
||||
{
|
||||
GD.Print($"物品: {itemData.Name}");
|
||||
GD.Print($"描述: {itemData.Description}");
|
||||
GD.Print($"最大堆叠: {itemData.MaxStack}");
|
||||
}
|
||||
else
|
||||
{
|
||||
GD.Print($"物品 {itemId} 不存在");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 递归加载资源
|
||||
|
||||
```csharp
|
||||
public class AssetRepository : GodotResourceRepository<string, AssetData>
|
||||
{
|
||||
public AssetRepository()
|
||||
{
|
||||
// 递归加载所有子目录中的资源
|
||||
LoadFromPathRecursive("res://assets");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 多路径加载
|
||||
|
||||
```csharp
|
||||
public class ConfigRepository : GodotResourceRepository<string, ConfigData>
|
||||
{
|
||||
public ConfigRepository()
|
||||
{
|
||||
// 从多个路径加载资源
|
||||
LoadFromPath(
|
||||
"res://config/gameplay",
|
||||
"res://config/ui",
|
||||
"res://config/audio"
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 动态添加资源
|
||||
|
||||
```csharp
|
||||
public partial class ResourceManager : Node
|
||||
{
|
||||
private IResourceRepository<string, ItemData> _itemRepo;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_itemRepo = this.GetUtility<IResourceRepository<string, ItemData>>();
|
||||
}
|
||||
|
||||
public void AddCustomItem(ItemData item)
|
||||
{
|
||||
// 动态添加资源
|
||||
_itemRepo.Add(item.Id, item);
|
||||
GD.Print($"添加物品: {item.Name}");
|
||||
}
|
||||
|
||||
public void RemoveItem(string itemId)
|
||||
{
|
||||
// 移除资源
|
||||
if (_itemRepo.Contains(itemId))
|
||||
{
|
||||
_itemRepo.Remove(itemId);
|
||||
GD.Print($"移除物品: {itemId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 获取所有资源
|
||||
|
||||
```csharp
|
||||
public partial class ItemListUI : Control
|
||||
{
|
||||
private IResourceRepository<string, ItemData> _itemRepo;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_itemRepo = this.GetUtility<IResourceRepository<string, ItemData>>();
|
||||
DisplayAllItems();
|
||||
}
|
||||
|
||||
private void DisplayAllItems()
|
||||
{
|
||||
// 获取所有物品
|
||||
var allItems = _itemRepo.GetAll();
|
||||
|
||||
GD.Print($"共有 {allItems.Count} 个物品:");
|
||||
foreach (var item in allItems)
|
||||
{
|
||||
GD.Print($"- {item.Name} ({item.Id})");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 资源预加载
|
||||
|
||||
```csharp
|
||||
public partial class GameInitializer : Node
|
||||
{
|
||||
public override async void _Ready()
|
||||
{
|
||||
await PreloadAllResources();
|
||||
GD.Print("所有资源预加载完成");
|
||||
}
|
||||
|
||||
private async Task PreloadAllResources()
|
||||
{
|
||||
// 预加载物品资源
|
||||
var itemRepo = new ItemRepository();
|
||||
this.RegisterUtility<IResourceRepository<string, ItemData>>(itemRepo);
|
||||
|
||||
// 预加载技能资源
|
||||
var skillRepo = new SkillRepository();
|
||||
this.RegisterUtility<IResourceRepository<string, SkillData>>(skillRepo);
|
||||
|
||||
// 预加载敌人资源
|
||||
var enemyRepo = new EnemyRepository();
|
||||
this.RegisterUtility<IResourceRepository<string, EnemyData>>(enemyRepo);
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 资源缓存管理
|
||||
|
||||
```csharp
|
||||
public class CachedResourceRepository<TKey, TResource>
|
||||
where TResource : Resource, IHasKey<TKey>
|
||||
where TKey : notnull
|
||||
{
|
||||
private readonly GodotResourceRepository<TKey, TResource> _repository;
|
||||
private readonly Dictionary<TKey, TResource> _cache = new();
|
||||
|
||||
public CachedResourceRepository(params string[] paths)
|
||||
{
|
||||
_repository = new GodotResourceRepository<TKey, TResource>();
|
||||
_repository.LoadFromPath(paths);
|
||||
}
|
||||
|
||||
public TResource Get(TKey key)
|
||||
{
|
||||
// 先从缓存获取
|
||||
if (_cache.TryGetValue(key, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 从仓储获取并缓存
|
||||
var resource = _repository.Get(key);
|
||||
_cache[key] = resource;
|
||||
return resource;
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
{
|
||||
_cache.Clear();
|
||||
GD.Print("资源缓存已清空");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 资源版本管理
|
||||
|
||||
```csharp
|
||||
[GlobalClass]
|
||||
public partial class VersionedItemData : Resource, IHasKey<string>
|
||||
{
|
||||
[Export]
|
||||
public string Id { get; set; }
|
||||
|
||||
[Export]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Export]
|
||||
public int Version { get; set; } = 1;
|
||||
|
||||
public string Key => Id;
|
||||
}
|
||||
|
||||
public class VersionedItemRepository : GodotResourceRepository<string, VersionedItemData>
|
||||
{
|
||||
public VersionedItemRepository()
|
||||
{
|
||||
LoadFromPath("res://data/items");
|
||||
ValidateVersions();
|
||||
}
|
||||
|
||||
private void ValidateVersions()
|
||||
{
|
||||
var allItems = GetAll();
|
||||
foreach (var item in allItems)
|
||||
{
|
||||
if (item.Version < 2)
|
||||
{
|
||||
GD.PrintErr($"物品 {item.Id} 版本过旧: v{item.Version}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 多类型资源管理
|
||||
|
||||
```csharp
|
||||
// 武器资源
|
||||
[GlobalClass]
|
||||
public partial class WeaponData : Resource, IHasKey<string>
|
||||
{
|
||||
[Export] public string Id { get; set; }
|
||||
[Export] public string Name { get; set; }
|
||||
[Export] public int Damage { get; set; }
|
||||
public string Key => Id;
|
||||
}
|
||||
|
||||
// 护甲资源
|
||||
[GlobalClass]
|
||||
public partial class ArmorData : Resource, IHasKey<string>
|
||||
{
|
||||
[Export] public string Id { get; set; }
|
||||
[Export] public string Name { get; set; }
|
||||
[Export] public int Defense { get; set; }
|
||||
public string Key => Id;
|
||||
}
|
||||
|
||||
// 统一管理
|
||||
public class EquipmentManager
|
||||
{
|
||||
private readonly IResourceRepository<string, WeaponData> _weaponRepo;
|
||||
private readonly IResourceRepository<string, ArmorData> _armorRepo;
|
||||
|
||||
public EquipmentManager(
|
||||
IResourceRepository<string, WeaponData> weaponRepo,
|
||||
IResourceRepository<string, ArmorData> armorRepo)
|
||||
{
|
||||
_weaponRepo = weaponRepo;
|
||||
_armorRepo = armorRepo;
|
||||
}
|
||||
|
||||
public void ShowAllEquipment()
|
||||
{
|
||||
GD.Print("=== 武器 ===");
|
||||
foreach (var weapon in _weaponRepo.GetAll())
|
||||
{
|
||||
GD.Print($"{weapon.Name}: 伤害 {weapon.Damage}");
|
||||
}
|
||||
|
||||
GD.Print("=== 护甲 ===");
|
||||
foreach (var armor in _armorRepo.GetAll())
|
||||
{
|
||||
GD.Print($"{armor.Name}: 防御 {armor.Defense}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 资源热重载
|
||||
|
||||
```csharp
|
||||
public partial class HotReloadManager : Node
|
||||
{
|
||||
private IResourceRepository<string, ItemData> _itemRepo;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_itemRepo = this.GetUtility<IResourceRepository<string, ItemData>>();
|
||||
}
|
||||
|
||||
public void ReloadResources()
|
||||
{
|
||||
// 清空现有资源
|
||||
_itemRepo.Clear();
|
||||
|
||||
// 重新加载
|
||||
var repo = _itemRepo as GodotResourceRepository<string, ItemData>;
|
||||
repo?.LoadFromPath("res://data/items");
|
||||
|
||||
GD.Print("资源已重新加载");
|
||||
}
|
||||
|
||||
public override void _Input(InputEvent @event)
|
||||
{
|
||||
// 按 F5 热重载
|
||||
if (@event is InputEventKey keyEvent &&
|
||||
keyEvent.Pressed &&
|
||||
keyEvent.Keycode == Key.F5)
|
||||
{
|
||||
ReloadResources();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **资源实现 IHasKey 接口**:确保资源可以被仓储管理
|
||||
```csharp
|
||||
✓ public partial class ItemData : Resource, IHasKey<string> { }
|
||||
✗ public partial class ItemData : Resource { } // 无法使用仓储
|
||||
```
|
||||
|
||||
2. **使用有意义的键类型**:根据业务需求选择合适的键类型
|
||||
```csharp
|
||||
✓ IResourceRepository<string, ItemData> // 字符串 ID
|
||||
✓ IResourceRepository<int, LevelData> // 整数关卡号
|
||||
✓ IResourceRepository<Guid, SaveData> // GUID 唯一标识
|
||||
```
|
||||
|
||||
3. **在架构初始化时加载资源**:避免运行时加载卡顿
|
||||
```csharp
|
||||
protected override void InstallModules()
|
||||
{
|
||||
var itemRepo = new ItemRepository();
|
||||
RegisterUtility<IResourceRepository<string, ItemData>>(itemRepo);
|
||||
}
|
||||
```
|
||||
|
||||
4. **使用递归加载组织资源**:保持目录结构清晰
|
||||
```csharp
|
||||
// 推荐的目录结构
|
||||
res://data/
|
||||
├── items/
|
||||
│ ├── weapons/
|
||||
│ ├── armors/
|
||||
│ └── consumables/
|
||||
└── enemies/
|
||||
├── bosses/
|
||||
└── minions/
|
||||
```
|
||||
|
||||
5. **处理资源不存在的情况**:使用 TryGet 避免异常
|
||||
```csharp
|
||||
✓ if (_itemRepo.TryGet(itemId, out var item)) { }
|
||||
✗ var item = _itemRepo.Get(itemId); // 可能抛出异常
|
||||
```
|
||||
|
||||
6. **合理使用资源缓存**:平衡内存和性能
|
||||
```csharp
|
||||
// 频繁访问的资源可以缓存
|
||||
private ItemData _cachedPlayerWeapon;
|
||||
|
||||
public ItemData GetPlayerWeapon()
|
||||
{
|
||||
return _cachedPlayerWeapon ??= _itemRepo.Get("player_weapon");
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:如何让资源支持仓储管理?
|
||||
|
||||
**解答**:
|
||||
资源类必须实现 `IHasKey<TKey>` 接口:
|
||||
|
||||
```csharp
|
||||
[GlobalClass]
|
||||
public partial class MyResource : Resource, IHasKey<string>
|
||||
{
|
||||
[Export]
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Key => Id;
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:资源文件必须是什么格式?
|
||||
|
||||
**解答**:
|
||||
资源仓储支持 Godot 的 `.tres` 和 `.res` 文件格式:
|
||||
|
||||
- `.tres`:文本格式,可读性好,适合版本控制
|
||||
- `.res`:二进制格式,加载更快,适合发布版本
|
||||
|
||||
### 问题:如何组织资源目录结构?
|
||||
|
||||
**解答**:
|
||||
推荐按类型和功能组织:
|
||||
|
||||
```
|
||||
res://data/
|
||||
├── items/ # 物品资源
|
||||
│ ├── weapons/
|
||||
│ ├── armors/
|
||||
│ └── consumables/
|
||||
├── enemies/ # 敌人资源
|
||||
├── skills/ # 技能资源
|
||||
└── levels/ # 关卡资源
|
||||
```
|
||||
|
||||
### 问题:资源加载会阻塞主线程吗?
|
||||
|
||||
**解答**:
|
||||
`LoadFromPath` 是同步操作,建议在游戏初始化时加载:
|
||||
|
||||
```csharp
|
||||
public override void _Ready()
|
||||
{
|
||||
// 在游戏启动时加载
|
||||
var itemRepo = new ItemRepository();
|
||||
RegisterUtility(itemRepo);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何处理重复的资源键?
|
||||
|
||||
**解答**:
|
||||
仓储会记录警告但不会抛出异常:
|
||||
|
||||
```csharp
|
||||
// 日志会显示: "Duplicate key detected: item_001"
|
||||
// 后加载的资源会被忽略
|
||||
```
|
||||
|
||||
### 问题:可以动态添加和移除资源吗?
|
||||
|
||||
**解答**:
|
||||
可以,使用 `Add` 和 `Remove` 方法:
|
||||
|
||||
```csharp
|
||||
// 添加资源
|
||||
_itemRepo.Add("new_item", newItemData);
|
||||
|
||||
// 移除资源
|
||||
_itemRepo.Remove("old_item");
|
||||
|
||||
// 清空所有资源
|
||||
_itemRepo.Clear();
|
||||
```
|
||||
|
||||
### 问题:如何实现资源的延迟加载?
|
||||
|
||||
**解答**:
|
||||
可以创建包装类实现延迟加载:
|
||||
|
||||
```csharp
|
||||
public class LazyResourceRepository<TKey, TResource>
|
||||
where TResource : Resource, IHasKey<TKey>
|
||||
where TKey : notnull
|
||||
{
|
||||
private GodotResourceRepository<TKey, TResource> _repository;
|
||||
private readonly string[] _paths;
|
||||
private bool _loaded;
|
||||
|
||||
public LazyResourceRepository(params string[] paths)
|
||||
{
|
||||
_paths = paths;
|
||||
}
|
||||
|
||||
private void EnsureLoaded()
|
||||
{
|
||||
if (_loaded) return;
|
||||
|
||||
_repository = new GodotResourceRepository<TKey, TResource>();
|
||||
_repository.LoadFromPath(_paths);
|
||||
_loaded = true;
|
||||
}
|
||||
|
||||
public TResource Get(TKey key)
|
||||
{
|
||||
EnsureLoaded();
|
||||
return _repository.Get(key);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [数据与存档系统](/zh-CN/game/data) - 数据持久化
|
||||
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
|
||||
- [Godot 场景系统](/zh-CN/godot/scene) - 场景资源管理
|
||||
- [资源管理系统](/zh-CN/core/resource) - 核心资源管理
|
||||
583
docs/zh-CN/godot/scene.md
Normal file
583
docs/zh-CN/godot/scene.md
Normal file
@ -0,0 +1,583 @@
|
||||
---
|
||||
title: Godot 场景系统
|
||||
description: Godot 场景系统提供了 GFramework 场景管理与 Godot 场景树的完整集成。
|
||||
---
|
||||
|
||||
# Godot 场景系统
|
||||
|
||||
## 概述
|
||||
|
||||
Godot 场景系统是 GFramework.Godot 中连接框架场景管理与 Godot 场景树的核心组件。它提供了场景行为封装、场景工厂、场景注册表等功能,让你可以在
|
||||
Godot 项目中使用 GFramework 的场景管理系统。
|
||||
|
||||
通过 Godot 场景系统,你可以使用 GFramework 的场景路由、生命周期管理等功能,同时保持与 Godot 场景系统的完美兼容。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- 场景行为封装(SceneBehavior)
|
||||
- 场景工厂和注册表
|
||||
- 与 Godot PackedScene 集成
|
||||
- 多种场景行为类型(Node2D、Node3D、Control)
|
||||
- 场景生命周期管理
|
||||
- 场景根节点管理
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 场景行为
|
||||
|
||||
`SceneBehaviorBase<T>` 封装了 Godot 节点的场景行为:
|
||||
|
||||
```csharp
|
||||
public abstract class SceneBehaviorBase<T> : ISceneBehavior
|
||||
where T : Node
|
||||
{
|
||||
protected readonly T Owner;
|
||||
public string Key { get; }
|
||||
public IScene Scene { get; }
|
||||
}
|
||||
```
|
||||
|
||||
### 场景工厂
|
||||
|
||||
`GodotSceneFactory` 负责创建场景实例:
|
||||
|
||||
```csharp
|
||||
public class GodotSceneFactory : ISceneFactory
|
||||
{
|
||||
public ISceneBehavior Create(string sceneKey);
|
||||
}
|
||||
```
|
||||
|
||||
### 场景注册表
|
||||
|
||||
`IGodotSceneRegistry` 管理场景资源:
|
||||
|
||||
```csharp
|
||||
public interface IGodotSceneRegistry
|
||||
{
|
||||
void Register(string key, PackedScene scene);
|
||||
PackedScene Get(string key);
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 创建场景脚本
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Game.Abstractions.scene;
|
||||
|
||||
public partial class MainMenuScene : Control, IScene
|
||||
{
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
{
|
||||
GD.Print("加载主菜单资源");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnEnterAsync()
|
||||
{
|
||||
GD.Print("进入主菜单");
|
||||
Show();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnPauseAsync()
|
||||
{
|
||||
GD.Print("暂停主菜单");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnResumeAsync()
|
||||
{
|
||||
GD.Print("恢复主菜单");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnExitAsync()
|
||||
{
|
||||
GD.Print("退出主菜单");
|
||||
Hide();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnUnloadAsync()
|
||||
{
|
||||
GD.Print("卸载主菜单资源");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册场景
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.scene;
|
||||
using Godot;
|
||||
|
||||
public class GameSceneRegistry : GodotSceneRegistry
|
||||
{
|
||||
publieneRegistry()
|
||||
{
|
||||
// 注册场景资源
|
||||
Register("MainMenu", GD.Load<PackedScene>("res://scenes/MainMenu.tscn"));
|
||||
Register("Gameplay", GD.Load<PackedScene>("res://scenes/Gameplay.tscn"));
|
||||
Register("Pause", GD.Load<PackedScene>("res://scenes/Pause.tscn"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 设置场景系统
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.architecture;
|
||||
using GFramework.Godot.scene;
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 注册场景注册表
|
||||
var sceneRegistry = new GameSceneRegistry();
|
||||
RegisterUtility<IGodotSceneRegistry>(sceneRegistry);
|
||||
|
||||
// 注册场景工厂
|
||||
var sceneFactory = new GodotSceneFactory();
|
||||
RegisterUtility<ISceneFactory>(sceneFactory);
|
||||
|
||||
// 注册场景路由
|
||||
var sceneRouter = new GodotSceneRouter();
|
||||
RegisterSystem<ISceneRouter>(sceneRouter);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用场景路由
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class GameController : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
// 切换到主菜单
|
||||
SwitchToMainMenu();
|
||||
}
|
||||
|
||||
private async void SwitchToMainMenu()
|
||||
{
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
await sceneRouter.ReplaceAsync("MainMenu");
|
||||
}
|
||||
|
||||
private async void StartGame()
|
||||
{
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
await sceneRouter.ReplaceAsync("Gameplay");
|
||||
}
|
||||
|
||||
private async void ShowPause()
|
||||
{
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
await sceneRouter.PushAsync("Pause");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 使用场景行为提供者
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Game.Abstractions.scene;
|
||||
using GFramework.Godot.scene;
|
||||
|
||||
public partial class GameplayScene : Node2D, ISceneBehaviorProvider
|
||||
{
|
||||
private GameplaySceneBehavior _behavior;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_behavior = new GameplaySceneBehavior(this, "Gameplay");
|
||||
}
|
||||
|
||||
public ISceneBehavior GetScene()
|
||||
{
|
||||
return _behavior;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义场景行为
|
||||
public class GameplaySceneBehavior : Node2DSceneBehavior
|
||||
{
|
||||
public GameplaySceneBehavior(Node2D owner, string key) : base(owner, key)
|
||||
{
|
||||
}
|
||||
|
||||
protected override async ValueTask OnLoadInternalAsync(ISceneEnterParam? param)
|
||||
{
|
||||
GD.Print("加载游戏场景");
|
||||
// 加载游戏资源
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override async ValueTask OnEnterInternalAsync()
|
||||
{
|
||||
GD.Print("进入游戏场景");
|
||||
Owner.Show();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 不同类型的场景行为
|
||||
|
||||
```csharp
|
||||
// Node2D 场景
|
||||
public class Node2DSceneBehavior : SceneBehaviorBase<Node2D>
|
||||
{
|
||||
public Node2DSceneBehavior(Node2D owner, string key) : base(owner, key)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// Node3D 场景
|
||||
public class Node3DSceneBehavior : SceneBehaviorBase<Node3D>
|
||||
{
|
||||
public Node3DSceneBehavior(Node3D owner, string key) : base(owner, key)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// Control 场景(UI)
|
||||
public class ControlSceneBehavior : SceneBehaviorBase<Control>
|
||||
{
|
||||
public ControlSceneBehavior(Control owner, string key) : base(owner, key)
|
||||
{
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景根节点管理
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.scene;
|
||||
|
||||
public partial class SceneRoot : Node, ISceneRoot
|
||||
{
|
||||
private Node _currentSceneNode;
|
||||
|
||||
public void AttachScene(Node sceneNode)
|
||||
{
|
||||
// 移除旧场景
|
||||
if (_currentSceneNode != null)
|
||||
{
|
||||
RemoveChild(_currentSceneNode);
|
||||
_currentSceneNode.QueueFree();
|
||||
}
|
||||
|
||||
// 添加新场景
|
||||
_currentSceneNode = sceneNode;
|
||||
AddChild(_currentSceneNode);
|
||||
}
|
||||
|
||||
public void DetachScene(Node sceneNode)
|
||||
{
|
||||
if (_currentSceneNode == sceneNode)
|
||||
{
|
||||
RemoveChild(_currentSceneNode);
|
||||
_currentSceneNode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景参数传递
|
||||
|
||||
```csharp
|
||||
// 定义场景参数
|
||||
public class GameplayEnterParam : ISceneEnterParam
|
||||
{
|
||||
public int Level { get; set; }
|
||||
public string Difficulty { get; set; }
|
||||
}
|
||||
|
||||
// 在场景中接收参数
|
||||
public partial class GameplayScene : Node2D, IScene
|
||||
{
|
||||
private int _level;
|
||||
private string _difficulty;
|
||||
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
{
|
||||
if (param is GameplayEnterParam gameplayParam)
|
||||
{
|
||||
_level = gameplayParam.Level;
|
||||
_difficulty = gameplayParam.Difficulty;
|
||||
GD.Print($"加载关卡 {_level},难度: {_difficulty}");
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ... 其他生命周期方法
|
||||
}
|
||||
|
||||
// 切换场景时传递参数
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam
|
||||
{
|
||||
Level = 1,
|
||||
Difficulty = "Normal"
|
||||
});
|
||||
```
|
||||
|
||||
### 场景预加载
|
||||
|
||||
```csharp
|
||||
public partial class LoadingScene : Control
|
||||
{
|
||||
public override async void _Ready()
|
||||
{
|
||||
// 预加载下一个场景
|
||||
await PreloadNextScene();
|
||||
|
||||
// 切换到预加载的场景
|
||||
var sceneRouter = this.GetSystem<ISceneRouter>();
|
||||
await sceneRouter.ReplaceAsync("Gameplay");
|
||||
}
|
||||
|
||||
private async Task PreloadNextScene()
|
||||
{
|
||||
var sceneFactory = this.GetUtility<ISceneFactory>();
|
||||
var sceneBehavior = sceneFactory.Create("Gameplay");
|
||||
|
||||
// 预加载场景资源
|
||||
await sceneBehavior.LoadAsync(null);
|
||||
|
||||
GD.Print("场景预加载完成");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景转换动画
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Game.Abstractions.scene;
|
||||
|
||||
public class FadeTransitionHandler : ISceneTransitionHandler
|
||||
{
|
||||
private ColorRect _fadeRect;
|
||||
|
||||
public FadeTransitionHandler(ColorRect fadeRect)
|
||||
{
|
||||
_fadeRect = fadeRect;
|
||||
}
|
||||
|
||||
public async ValueTask OnBeforeExitAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
// 淡出动画
|
||||
var tween = _fadeRect.CreateTween();
|
||||
tween.TweenProperty(_fadeRect, "modulate:a", 1.0f, 0.3f);
|
||||
await tween.ToSignal(tween, Tween.SignalName.Finished);
|
||||
}
|
||||
|
||||
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
// 淡入动画
|
||||
var tween = _fadeRect.CreateTween();
|
||||
tween.TweenProperty(_fadeRect, "modulate:a", 0.0f, 0.3f);
|
||||
await tween.ToSignal(tween, Tween.SignalName.Finished);
|
||||
}
|
||||
|
||||
// ... 其他方法
|
||||
}
|
||||
```
|
||||
|
||||
### 场景间通信
|
||||
|
||||
```csharp
|
||||
// 通过事件通信
|
||||
public partial class GameplayScene : Node2D, IScene
|
||||
{
|
||||
public async ValueTask OnEnterAsync()
|
||||
{
|
||||
// 发送场景进入事件
|
||||
this.SendEvent(new GameplaySceneEnteredEvent());
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
// 在其他地方监听
|
||||
public partial class HUD : Control
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
this.RegisterEvent<GameplaySceneEnteredEvent>(OnGameplayEntered);
|
||||
}
|
||||
|
||||
private void OnGameplayEntered(GameplaySceneEnteredEvent evt)
|
||||
{
|
||||
GD.Print("游戏场景已进入,显示 HUD");
|
||||
Show();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **场景脚本实现 IScene 接口**:获得完整的生命周期管理
|
||||
```csharp
|
||||
✓ public partial class MyScene : Node2D, IScene { }
|
||||
✗ public partial class MyScene : Node2D { } // 无生命周期管理
|
||||
```
|
||||
|
||||
2. **使用场景注册表管理场景资源**:集中管理所有场景
|
||||
```csharp
|
||||
public class GameSceneRegistry : GodotSceneRegistry
|
||||
{
|
||||
public GameSceneRegistry()
|
||||
{
|
||||
Register("MainMenu", GD.Load<PackedScene>("res://scenes/MainMenu.tscn"));
|
||||
Register("Gameplay", GD.Load<PackedScene>("res://scenes/Gameplay.tscn"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **在 OnLoadAsync 中加载资源**:避免场景切换卡顿
|
||||
```csharp
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param)
|
||||
{
|
||||
// 异步加载资源
|
||||
await LoadTexturesAsync();
|
||||
await LoadAudioAsync();
|
||||
}
|
||||
```
|
||||
|
||||
4. **使用场景根节点管理场景树**:保持场景树结构清晰
|
||||
```csharp
|
||||
// 创建场景根节点
|
||||
var sceneRoot = new Node { Name = "SceneRoot" };
|
||||
AddChild(sceneRoot);
|
||||
|
||||
// 绑定到场景路由
|
||||
sceneRouter.BindRoot(sceneRoot);
|
||||
```
|
||||
|
||||
5. **正确清理场景资源**:在 OnUnloadAsync 中释放资源
|
||||
```csharp
|
||||
public async ValueTask OnUnloadAsync()
|
||||
{
|
||||
// 释放资源
|
||||
_texture?.Dispose();
|
||||
_audioStream?.Dispose();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
```
|
||||
|
||||
6. **使用场景参数传递数据**:避免使用全局变量
|
||||
```csharp
|
||||
✓ await sceneRouter.ReplaceAsync("Gameplay", new GameplayEnterParam { Level = 1 });
|
||||
✗ GlobalData.CurrentLevel = 1; // 避免全局状态
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:如何在 Godot 场景中使用 GFramework?
|
||||
|
||||
**解答**:
|
||||
场景脚本实现 `IScene` 接口:
|
||||
|
||||
```csharp
|
||||
public partial class MyScene : Node2D, IScene
|
||||
{
|
||||
public async ValueTask OnLoadAsync(ISceneEnterParam? param) { }
|
||||
public async ValueTask OnEnterAsync() { }
|
||||
// ... 实现其他方法
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:场景切换时节点如何管理?
|
||||
|
||||
**解答**:
|
||||
使用场景根节点管理:
|
||||
|
||||
```csharp
|
||||
// 场景路由会自动管理节点的添加和移除
|
||||
await sceneRouter.ReplaceAsync("NewScene");
|
||||
// 旧场景节点会被移除,新场景节点会被添加
|
||||
```
|
||||
|
||||
### 问题:如何实现场景预加载?
|
||||
|
||||
**解答**:
|
||||
使用场景工厂提前创建场景:
|
||||
|
||||
```csharp
|
||||
var sceneFactory = this.GetUtility<ISceneFactory>();
|
||||
var sceneBehavior = sceneFactory.Create("NextScene");
|
||||
await sceneBehavior.LoadAsync(null);
|
||||
```
|
||||
|
||||
### 问题:场景生命周期方法的调用顺序是什么?
|
||||
|
||||
**解答**:
|
||||
|
||||
- 进入场景:`OnLoadAsync` -> `OnEnterAsync` -> `OnShow`
|
||||
- 暂停场景:`OnPause` -> `OnHide`
|
||||
- 恢复场景:`OnShow` -> `OnResume`
|
||||
- 退出场景:`OnHide` -> `OnExitAsync` -> `OnUnloadAsync`
|
||||
|
||||
### 问题:如何在场景中访问架构组件?
|
||||
|
||||
**解答**:
|
||||
使用扩展方法:
|
||||
|
||||
```csharp
|
||||
public partial class MyScene : Node2D, IScene
|
||||
{
|
||||
public async ValueTask OnEnterAsync()
|
||||
{
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
var gameSystem = this.GetSystem<GameSystem>();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:场景切换时如何显示加载界面?
|
||||
|
||||
**解答**:
|
||||
使用场景转换处理器:
|
||||
|
||||
```csharp
|
||||
public class LoadingScreenHandler : ISceneTransitionHandler
|
||||
{
|
||||
public async ValueTask OnBeforeLoadAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
// 显示加载界面
|
||||
ShowLoadingScreen();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask OnAfterEnterAsync(SceneTransitionEvent @event)
|
||||
{
|
||||
// 隐藏加载界面
|
||||
HideLoadingScreen();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [场景系统](/zh-CN/game/scene) - 核心场景管理
|
||||
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
|
||||
- [Godot UI 系统](/zh-CN/godot/ui) - Godot UI 集成
|
||||
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
|
||||
643
docs/zh-CN/godot/ui.md
Normal file
643
docs/zh-CN/godot/ui.md
Normal file
@ -0,0 +1,643 @@
|
||||
---
|
||||
title: Godot UI 系统
|
||||
description: Godot UI 系统提供了 GFramework UI 管理与 Godot Control 节点的完整集成。
|
||||
---
|
||||
|
||||
# Godot UI 系统
|
||||
|
||||
## 概述
|
||||
|
||||
Godot UI 系统是 GFramework.Godot 中连接框架 UI 管理与 Godot Control 节点的核心组件。它提供了 UI 页面行为封装、UI 工厂、UI
|
||||
注册表等功能,支持多层级 UI 显示,让你可以在 Godot 项目中使用 GFramework 的 UI 管理系统。
|
||||
|
||||
通过 Godot UI 系统,你可以使用 GFramework 的 UI 路由、生命周期管理、多层级显示等功能,同时保持与 Godot UI 系统的完美兼容。
|
||||
|
||||
**主要特性**:
|
||||
|
||||
- UI 页面行为封装
|
||||
- UI 工厂和注册表
|
||||
- 与 Godot PackedScene 集成
|
||||
- 多层级 UI 支持(Page、Overlay、Modal、Toast、Topmost)
|
||||
- UI 生命周期管理
|
||||
- UI 根节点管理
|
||||
|
||||
## 核心概念
|
||||
|
||||
### UI 页面行为
|
||||
|
||||
`CanvasItemUiPageBehaviorBase<T>` 封装了 Godot Control 节点的 UI 行为:
|
||||
|
||||
```csharp
|
||||
public abstract class CanvasItemUiPageBehaviorBase<T> : IUiPageBehavior
|
||||
where T : CanvasItem
|
||||
{
|
||||
protected readonly T Owner;
|
||||
public string Key { get; }
|
||||
public UiLayer Layer { get; }
|
||||
public bool IsReentrant { get; }
|
||||
}
|
||||
```
|
||||
|
||||
### UI 工厂
|
||||
|
||||
`GodotUiFactory` 负责创建 UI 实例:
|
||||
|
||||
```csharp
|
||||
public class GodotUiFactory : IUiFactory
|
||||
{
|
||||
public IUiPageBehavior Create(string uiKey);
|
||||
}
|
||||
```
|
||||
|
||||
### UI 层级行为
|
||||
|
||||
不同层级的 UI 有不同的行为类:
|
||||
|
||||
```csharp
|
||||
// Page 层(栈管理)
|
||||
public class PageLayerUiPageBehavior : CanvasItemUiPageBehaviorBase<Control>
|
||||
{
|
||||
public override UiLayer Layer => UiLayer.Page;
|
||||
public override bool IsReentrant => false;
|
||||
}
|
||||
|
||||
// Modal 层(模态对话框)
|
||||
public class ModalLayerUiPageBehavior : CanvasItemUiPageBehaviorBase<Control>
|
||||
{
|
||||
public override UiLayer Layer => UiLayer.Modal;
|
||||
public override bool IsReentrant => true;
|
||||
}
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 创建 UI 脚本
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Game.Abstractions.ui;
|
||||
|
||||
public partial class MainMenuPage : Control, IUiPage
|
||||
{
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
GD.Print("进入主菜单");
|
||||
Show();
|
||||
}
|
||||
|
||||
public void OnExit()
|
||||
{
|
||||
GD.Print("退出主菜单");
|
||||
Hide();
|
||||
}
|
||||
|
||||
public void OnPause()
|
||||
{
|
||||
GD.Print("暂停主菜单");
|
||||
}
|
||||
|
||||
public void OnResume()
|
||||
{
|
||||
GD.Print("恢复主菜单");
|
||||
}
|
||||
|
||||
public void OnShow()
|
||||
{
|
||||
Show();
|
||||
}
|
||||
|
||||
public void OnHide()
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实现 UI 页面行为提供者
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Game.Abstractions.ui;
|
||||
using GFramework.Godot.ui;
|
||||
|
||||
public partial class MainMenuPage : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
private PageLayerUiPageBehavior _behavior;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_behavior = new PageLayerUiPageBehavior(this, "MainMenu");
|
||||
}
|
||||
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return _behavior;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注册 UI
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.ui;
|
||||
using Godot;
|
||||
|
||||
public class GameUiRegistry : GodotUiRegistry
|
||||
{
|
||||
public GameUiRegistry()
|
||||
{
|
||||
// 注册 UI 资源
|
||||
Register("MainMenu", GD.Load<PackedScene>("res://ui/MainMenu.tscn"));
|
||||
Register("Settings", GD.Load<PackedScene>("res://ui/Settings.tscn"));
|
||||
Register("ConfirmDialog", GD.Load<PackedScene>("res://ui/ConfirmDialog.tscn"));
|
||||
Register("Toast", GD.Load<PackedScene>("res://ui/Toast.tscn"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 设置 UI 系统
|
||||
|
||||
```csharp
|
||||
using GFramework.Godot.architecture;
|
||||
using GFramework.Godot.ui;
|
||||
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
protected override void InstallModules()
|
||||
{
|
||||
// 注册 UI 注册表
|
||||
var uiRegistry = new GameUiRegistry();
|
||||
RegisterUtility<IGodotUiRegistry>(uiRegistry);
|
||||
|
||||
// 注册 UI 工厂
|
||||
var uiFactory = new GodotUiFactory();
|
||||
RegisterUtility<IUiFactory>(uiFactory);
|
||||
|
||||
// 注册 UI 路由
|
||||
var uiRouter = new GodotUiRouter();
|
||||
RegisterSystem<IUiRouter>(uiRouter);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 UI 路由
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.extensions;
|
||||
|
||||
public partial class GameController : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
ShowMainMenu();
|
||||
}
|
||||
|
||||
private async void ShowMainMenu()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
await uiRouter.PushAsync("MainMenu");
|
||||
}
|
||||
|
||||
private async void ShowSettings()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
await uiRouter.PushAsync("Settings");
|
||||
}
|
||||
|
||||
private void ShowDialog()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
uiRouter.Show("ConfirmDialog", UiLayer.Modal);
|
||||
}
|
||||
|
||||
private void ShowToast(string message)
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
uiRouter.Show("Toast", UiLayer.Toast, new ToastParam { Message = message });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 不同层级的 UI 行为
|
||||
|
||||
```csharp
|
||||
// Page 层 UI(栈管理,不可重入)
|
||||
public partial class MainMenuPage : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return new PageLayerUiPageBehavior(this, "MainMenu");
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay 层 UI(浮层,可重入)
|
||||
public partial class InfoPanel : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return new OverlayLayerUiPageBehavior(this, "InfoPanel");
|
||||
}
|
||||
}
|
||||
|
||||
// Modal 层 UI(模态对话框,可重入)
|
||||
public partial class ConfirmDialog : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return new ModalLayerUiPageBehavior(this, "ConfirmDialog");
|
||||
}
|
||||
}
|
||||
|
||||
// Toast 层 UI(提示,可重入)
|
||||
public partial class ToastMessage : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return new ToastLayerUiPageBehavior(this, "Toast");
|
||||
}
|
||||
}
|
||||
|
||||
// Topmost 层 UI(顶层,不可重入)
|
||||
public partial class LoadingScreen : Control, IUiPageBehaviorProvider
|
||||
{
|
||||
public IUiPageBehavior GetPage()
|
||||
{
|
||||
return new TopmostLayerUiPageBehavior(this, "Loading");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UI 参数传递
|
||||
|
||||
```csharp
|
||||
// 定义 UI 参数
|
||||
public class ConfirmDialogParam : IUiPageEnterParam
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Message { get; set; }
|
||||
public Action OnConfirm { get; set; }
|
||||
public Action OnCancel { get; set; }
|
||||
}
|
||||
|
||||
// 在 UI 中接收参数
|
||||
public partial class ConfirmDialog : Control, IUiPage
|
||||
{
|
||||
private Label _titleLabel;
|
||||
private Label _messageLabel;
|
||||
private Action _onConfirm;
|
||||
private Action _onCancel;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_titleLabel = GetNode<Label>("Title");
|
||||
_messageLabel = GetNode<Label>("Message");
|
||||
|
||||
GetNode<Button>("ConfirmButton").Pressed += OnConfirmPressed;
|
||||
GetNode<Button>("CancelButton").Pressed += OnCancelPressed;
|
||||
}
|
||||
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
if (param is ConfirmDialogParam dialogParam)
|
||||
{
|
||||
_titleLabel.Text = dialogParam.Title;
|
||||
_messageLabel.Text = dialogParam.Message;
|
||||
_onConfirm = dialogParam.OnConfirm;
|
||||
_onCancel = dialogParam.OnCancel;
|
||||
}
|
||||
|
||||
Show();
|
||||
}
|
||||
|
||||
private void OnConfirmPressed()
|
||||
{
|
||||
_onConfirm?.Invoke();
|
||||
CloseDialog();
|
||||
}
|
||||
|
||||
private void OnCancelPressed()
|
||||
{
|
||||
_onCancel?.Invoke();
|
||||
CloseDialog();
|
||||
}
|
||||
|
||||
private void CloseDialog()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
if (Handle.HasValue)
|
||||
{
|
||||
uiRouter.Hide(Handle.Value, UiLayer.Modal, destroy: true);
|
||||
}
|
||||
}
|
||||
|
||||
// ... 其他生命周期方法
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
uiRouter.Show("ConfirmDialog", UiLayer.Modal, new ConfirmDialogParam
|
||||
{
|
||||
Title = "确认",
|
||||
Message = "确定要退出吗?",
|
||||
OnConfirm = () => GD.Print("确认"),
|
||||
OnCancel = () => GD.Print("取消")
|
||||
});
|
||||
```
|
||||
|
||||
### UI 根节点管理
|
||||
|
||||
```csharp
|
||||
using Godot;
|
||||
using GFramework.Godot.ui;
|
||||
|
||||
public partial class UiRoot : CanvasLayer, IUiRoot
|
||||
{
|
||||
private Control _pageLayer;
|
||||
private Control _overlayLayer;
|
||||
private Control _modalLayer;
|
||||
private Control _toastLayer;
|
||||
private Control _topmostLayer;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 创建各层级容器
|
||||
_pageLayer = new Control { Name = "PageLayer" };
|
||||
_overlayLayer = new Control { Name = "OverlayLayer" };
|
||||
_modalLayer = new Control { Name = "ModalLayer" };
|
||||
_toastLayer = new Control { Name = "ToastLayer" };
|
||||
_topmostLayer = new Control { Name = "TopmostLayer" };
|
||||
|
||||
AddChild(_pageLayer);
|
||||
AddChild(_overlayLayer);
|
||||
AddChild(_modalLayer);
|
||||
AddChild(_toastLayer);
|
||||
AddChild(_topmostLayer);
|
||||
}
|
||||
|
||||
public void AttachPage(Control page, UiLayer layer)
|
||||
{
|
||||
var container = GetLayerContainer(layer);
|
||||
container.AddChild(page);
|
||||
}
|
||||
|
||||
public void DetachPage(Control page, UiLayer layer)
|
||||
{
|
||||
var container = GetLayerContainer(layer);
|
||||
container.RemoveChild(page);
|
||||
}
|
||||
|
||||
private Control GetLayerContainer(UiLayer layer)
|
||||
{
|
||||
return layer switch
|
||||
{
|
||||
UiLayer.Page => _pageLayer,
|
||||
UiLayer.Overlay => _overlayLayer,
|
||||
UiLayer.Modal => _modalLayer,
|
||||
UiLayer.Toast => _toastLayer,
|
||||
UiLayer.Topmost => _topmostLayer,
|
||||
_ => _pageLayer
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UI 动画和过渡
|
||||
|
||||
```csharp
|
||||
public partial class AnimatedPage : Control, IUiPage
|
||||
{
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
// 淡入动画
|
||||
Modulate = new Color(1, 1, 1, 0);
|
||||
Show();
|
||||
|
||||
var tween = CreateTween();
|
||||
tween.TweenProperty(this, "modulate:a", 1.0f, 0.3f);
|
||||
}
|
||||
|
||||
public void OnExit()
|
||||
{
|
||||
// 淡出动画
|
||||
var tween = CreateTween();
|
||||
tween.TweenProperty(this, "modulate:a", 0.0f, 0.3f);
|
||||
tween.TweenCallback(Callable.From(Hide));
|
||||
}
|
||||
|
||||
public void OnShow()
|
||||
{
|
||||
Show();
|
||||
}
|
||||
|
||||
public void OnHide()
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
|
||||
// ... 其他方法
|
||||
}
|
||||
```
|
||||
|
||||
### UI 句柄管理
|
||||
|
||||
```csharp
|
||||
public partial class DialogManager : Node
|
||||
{
|
||||
private UiHandle? _currentDialog;
|
||||
|
||||
public void ShowDialog(string dialogKey)
|
||||
{
|
||||
// 关闭当前对话框
|
||||
CloseCurrentDialog();
|
||||
|
||||
// 显示新对话框
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
_currentDialog = uiRouter.Show(dialogKey, UiLayer.Modal);
|
||||
}
|
||||
|
||||
public void CloseCurrentDialog()
|
||||
{
|
||||
if (_currentDialog.HasValue)
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
uiRouter.Hide(_currentDialog.Value, UiLayer.Modal, destroy: true);
|
||||
_currentDialog = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 多个 Toast 显示
|
||||
|
||||
```csharp
|
||||
public partial class ToastManager : Node
|
||||
{
|
||||
private readonly List<UiHandle> _activeToasts = new();
|
||||
|
||||
public void ShowToast(string message, float duration = 3.0f)
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
// Toast 层支持重入,可以同时显示多个
|
||||
var handle = uiRouter.Show("Toast", UiLayer.Toast, new ToastParam
|
||||
{
|
||||
Message = message
|
||||
});
|
||||
|
||||
_activeToasts.Add(handle);
|
||||
|
||||
// 自动隐藏
|
||||
GetTree().CreateTimer(duration).Timeout += () =>
|
||||
{
|
||||
uiRouter.Hide(handle, UiLayer.Toast, destroy: true);
|
||||
_activeToasts.Remove(handle);
|
||||
};
|
||||
}
|
||||
|
||||
public void ClearAllToasts()
|
||||
{
|
||||
var uiRouter = this.GetSystem<IUiRouter>();
|
||||
|
||||
foreach (var handle in _activeToasts)
|
||||
{
|
||||
uiRouter.Hide(handle, UiLayer.Toast, destroy: true);
|
||||
}
|
||||
|
||||
_activeToasts.Clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **UI 脚本实现 IUiPage 接口**:获得完整的生命周期管理
|
||||
```csharp
|
||||
✓ public partial class MyPage : Control, IUiPage { }
|
||||
✗ public partial class MyPage : Control { } // 无生命周期管理
|
||||
```
|
||||
|
||||
2. **使用正确的 UI 层级**:根据 UI 类型选择合适的层级
|
||||
```csharp
|
||||
✓ Page: 主要页面(主菜单、设置)
|
||||
✓ Overlay: 浮层(信息面板)
|
||||
✓ Modal: 模态对话框(确认框)
|
||||
✓ Toast: 提示消息
|
||||
✓ Topmost: 系统级(加载界面)
|
||||
```
|
||||
|
||||
3. **在 OnEnter 中显示 UI**:确保 UI 正确显示
|
||||
```csharp
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
Show(); // 显示 UI
|
||||
// 初始化 UI 状态
|
||||
}
|
||||
```
|
||||
|
||||
4. **在 OnExit 中隐藏 UI**:确保 UI 正确隐藏
|
||||
```csharp
|
||||
public void OnExit()
|
||||
{
|
||||
Hide(); // 隐藏 UI
|
||||
// 清理 UI 状态
|
||||
}
|
||||
```
|
||||
|
||||
5. **使用 UI 句柄管理非栈 UI**:对于 Modal、Toast 等层级
|
||||
```csharp
|
||||
var handle = uiRouter.Show("Dialog", UiLayer.Modal);
|
||||
// 保存句柄以便后续关闭
|
||||
uiRouter.Hide(handle, UiLayer.Modal, destroy: true);
|
||||
```
|
||||
|
||||
6. **使用 UI 参数传递数据**:避免使用全局变量
|
||||
```csharp
|
||||
✓ uiRouter.Show("Dialog", UiLayer.Modal, new DialogParam { ... });
|
||||
✗ GlobalData.DialogMessage = "..."; // 避免全局状态
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 问题:如何在 Godot UI 中使用 GFramework?
|
||||
|
||||
**解答**:
|
||||
UI 脚本实现 `IUiPage` 和 `IUiPageBehaviorProvider` 接口:
|
||||
|
||||
```csharp
|
||||
public partial class MyPage : Control, IUiPage, IUiPageBehaviorProvider
|
||||
{
|
||||
public void OnEnter(IUiPageEnterParam? param) { }
|
||||
public IUiPageBehavior GetPage() { return new PageLayerUiPageBehavior(this, "MyPage"); }
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:UI 层级有什么区别?
|
||||
|
||||
**解答**:
|
||||
|
||||
- **Page**:栈管理,不可重入,用于主要页面
|
||||
- **Overlay**:可重入,用于浮层
|
||||
- **Modal**:可重入,带遮罩,用于对话框
|
||||
- **Toast**:可重入,轻量提示
|
||||
- **Topmost**:不可重入,最高优先级
|
||||
|
||||
### 问题:如何实现 UI 动画?
|
||||
|
||||
**解答**:
|
||||
在生命周期方法中使用 Godot Tween:
|
||||
|
||||
```csharp
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
var tween = CreateTween();
|
||||
tween.TweenProperty(this, "modulate:a", 1.0f, 0.3f);
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何在 UI 中访问架构组件?
|
||||
|
||||
**解答**:
|
||||
使用扩展方法:
|
||||
|
||||
```csharp
|
||||
public partial class MyPage : Control, IUiPage
|
||||
{
|
||||
public void OnEnter(IUiPageEnterParam? param)
|
||||
{
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
var gameSystem = this.GetSystem<GameSystem>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 问题:如何关闭 Modal 或 Toast?
|
||||
|
||||
**解答**:
|
||||
使用 UI 句柄:
|
||||
|
||||
```csharp
|
||||
// 显示时保存句柄
|
||||
var handle = uiRouter.Show("Dialog", UiLayer.Modal);
|
||||
|
||||
// 关闭时使用句柄
|
||||
uiRouter.Hide(handle, UiLayer.Modal, destroy: true);
|
||||
```
|
||||
|
||||
### 问题:UI 生命周期方法的调用顺序是什么?
|
||||
|
||||
**解答**:
|
||||
|
||||
- 进入:`OnEnter` -> `OnShow`
|
||||
- 暂停:`OnPause` -> `OnHide`
|
||||
- 恢复:`OnShow` -> `OnResume`
|
||||
- 退出:`OnHide` -> `OnExit`
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [UI 系统](/zh-CN/game/ui) - 核心 UI 管理
|
||||
- [Godot 架构集成](/zh-CN/godot/architecture) - Godot 架构基础
|
||||
- [Godot 场景系统](/zh-CN/godot/scene) - Godot 场景集成
|
||||
- [Godot 扩展](/zh-CN/godot/extensions) - Godot 扩展方法
|
||||
950
docs/zh-CN/migration-guide.md
Normal file
950
docs/zh-CN/migration-guide.md
Normal file
@ -0,0 +1,950 @@
|
||||
# 版本迁移指南
|
||||
|
||||
本文档提供 GFramework 不同版本之间的迁移指导,帮助开发者平滑升级到新版本。
|
||||
|
||||
## 概述
|
||||
|
||||
### 迁移指南的使用
|
||||
|
||||
本迁移指南旨在帮助开发者:
|
||||
|
||||
- **了解版本间的重大变更**:明确不同版本之间的 API 变化和行为差异
|
||||
- **规划迁移路径**:根据项目实际情况选择合适的迁移策略
|
||||
- **减少迁移风险**:通过详细的步骤说明和代码示例降低升级风险
|
||||
- **快速定位问题**:提供常见问题的解决方案和回滚策略
|
||||
|
||||
### 阅读建议
|
||||
|
||||
1. **确认当前版本**:查看项目中使用的 GFramework 版本号
|
||||
2. **查看目标版本**:确定要升级到的目标版本
|
||||
3. **阅读相关章节**:重点关注涉及的版本迁移章节
|
||||
4. **测试验证**:在测试环境中完成迁移并充分测试
|
||||
5. **逐步升级**:对于跨多个大版本的升级,建议分步进行
|
||||
|
||||
## 版本兼容性
|
||||
|
||||
### 版本号说明
|
||||
|
||||
GFramework 遵循 [语义化版本](https://semver.org/lang/zh-CN/) 规范:
|
||||
|
||||
```
|
||||
主版本号.次版本号.修订号 (MAJOR.MINOR.PATCH)
|
||||
```
|
||||
|
||||
- **主版本号(MAJOR)**:不兼容的 API 变更
|
||||
- **次版本号(MINOR)**:向后兼容的功能新增
|
||||
- **修订号(PATCH)**:向后兼容的问题修正
|
||||
|
||||
### 兼容性矩阵
|
||||
|
||||
| 源版本 | 目标版本 | 兼容性 | 迁移难度 | 说明 |
|
||||
|-------|-------|---------|------|---------------|
|
||||
| 0.0.x | 0.0.y | ✅ 完全兼容 | 低 | 修订版本,直接升级 |
|
||||
| 0.0.x | 1.0.0 | ⚠️ 部分兼容 | 中 | 需要代码调整 |
|
||||
| 0.x.x | 1.x.x | ❌ 不兼容 | 高 | 重大变更,需要重构 |
|
||||
| 1.x.x | 1.y.y | ✅ 向后兼容 | 低 | 次版本升级,可能有废弃警告 |
|
||||
| 1.x.x | 2.0.0 | ❌ 不兼容 | 高 | 重大变更,需要重构 |
|
||||
|
||||
### .NET 版本支持
|
||||
|
||||
| GFramework 版本 | .NET 8.0 | .NET 9.0 | .NET 10.0 |
|
||||
|---------------|----------|----------|-----------|
|
||||
| 0.0.x | ✅ | ✅ | ✅ |
|
||||
| 1.0.x | ✅ | ✅ | ✅ |
|
||||
| 2.0.x | ❌ | ✅ | ✅ |
|
||||
|
||||
### Godot 版本支持
|
||||
|
||||
| GFramework 版本 | Godot 4.3 | Godot 4.4 | Godot 4.5 | Godot 4.6+ |
|
||||
|---------------|-----------|-----------|-----------|------------|
|
||||
| 0.0.x | ✅ | ✅ | ✅ | ✅ |
|
||||
| 1.0.x | ❌ | ✅ | ✅ | ✅ |
|
||||
| 2.0.x | ❌ | ❌ | ✅ | ✅ |
|
||||
|
||||
## 从 0.x 迁移到 1.x
|
||||
|
||||
### 重大变更概述
|
||||
|
||||
1.0 版本是 GFramework 的第一个稳定版本,引入了多项重大变更以提升框架的稳定性、性能和可维护性。
|
||||
|
||||
#### 架构层面变更
|
||||
|
||||
- **架构初始化方式变更**:统一使用异步初始化
|
||||
- **生命周期阶段调整**:简化阶段流程,移除冗余阶段
|
||||
- **IOC 容器增强**:支持作用域和生命周期管理
|
||||
- **模块系统重构**:引入新的模块注册机制
|
||||
|
||||
#### API 变更
|
||||
|
||||
- **命名空间调整**:部分类型移动到新的命名空间
|
||||
- **接口签名变更**:部分接口方法签名调整
|
||||
- **废弃 API 移除**:移除 0.x 中标记为废弃的 API
|
||||
- **泛型约束调整**:部分泛型方法增加或调整约束
|
||||
|
||||
#### 行为变更
|
||||
|
||||
- **事件传播机制**:优化事件传播逻辑
|
||||
- **协程调度策略**:改进协程调度算法
|
||||
- **资源管理策略**:引入新的资源释放策略
|
||||
|
||||
### 迁移前准备
|
||||
|
||||
#### 1. 备份项目
|
||||
|
||||
```bash
|
||||
# 创建项目备份
|
||||
git checkout -b backup-before-migration
|
||||
git push origin backup-before-migration
|
||||
|
||||
# 或使用文件系统备份
|
||||
cp -r YourProject YourProject-backup
|
||||
```
|
||||
|
||||
#### 2. 检查当前版本
|
||||
|
||||
```bash
|
||||
# 查看当前使用的 GFramework 版本
|
||||
dotnet list package | grep GFramework
|
||||
```
|
||||
|
||||
#### 3. 更新依赖工具
|
||||
|
||||
```bash
|
||||
# 更新 .NET SDK
|
||||
dotnet --version
|
||||
|
||||
# 更新 NuGet 客户端
|
||||
dotnet nuget --version
|
||||
```
|
||||
|
||||
#### 4. 运行现有测试
|
||||
|
||||
```bash
|
||||
# 确保所有测试通过
|
||||
dotnet test
|
||||
|
||||
# 记录测试结果作为基准
|
||||
dotnet test --logger "trx;LogFileName=baseline-tests.trx"
|
||||
```
|
||||
|
||||
### 迁移步骤
|
||||
|
||||
#### 步骤 1:更新 NuGet 包
|
||||
|
||||
```bash
|
||||
# 更新核心包
|
||||
dotnet add package GeWuYou.GFramework.Core --version 1.0.0
|
||||
dotnet add package GeWuYou.GFramework.Core.Abstractions --version 1.0.0
|
||||
|
||||
# 更新游戏扩展包
|
||||
dotnet add package GeWuYou.GFramework.Game --version 1.0.0
|
||||
dotnet add package GeWuYou.GFramework.Game.Abstractions --version 1.0.0
|
||||
|
||||
# 更新 Godot 集成包(如果使用)
|
||||
dotnet add package GeWuYou.GFramework.Godot --version 1.0.0
|
||||
|
||||
# 更新源码生成器
|
||||
dotnet add package GeWuYou.GFramework.SourceGenerators --version 1.0.0
|
||||
```
|
||||
|
||||
#### 步骤 2:更新命名空间引用
|
||||
|
||||
**0.x 版本:**
|
||||
|
||||
```csharp
|
||||
using GFramework.Core;
|
||||
using GFramework.Core.Architecture;
|
||||
using GFramework.Core.Events;
|
||||
```
|
||||
|
||||
**1.x 版本:**
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.Architecture;
|
||||
using GFramework.Core.Abstractions.Events;
|
||||
using GFramework.Core.Architecture;
|
||||
using GFramework.Core.Events;
|
||||
```
|
||||
|
||||
#### 步骤 3:更新架构初始化代码
|
||||
|
||||
**0.x 版本:**
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void Init()
|
||||
{
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterSystem(new GameplaySystem());
|
||||
}
|
||||
}
|
||||
|
||||
// 同步初始化
|
||||
var architecture = new GameArchitecture();
|
||||
architecture.Initialize();
|
||||
```
|
||||
|
||||
**1.x 版本:**
|
||||
|
||||
```csharp
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
protected override void Init()
|
||||
{
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterSystem(new GameplaySystem());
|
||||
}
|
||||
}
|
||||
|
||||
// 推荐使用异步初始化
|
||||
var architecture = new GameArchitecture();
|
||||
await architecture.InitializeAsync();
|
||||
|
||||
// 或者使用同步初始化(不推荐)
|
||||
// architecture.Initialize();
|
||||
```
|
||||
|
||||
#### 步骤 4:更新事件注册代码
|
||||
|
||||
**0.x 版本:**
|
||||
|
||||
```csharp
|
||||
// 注册事件
|
||||
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
|
||||
|
||||
// 发送事件
|
||||
this.SendEvent(new PlayerDiedEvent());
|
||||
```
|
||||
|
||||
**1.x 版本:**
|
||||
|
||||
```csharp
|
||||
// 注册事件(API 保持兼容)
|
||||
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
|
||||
|
||||
// 发送事件(API 保持兼容)
|
||||
this.SendEvent(new PlayerDiedEvent());
|
||||
|
||||
// 新增:带优先级的事件注册
|
||||
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied, priority: 100);
|
||||
```
|
||||
|
||||
#### 步骤 5:更新命令和查询代码
|
||||
|
||||
**0.x 版本:**
|
||||
|
||||
```csharp
|
||||
public class MovePlayerCommand : AbstractCommand
|
||||
{
|
||||
public Vector2 Direction { get; set; }
|
||||
|
||||
protected override void OnDo()
|
||||
{
|
||||
// 执行逻辑
|
||||
}
|
||||
}
|
||||
|
||||
// 发送命令
|
||||
this.SendCommand(new MovePlayerCommand { Direction = direction });
|
||||
```
|
||||
|
||||
**1.x 版本:**
|
||||
|
||||
```csharp
|
||||
// 命令 API 保持兼容
|
||||
public class MovePlayerCommand : AbstractCommand
|
||||
{
|
||||
public Vector2 Direction { get; set; }
|
||||
|
||||
protected override void OnDo()
|
||||
{
|
||||
// 执行逻辑
|
||||
}
|
||||
}
|
||||
|
||||
// 发送命令(API 保持兼容)
|
||||
this.SendCommand(new MovePlayerCommand { Direction = direction });
|
||||
|
||||
// 新增:异步命令支持
|
||||
public class LoadDataCommand : AbstractAsyncCommand
|
||||
{
|
||||
protected override async Task OnDoAsync()
|
||||
{
|
||||
await Task.Delay(100);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 步骤 6:更新 IOC 容器使用
|
||||
|
||||
**0.x 版本:**
|
||||
|
||||
```csharp
|
||||
// 注册服务
|
||||
RegisterUtility(new StorageUtility());
|
||||
|
||||
// 获取服务
|
||||
var storage = this.GetUtility<StorageUtility>();
|
||||
```
|
||||
|
||||
**1.x 版本:**
|
||||
|
||||
```csharp
|
||||
// 注册服务(API 保持兼容)
|
||||
RegisterUtility(new StorageUtility());
|
||||
|
||||
// 获取服务(API 保持兼容)
|
||||
var storage = this.GetUtility<StorageUtility>();
|
||||
|
||||
// 新增:按优先级获取服务
|
||||
var storages = this.GetUtilities<IStorageUtility>();
|
||||
var primaryStorage = storages.FirstOrDefault();
|
||||
```
|
||||
|
||||
#### 步骤 7:更新协程代码
|
||||
|
||||
**0.x 版本:**
|
||||
|
||||
```csharp
|
||||
// 启动协程
|
||||
var handle = CoroutineHelper.Start(MyCoroutine());
|
||||
|
||||
// 等待协程
|
||||
yield return new WaitForCoroutine(handle);
|
||||
```
|
||||
|
||||
**1.x 版本:**
|
||||
|
||||
```csharp
|
||||
// 启动协程(API 保持兼容)
|
||||
var handle = CoroutineHelper.Start(MyCoroutine());
|
||||
|
||||
// 等待协程(API 保持兼容)
|
||||
yield return new WaitForCoroutine(handle);
|
||||
|
||||
// 新增:协程分组和优先级
|
||||
var handle = CoroutineHelper.Start(
|
||||
MyCoroutine(),
|
||||
group: "gameplay",
|
||||
priority: CoroutinePriority.High
|
||||
);
|
||||
```
|
||||
|
||||
### API 变更详解
|
||||
|
||||
#### 废弃的 API
|
||||
|
||||
以下 API 在 1.0 版本中已被移除:
|
||||
|
||||
##### 1. 同步命令查询扩展(已废弃)
|
||||
|
||||
**0.x 版本:**
|
||||
|
||||
```csharp
|
||||
// 这些方法在 1.0 中已移除
|
||||
this.SendCommandSync(command);
|
||||
this.SendQuerySync(query);
|
||||
```
|
||||
|
||||
**1.x 版本:**
|
||||
|
||||
```csharp
|
||||
// 使用标准方法
|
||||
this.SendCommand(command);
|
||||
this.SendQuery(query);
|
||||
|
||||
// 或使用异步方法
|
||||
await this.SendCommandAsync(command);
|
||||
await this.SendQueryAsync(query);
|
||||
```
|
||||
|
||||
##### 2. 旧版事件 API(已废弃)
|
||||
|
||||
**0.x 版本:**
|
||||
|
||||
```csharp
|
||||
// 旧版事件注册方式
|
||||
EventBus.Register<MyEvent>(handler);
|
||||
```
|
||||
|
||||
**1.x 版本:**
|
||||
|
||||
```csharp
|
||||
// 使用新的事件注册方式
|
||||
this.RegisterEvent<MyEvent>(handler);
|
||||
|
||||
// 或使用事件总线
|
||||
this.GetEventBus().Register<MyEvent>(handler);
|
||||
```
|
||||
|
||||
##### 3. 直接访问 IOC 容器(已限制)
|
||||
|
||||
**0.x 版本:**
|
||||
|
||||
```csharp
|
||||
// 直接访问容器
|
||||
var container = architecture.Container;
|
||||
container.Register<IService, ServiceImpl>();
|
||||
```
|
||||
|
||||
**1.x 版本:**
|
||||
|
||||
```csharp
|
||||
// 使用架构提供的注册方法
|
||||
architecture.RegisterUtility<IService>(new ServiceImpl());
|
||||
|
||||
// 或在 Init 方法中注册
|
||||
protected override void Init()
|
||||
{
|
||||
RegisterUtility<IService>(new ServiceImpl());
|
||||
}
|
||||
```
|
||||
|
||||
#### 新增的 API
|
||||
|
||||
##### 1. 优先级支持
|
||||
|
||||
```csharp
|
||||
// 事件优先级
|
||||
this.RegisterEvent<MyEvent>(handler, priority: 100);
|
||||
|
||||
// 服务优先级
|
||||
RegisterUtility<IService>(service, priority: 10);
|
||||
|
||||
// 协程优先级
|
||||
CoroutineHelper.Start(routine, priority: CoroutinePriority.High);
|
||||
```
|
||||
|
||||
##### 2. 异步初始化增强
|
||||
|
||||
```csharp
|
||||
// 异步初始化架构
|
||||
await architecture.InitializeAsync();
|
||||
|
||||
// 等待架构就绪
|
||||
await architecture.WaitUntilReadyAsync();
|
||||
|
||||
// 异步初始化组件
|
||||
public class MyModel : AbstractModel, IAsyncInitializable
|
||||
{
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await LoadDataAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 3. 事件过滤和统计
|
||||
|
||||
```csharp
|
||||
// 事件过滤
|
||||
this.RegisterEvent<MyEvent>(handler, filter: e => e.IsValid);
|
||||
|
||||
// 事件统计
|
||||
var stats = eventBus.GetStatistics();
|
||||
Console.WriteLine($"Total events: {stats.TotalEventsSent}");
|
||||
```
|
||||
|
||||
##### 4. 协程分组管理
|
||||
|
||||
```csharp
|
||||
// 创建协程组
|
||||
var handle = CoroutineHelper.Start(
|
||||
routine,
|
||||
group: "ui-animations"
|
||||
);
|
||||
|
||||
// 暂停协程组
|
||||
CoroutineHelper.PauseGroup("ui-animations");
|
||||
|
||||
// 恢复协程组
|
||||
CoroutineHelper.ResumeGroup("ui-animations");
|
||||
|
||||
// 停止协程组
|
||||
CoroutineHelper.StopGroup("ui-animations");
|
||||
```
|
||||
|
||||
### 配置变更
|
||||
|
||||
#### 架构配置
|
||||
|
||||
**0.x 版本:**
|
||||
|
||||
```csharp
|
||||
var architecture = new GameArchitecture();
|
||||
```
|
||||
|
||||
**1.x 版本:**
|
||||
|
||||
```csharp
|
||||
// 使用配置对象
|
||||
var config = new ArchitectureConfiguration
|
||||
{
|
||||
ArchitectureProperties = new ArchitectureProperties
|
||||
{
|
||||
StrictPhaseValidation = true,
|
||||
AllowLateRegistration = false
|
||||
},
|
||||
LoggerProperties = new LoggerProperties
|
||||
{
|
||||
MinimumLevel = LogLevel.Information
|
||||
}
|
||||
};
|
||||
|
||||
var architecture = new GameArchitecture(configuration: config);
|
||||
```
|
||||
|
||||
#### 日志配置
|
||||
|
||||
**0.x 版本:**
|
||||
|
||||
```csharp
|
||||
// 使用默认日志
|
||||
```
|
||||
|
||||
**1.x 版本:**
|
||||
|
||||
```csharp
|
||||
// 配置日志系统
|
||||
var logConfig = new LoggingConfiguration
|
||||
{
|
||||
MinimumLevel = LogLevel.Debug,
|
||||
Appenders = new List<ILogAppender>
|
||||
{
|
||||
new ConsoleAppender(),
|
||||
new FileAppender("logs/game.log")
|
||||
},
|
||||
Filters = new List<ILogFilter>
|
||||
{
|
||||
new LogLevelFilter(LogLevel.Warning),
|
||||
new NamespaceFilter("GFramework.*")
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 依赖变更
|
||||
|
||||
#### NuGet 包更新
|
||||
|
||||
| 包名 | 0.x 版本 | 1.x 版本 | 变更说明 |
|
||||
|------------------------------------------|--------|--------|----------|
|
||||
| Microsoft.Extensions.DependencyInjection | 8.0.0 | 10.0.3 | 升级到最新版本 |
|
||||
| Arch | 1.x | 2.1.0 | ECS 框架升级 |
|
||||
| Arch.System | 1.0.x | 1.1.0 | 系统组件升级 |
|
||||
|
||||
#### 包拆分
|
||||
|
||||
1.0 版本对包结构进行了优化:
|
||||
|
||||
**0.x 版本:**
|
||||
|
||||
```xml
|
||||
<PackageReference Include="GeWuYou.GFramework" Version="0.0.200" />
|
||||
```
|
||||
|
||||
**1.x 版本:**
|
||||
|
||||
```xml
|
||||
<!-- 推荐按需引用 -->
|
||||
<PackageReference Include="GeWuYou.GFramework.Core" Version="1.0.0" />
|
||||
<PackageReference Include="GeWuYou.GFramework.Core.Abstractions" Version="1.0.0" />
|
||||
<PackageReference Include="GeWuYou.GFramework.Game" Version="1.0.0" />
|
||||
<PackageReference Include="GeWuYou.GFramework.Godot" Version="1.0.0" />
|
||||
```
|
||||
|
||||
### 代码迁移工具
|
||||
|
||||
#### 自动化迁移工具
|
||||
|
||||
GFramework 提供了迁移工具来自动化部分迁移工作:
|
||||
|
||||
```bash
|
||||
# 安装迁移工具
|
||||
dotnet tool install -g GFramework.MigrationTool
|
||||
|
||||
# 运行迁移分析
|
||||
gframework-migrate analyze --project YourProject.csproj
|
||||
|
||||
# 执行自动迁移
|
||||
gframework-migrate apply --project YourProject.csproj --target-version 1.0.0
|
||||
|
||||
# 生成迁移报告
|
||||
gframework-migrate report --output migration-report.html
|
||||
```
|
||||
|
||||
#### 手动迁移检查清单
|
||||
|
||||
使用以下清单确保完整迁移:
|
||||
|
||||
- [ ] 更新所有 NuGet 包到 1.0.0
|
||||
- [ ] 更新命名空间引用
|
||||
- [ ] 替换废弃的 API
|
||||
- [ ] 更新架构初始化代码
|
||||
- [ ] 更新配置代码
|
||||
- [ ] 运行所有单元测试
|
||||
- [ ] 运行集成测试
|
||||
- [ ] 执行性能测试
|
||||
- [ ] 更新文档和注释
|
||||
- [ ] 代码审查
|
||||
|
||||
### 测试迁移
|
||||
|
||||
#### 单元测试更新
|
||||
|
||||
**0.x 版本:**
|
||||
|
||||
```csharp
|
||||
[Test]
|
||||
public void TestArchitectureInit()
|
||||
{
|
||||
var architecture = new TestArchitecture();
|
||||
architecture.Initialize();
|
||||
|
||||
Assert.That(architecture.CurrentPhase, Is.EqualTo(ArchitecturePhase.Ready));
|
||||
}
|
||||
```
|
||||
|
||||
**1.x 版本:**
|
||||
|
||||
```csharp
|
||||
[Test]
|
||||
public async Task TestArchitectureInit()
|
||||
{
|
||||
var architecture = new TestArchitecture();
|
||||
await architecture.InitializeAsync();
|
||||
|
||||
Assert.That(architecture.CurrentPhase, Is.EqualTo(ArchitecturePhase.Ready));
|
||||
}
|
||||
```
|
||||
|
||||
#### 集成测试更新
|
||||
|
||||
**0.x 版本:**
|
||||
|
||||
```csharp
|
||||
[Test]
|
||||
public void TestGameFlow()
|
||||
{
|
||||
var game = new GameArchitecture();
|
||||
game.Initialize();
|
||||
|
||||
game.SendCommand(new StartGameCommand());
|
||||
var score = game.SendQuery(new GetScoreQuery());
|
||||
|
||||
Assert.That(score, Is.EqualTo(0));
|
||||
}
|
||||
```
|
||||
|
||||
**1.x 版本:**
|
||||
|
||||
```csharp
|
||||
[Test]
|
||||
public async Task TestGameFlow()
|
||||
{
|
||||
var game = new GameArchitecture();
|
||||
await game.InitializeAsync();
|
||||
|
||||
await game.SendCommandAsync(new StartGameCommand());
|
||||
var score = await game.SendQueryAsync(new GetScoreQuery());
|
||||
|
||||
Assert.That(score, Is.EqualTo(0));
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 编译错误
|
||||
|
||||
#### 问题 1:命名空间找不到
|
||||
|
||||
**错误信息:**
|
||||
|
||||
```
|
||||
error CS0246: The type or namespace name 'IArchitecture' could not be found
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
|
||||
```csharp
|
||||
// 添加正确的命名空间引用
|
||||
using GFramework.Core.Abstractions.Architecture;
|
||||
```
|
||||
|
||||
#### 问题 2:方法签名不匹配
|
||||
|
||||
**错误信息:**
|
||||
|
||||
```
|
||||
error CS1501: No overload for method 'RegisterEvent' takes 1 arguments
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
|
||||
```csharp
|
||||
// 0.x 版本
|
||||
this.RegisterEvent<MyEvent>(handler);
|
||||
|
||||
// 1.x 版本(兼容)
|
||||
this.RegisterEvent<MyEvent>(handler);
|
||||
|
||||
// 1.x 版本(带优先级)
|
||||
this.RegisterEvent<MyEvent>(handler, priority: 100);
|
||||
```
|
||||
|
||||
#### 问题 3:泛型约束不满足
|
||||
|
||||
**错误信息:**
|
||||
|
||||
```
|
||||
error CS0311: The type 'MyType' cannot be used as type parameter 'T'
|
||||
in the generic type or method. There is no implicit reference conversion
|
||||
from 'MyType' to 'GFramework.Core.Abstractions.IModel'.
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
|
||||
```csharp
|
||||
// 确保类型实现了正确的接口
|
||||
public class MyModel : AbstractModel, IModel
|
||||
{
|
||||
// 实现
|
||||
}
|
||||
```
|
||||
|
||||
### 运行时错误
|
||||
|
||||
#### 问题 1:架构未初始化
|
||||
|
||||
**错误信息:**
|
||||
|
||||
```
|
||||
InvalidOperationException: Architecture is not initialized
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
|
||||
```csharp
|
||||
// 确保在使用前初始化架构
|
||||
var architecture = new GameArchitecture();
|
||||
await architecture.InitializeAsync();
|
||||
await architecture.WaitUntilReadyAsync();
|
||||
|
||||
// 然后再使用
|
||||
this.SendCommand(new MyCommand());
|
||||
```
|
||||
|
||||
#### 问题 2:服务未注册
|
||||
|
||||
**错误信息:**
|
||||
|
||||
```
|
||||
InvalidOperationException: Service of type 'IMyService' is not registered
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
|
||||
```csharp
|
||||
// 在 Init 方法中注册服务
|
||||
protected override void Init()
|
||||
{
|
||||
RegisterUtility<IMyService>(new MyServiceImpl());
|
||||
}
|
||||
```
|
||||
|
||||
#### 问题 3:事件处理器未触发
|
||||
|
||||
**问题描述:**
|
||||
事件发送后,注册的处理器没有被调用。
|
||||
|
||||
**解决方案:**
|
||||
|
||||
```csharp
|
||||
// 确保事件处理器正确注册
|
||||
var unregister = this.RegisterEvent<MyEvent>(OnMyEvent);
|
||||
|
||||
// 确保在对象销毁时注销
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
unregister?.UnRegister();
|
||||
}
|
||||
|
||||
// 检查事件类型是否匹配
|
||||
this.SendEvent(new MyEvent()); // 确保类型完全一致
|
||||
```
|
||||
|
||||
### 性能问题
|
||||
|
||||
#### 问题 1:初始化时间过长
|
||||
|
||||
**问题描述:**
|
||||
架构初始化耗时明显增加。
|
||||
|
||||
**解决方案:**
|
||||
|
||||
```csharp
|
||||
// 使用异步初始化
|
||||
await architecture.InitializeAsync();
|
||||
|
||||
// 对于耗时的初始化操作,使用异步方法
|
||||
public class MyModel : AbstractModel, IAsyncInitializable
|
||||
{
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// 异步加载数据
|
||||
await LoadDataAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 问题 2:事件处理性能下降
|
||||
|
||||
**问题描述:**
|
||||
事件处理速度变慢。
|
||||
|
||||
**解决方案:**
|
||||
|
||||
```csharp
|
||||
// 使用事件过滤减少不必要的处理
|
||||
this.RegisterEvent<MyEvent>(
|
||||
handler,
|
||||
filter: e => e.ShouldProcess
|
||||
);
|
||||
|
||||
// 使用优先级控制处理顺序
|
||||
this.RegisterEvent<MyEvent>(
|
||||
criticalHandler,
|
||||
priority: 100
|
||||
);
|
||||
```
|
||||
|
||||
### 兼容性问题
|
||||
|
||||
#### 问题 1:Godot 版本不兼容
|
||||
|
||||
**问题描述:**
|
||||
升级后在 Godot 4.3 中无法运行。
|
||||
|
||||
**解决方案:**
|
||||
|
||||
```bash
|
||||
# GFramework 1.0 要求 Godot 4.4+
|
||||
# 升级 Godot 到 4.4 或更高版本
|
||||
```
|
||||
|
||||
#### 问题 2:.NET 版本不兼容
|
||||
|
||||
**问题描述:**
|
||||
项目使用 .NET 7.0,无法使用 GFramework 1.0。
|
||||
|
||||
**解决方案:**
|
||||
|
||||
```xml
|
||||
<!-- 升级项目到 .NET 8.0 或更高版本 -->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
## 回滚方案
|
||||
|
||||
如果迁移过程中遇到无法解决的问题,可以按以下步骤回滚:
|
||||
|
||||
### 步骤 1:恢复包版本
|
||||
|
||||
```bash
|
||||
# 回滚到 0.x 版本
|
||||
dotnet add package GeWuYou.GFramework.Core --version 0.0.200
|
||||
dotnet add package GeWuYou.GFramework.Core.Abstractions --version 0.0.200
|
||||
dotnet add package GeWuYou.GFramework.Game --version 0.0.200
|
||||
dotnet add package GeWuYou.GFramework.Godot --version 0.0.200
|
||||
```
|
||||
|
||||
### 步骤 2:恢复代码
|
||||
|
||||
```bash
|
||||
# 从备份分支恢复
|
||||
git checkout backup-before-migration
|
||||
|
||||
# 或从文件系统备份恢复
|
||||
rm -rf YourProject
|
||||
cp -r YourProject-backup YourProject
|
||||
```
|
||||
|
||||
### 步骤 3:验证回滚
|
||||
|
||||
```bash
|
||||
# 清理构建缓存
|
||||
dotnet clean
|
||||
rm -rf bin obj
|
||||
|
||||
# 重新构建
|
||||
dotnet build
|
||||
|
||||
# 运行测试
|
||||
dotnet test
|
||||
```
|
||||
|
||||
### 步骤 4:记录问题
|
||||
|
||||
创建问题报告,包含:
|
||||
|
||||
- 遇到的具体错误
|
||||
- 错误发生的环境信息
|
||||
- 复现步骤
|
||||
- 相关代码片段
|
||||
|
||||
提交到 [GitHub Issues](https://github.com/GeWuYou/GFramework/issues)。
|
||||
|
||||
## 获取帮助
|
||||
|
||||
### 官方资源
|
||||
|
||||
- **文档中心**:[https://gewuyou.github.io/GFramework/](https://gewuyou.github.io/GFramework/)
|
||||
- **GitHub 仓库**:[https://github.com/GeWuYou/GFramework](https://github.com/GeWuYou/GFramework)
|
||||
- **问题追踪**:[https://github.com/GeWuYou/GFramework/issues](https://github.com/GeWuYou/GFramework/issues)
|
||||
- **讨论区**:[https://github.com/GeWuYou/GFramework/discussions](https://github.com/GeWuYou/GFramework/discussions)
|
||||
|
||||
### 社区支持
|
||||
|
||||
- 在 GitHub Discussions 中提问
|
||||
- 查看已有的 Issues 和 Pull Requests
|
||||
- 参考示例项目和教程
|
||||
|
||||
### 商业支持
|
||||
|
||||
如需专业的迁移支持服务,请联系项目维护团队。
|
||||
|
||||
## 附录
|
||||
|
||||
### A. 完整的 API 对照表
|
||||
|
||||
| 0.x API | 1.x API | 说明 |
|
||||
|-----------------------------|----------------------------------------|------------|
|
||||
| `architecture.Initialize()` | `await architecture.InitializeAsync()` | 推荐使用异步初始化 |
|
||||
| `this.SendCommandSync()` | `this.SendCommand()` | 移除 Sync 后缀 |
|
||||
| `this.SendQuerySync()` | `this.SendQuery()` | 移除 Sync 后缀 |
|
||||
| `EventBus.Register()` | `this.RegisterEvent()` | 使用扩展方法 |
|
||||
| `Container.Register()` | `RegisterUtility()` | 使用架构方法 |
|
||||
|
||||
### B. 迁移时间估算
|
||||
|
||||
| 项目规模 | 预估时间 | 说明 |
|
||||
|---------------|-------|--------------|
|
||||
| 小型(<10k 行) | 1-2 天 | 主要是包更新和测试 |
|
||||
| 中型(10k-50k 行) | 3-5 天 | 需要代码审查和重构 |
|
||||
| 大型(>50k 行) | 1-2 周 | 需要分模块迁移和充分测试 |
|
||||
|
||||
### C. 相关资源
|
||||
|
||||
- [架构设计文档](./core/architecture.md)
|
||||
- [事件系统文档](./core/events.md)
|
||||
- [命令查询文档](./core/command.md)
|
||||
- [协程系统文档](./core/coroutine.md)
|
||||
- [最佳实践](./best-practices/architecture-patterns.md)
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:1.0.0
|
||||
**最后更新**:2026-03-07
|
||||
**许可证**:Apache 2.0
|
||||
File diff suppressed because it is too large
Load Diff
598
docs/zh-CN/tutorials/coroutine-tutorial.md
Normal file
598
docs/zh-CN/tutorials/coroutine-tutorial.md
Normal file
@ -0,0 +1,598 @@
|
||||
---
|
||||
title: 使用协程系统
|
||||
description: 学习如何使用协程系统实现异步操作和时间控制
|
||||
---
|
||||
|
||||
# 使用协程系统
|
||||
|
||||
## 学习目标
|
||||
|
||||
完成本教程后,你将能够:
|
||||
|
||||
- 理解协程的基本概念和执行机制
|
||||
- 创建和启动协程
|
||||
- 使用各种等待指令控制协程执行
|
||||
- 在架构组件中使用协程
|
||||
- 实现常见的游戏逻辑(延迟执行、循环任务、事件等待)
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已安装 GFramework.Core NuGet 包
|
||||
- 了解 C# 基础语法和迭代器(IEnumerator)
|
||||
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
|
||||
- 了解[生命周期管理](/zh-CN/core/lifecycle)
|
||||
|
||||
## 步骤 1:创建第一个协程
|
||||
|
||||
首先,让我们创建一个简单的协程来理解基本概念。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.coroutine;
|
||||
using GFramework.Core.coroutine;
|
||||
using GFramework.Core.coroutine.instructions;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
public class TutorialSystem : AbstractSystem
|
||||
{
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 启动协程
|
||||
this.StartCoroutine(MyFirstCoroutine());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 第一个协程示例
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> MyFirstCoroutine()
|
||||
{
|
||||
Console.WriteLine("协程开始执行");
|
||||
|
||||
// 等待 1 秒
|
||||
yield return CoroutineHelper.WaitForSeconds(1.0);
|
||||
|
||||
Console.WriteLine("1 秒后执行");
|
||||
|
||||
// 等待 1 帧
|
||||
yield return CoroutineHelper.WaitForOneFrame();
|
||||
|
||||
Console.WriteLine("下一帧执行");
|
||||
|
||||
// 等待 5 帧
|
||||
yield return CoroutineHelper.WaitForFrames(5);
|
||||
|
||||
Console.WriteLine("5 帧后执行");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 协程方法返回 `IEnumerator<IYieldInstruction>`
|
||||
- 使用 `yield return` 返回等待指令
|
||||
- `this.StartCoroutine()` 扩展方法启动协程
|
||||
- `WaitForSeconds` 等待指定秒数
|
||||
- `WaitForOneFrame` 等待一帧
|
||||
- `WaitForFrames` 等待多帧
|
||||
|
||||
## 步骤 2:实现生命值自动恢复
|
||||
|
||||
让我们实现一个实用的功能:玩家生命值自动恢复。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.model;
|
||||
using GFramework.Core.Abstractions.property;
|
||||
using GFramework.Core.model;
|
||||
|
||||
namespace MyGame.Models
|
||||
{
|
||||
public class PlayerModel : AbstractModel
|
||||
{
|
||||
// 当前生命值
|
||||
public BindableProperty<int> Health { get; } = new(100);
|
||||
|
||||
// 最大生命值
|
||||
public BindableProperty<int> MaxHealth { get; } = new(100);
|
||||
|
||||
// 是否启用自动恢复
|
||||
public BindableProperty<bool> AutoRegenEnabled { get; } = new(true);
|
||||
|
||||
private CoroutineHandle? _regenHandle;
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 启动生命值恢复协程
|
||||
StartHealthRegeneration();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动生命值恢复
|
||||
/// </summary>
|
||||
public void StartHealthRegeneration()
|
||||
{
|
||||
// 如果已经在运行,先停止
|
||||
if (_regenHandle.HasValue)
|
||||
{
|
||||
this.StopCoroutine(_regenHandle.Value);
|
||||
}
|
||||
|
||||
// 启动新的恢复协程
|
||||
_regenHandle = this.StartCoroutine(HealthRegenerationCoroutine());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止生命值恢复
|
||||
/// </summary>
|
||||
public void StopHealthRegeneration()
|
||||
{
|
||||
if (_regenHandle.HasValue)
|
||||
{
|
||||
this.StopCoroutine(_regenHandle.Value);
|
||||
_regenHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生命值恢复协程
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> HealthRegenerationCoroutine()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// 等待 1 秒
|
||||
yield return CoroutineHelper.WaitForSeconds(1.0);
|
||||
|
||||
// 检查是否启用自动恢复
|
||||
if (!AutoRegenEnabled.Value)
|
||||
continue;
|
||||
|
||||
// 如果生命值未满,恢复 5 点
|
||||
if (Health.Value < MaxHealth.Value)
|
||||
{
|
||||
Health.Value = Math.Min(Health.Value + 5, MaxHealth.Value);
|
||||
Console.WriteLine($"生命值恢复: {Health.Value}/{MaxHealth.Value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 使用 `while (true)` 创建无限循环协程
|
||||
- 保存协程句柄以便后续控制
|
||||
- 使用 `StopCoroutine` 停止协程
|
||||
- 协程中可以访问类成员变量
|
||||
|
||||
## 步骤 3:实现技能冷却系统
|
||||
|
||||
接下来实现一个技能冷却系统,展示如何使用协程管理时间相关的游戏逻辑。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.system;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
public class SkillSystem : AbstractSystem
|
||||
{
|
||||
// 技能冷却状态
|
||||
private readonly Dictionary<string, bool> _skillCooldowns = new();
|
||||
|
||||
/// <summary>
|
||||
/// 使用技能
|
||||
/// </summary>
|
||||
public bool UseSkill(string skillName, double cooldownTime)
|
||||
{
|
||||
// 检查是否在冷却中
|
||||
if (_skillCooldowns.TryGetValue(skillName, out var isOnCooldown) && isOnCooldown)
|
||||
{
|
||||
Console.WriteLine($"技能 {skillName} 冷却中...");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 执行技能
|
||||
Console.WriteLine($"使用技能: {skillName}");
|
||||
|
||||
// 启动冷却协程
|
||||
this.StartCoroutine(SkillCooldownCoroutine(skillName, cooldownTime));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 技能冷却协程
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> SkillCooldownCoroutine(string skillName, double cooldownTime)
|
||||
{
|
||||
// 标记为冷却中
|
||||
_skillCooldowns[skillName] = true;
|
||||
|
||||
Console.WriteLine($"技能 {skillName} 开始冷却 {cooldownTime} 秒");
|
||||
|
||||
// 等待冷却时间
|
||||
yield return CoroutineHelper.WaitForSeconds(cooldownTime);
|
||||
|
||||
// 冷却结束
|
||||
_skillCooldowns[skillName] = false;
|
||||
Console.WriteLine($"技能 {skillName} 冷却完成");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 带进度显示的技能冷却
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> SkillCooldownWithProgressCoroutine(
|
||||
string skillName,
|
||||
double cooldownTime)
|
||||
{
|
||||
_skillCooldowns[skillName] = true;
|
||||
|
||||
// 使用 WaitForProgress 显示冷却进度
|
||||
yield return CoroutineHelper.WaitForProgress(
|
||||
duration: cooldownTime,
|
||||
onProgress: progress =>
|
||||
{
|
||||
Console.WriteLine($"技能 {skillName} 冷却进度: {progress * 100:F0}%");
|
||||
}
|
||||
);
|
||||
|
||||
_skillCooldowns[skillName] = false;
|
||||
Console.WriteLine($"技能 {skillName} 冷却完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 使用字典管理多个技能的冷却状态
|
||||
- 每个技能使用独立的协程管理冷却
|
||||
- `WaitForProgress` 可以在等待期间执行回调
|
||||
- 协程结束后自动清理冷却状态
|
||||
|
||||
## 步骤 4:等待事件触发
|
||||
|
||||
实现一个等待玩家完成任务的系统,展示如何在协程中等待事件。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.events;
|
||||
using GFramework.Core.coroutine.instructions;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
// 任务完成事件
|
||||
public record QuestCompletedEvent(int QuestId, string QuestName) : IEvent;
|
||||
|
||||
public class QuestSystem : AbstractSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 开始任务并等待完成
|
||||
/// </summary>
|
||||
public void StartQuest(int questId, string questName)
|
||||
{
|
||||
this.StartCoroutine(QuestCoroutine(questId, questName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务协程
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> QuestCoroutine(int questId, string questName)
|
||||
{
|
||||
Console.WriteLine($"任务开始: {questName}");
|
||||
|
||||
// 获取事件总线
|
||||
var eventBus = this.GetService<IEventBus>();
|
||||
|
||||
// 等待任务完成事件
|
||||
var waitEvent = new WaitForEvent<QuestCompletedEvent>(
|
||||
eventBus,
|
||||
evt => evt.QuestId == questId // 过滤条件
|
||||
);
|
||||
|
||||
yield return waitEvent;
|
||||
|
||||
// 获取事件数据
|
||||
var completedEvent = waitEvent.EventData;
|
||||
Console.WriteLine($"任务完成: {completedEvent.QuestName}");
|
||||
|
||||
// 发放奖励
|
||||
GiveReward(questId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 带超时的任务
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> TimedQuestCoroutine(
|
||||
int questId,
|
||||
string questName,
|
||||
double timeLimit)
|
||||
{
|
||||
Console.WriteLine($"限时任务开始: {questName} (时限: {timeLimit}秒)");
|
||||
|
||||
var eventBus = this.GetService<IEventBus>();
|
||||
|
||||
// 等待事件,带超时
|
||||
var waitEvent = new WaitForEventWithTimeout<QuestCompletedEvent>(
|
||||
eventBus,
|
||||
timeout: timeLimit,
|
||||
predicate: evt => evt.QuestId == questId
|
||||
);
|
||||
|
||||
yield return waitEvent;
|
||||
|
||||
if (waitEvent.IsTimeout)
|
||||
{
|
||||
Console.WriteLine($"任务超时失败: {questName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"任务完成: {questName}");
|
||||
GiveReward(questId);
|
||||
}
|
||||
}
|
||||
|
||||
private void GiveReward(int questId)
|
||||
{
|
||||
Console.WriteLine($"发放任务 {questId} 的奖励");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- `WaitForEvent` 等待特定事件触发
|
||||
- 可以使用 `predicate` 参数过滤事件
|
||||
- `WaitForEventWithTimeout` 支持超时机制
|
||||
- 通过 `EventData` 属性获取事件数据
|
||||
|
||||
## 步骤 5:协程组合与嵌套
|
||||
|
||||
实现一个复杂的游戏流程,展示如何组合多个协程。
|
||||
|
||||
```csharp
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
public class GameFlowSystem : AbstractSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 游戏开始流程
|
||||
/// </summary>
|
||||
public void StartGame()
|
||||
{
|
||||
this.StartCoroutine(GameStartSequence());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏开始序列
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> GameStartSequence()
|
||||
{
|
||||
Console.WriteLine("=== 游戏开始 ===");
|
||||
|
||||
// 1. 显示标题
|
||||
yield return ShowTitle();
|
||||
|
||||
// 2. 加载资源
|
||||
yield return LoadResources();
|
||||
|
||||
// 3. 初始化玩家
|
||||
yield return InitializePlayer();
|
||||
|
||||
// 4. 播放开场动画
|
||||
yield return PlayOpeningAnimation();
|
||||
|
||||
Console.WriteLine("=== 游戏准备完成 ===");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示标题
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> ShowTitle()
|
||||
{
|
||||
Console.WriteLine("显示游戏标题...");
|
||||
yield return CoroutineHelper.WaitForSeconds(2.0);
|
||||
Console.WriteLine("标题显示完成");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载资源
|
||||
/// </summary>
|
||||
private IEnumerator<IYieldInstruction> LoadResources()
|
||||
{
|
||||
Console.WriteLine("开始加载资源...");
|
||||
|
||||
// 并行加载多个资源
|
||||
var loadTextures = LoadTexturesCoroutine();
|
||||
var loadAudio = LoadAudioCoroutine();
|
||||
var loadModels = LoadModelsCoroutine();
|
||||
|
||||
// 等待所有资源加载完成
|
||||
yield return new WaitForAllCoroutines(
|
||||
this.GetCoroutineScheduler(),
|
||||
loadTextures,
|
||||
loadAudio,
|
||||
loadModels
|
||||
);
|
||||
|
||||
Console.WriteLine("所有资源加载完成");
|
||||
}
|
||||
|
||||
private IEnumerator<IYieldInstruction> LoadTexturesCoroutine()
|
||||
{
|
||||
Console.WriteLine(" 加载纹理...");
|
||||
yield return CoroutineHelper.WaitForSeconds(1.0);
|
||||
Console.WriteLine(" 纹理加载完成");
|
||||
}
|
||||
|
||||
private IEnumerator<IYieldInstruction> LoadAudioCoroutine()
|
||||
{
|
||||
Console.WriteLine(" 加载音频...");
|
||||
yield return CoroutineHelper.WaitForSeconds(1.5);
|
||||
Console.WriteLine(" 音频加载完成");
|
||||
}
|
||||
|
||||
private IEnumerator<IYieldInstruction> LoadModelsCoroutine()
|
||||
{
|
||||
Console.WriteLine(" 加载模型...");
|
||||
yield return CoroutineHelper.WaitForSeconds(0.8);
|
||||
Console.WriteLine(" 模型加载完成");
|
||||
}
|
||||
|
||||
private IEnumerator<IYieldInstruction> InitializePlayer()
|
||||
{
|
||||
Console.WriteLine("初始化玩家...");
|
||||
yield return CoroutineHelper.WaitForSeconds(0.5);
|
||||
Console.WriteLine("玩家初始化完成");
|
||||
}
|
||||
|
||||
private IEnumerator<IYieldInstruction> PlayOpeningAnimation()
|
||||
{
|
||||
Console.WriteLine("播放开场动画...");
|
||||
yield return CoroutineHelper.WaitForSeconds(3.0);
|
||||
Console.WriteLine("开场动画播放完成");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取协程调度器
|
||||
/// </summary>
|
||||
private CoroutineScheduler GetCoroutineScheduler()
|
||||
{
|
||||
// 从架构服务中获取
|
||||
return this.GetService<CoroutineScheduler>();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 使用 `yield return` 调用其他协程实现嵌套
|
||||
- `WaitForAllCoroutines` 并行执行多个协程
|
||||
- 协程可以像函数一样组合和复用
|
||||
- 清晰的流程控制,避免回调嵌套
|
||||
|
||||
## 完整代码
|
||||
|
||||
### GameArchitecture.cs
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.architecture;
|
||||
|
||||
namespace MyGame
|
||||
{
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
public static IArchitecture Interface { get; private set; }
|
||||
|
||||
protected override void Init()
|
||||
{
|
||||
Interface = this;
|
||||
|
||||
// 注册 Model
|
||||
RegisterModel(new PlayerModel());
|
||||
|
||||
// 注册 System
|
||||
RegisterSystem(new TutorialSystem());
|
||||
RegisterSystem(new SkillSystem());
|
||||
RegisterSystem(new QuestSystem());
|
||||
RegisterSystem(new GameFlowSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 测试代码
|
||||
|
||||
```csharp
|
||||
using MyGame;
|
||||
using MyGame.Systems;
|
||||
|
||||
// 初始化架构
|
||||
var architecture = new GameArchitecture();
|
||||
architecture.Initialize();
|
||||
await architecture.WaitUntilReadyAsync();
|
||||
|
||||
// 测试技能系统
|
||||
var skillSystem = architecture.GetSystem<SkillSystem>();
|
||||
skillSystem.UseSkill("火球术", 3.0);
|
||||
await Task.Delay(1000);
|
||||
skillSystem.UseSkill("火球术", 3.0); // 冷却中
|
||||
await Task.Delay(3000);
|
||||
skillSystem.UseSkill("火球术", 3.0); // 冷却完成
|
||||
|
||||
// 测试任务系统
|
||||
var questSystem = architecture.GetSystem<QuestSystem>();
|
||||
questSystem.StartQuest(1, "击败史莱姆");
|
||||
|
||||
// 模拟任务完成
|
||||
await Task.Delay(2000);
|
||||
var eventBus = architecture.GetService<IEventBus>();
|
||||
eventBus.Publish(new QuestCompletedEvent(1, "击败史莱姆"));
|
||||
|
||||
// 测试游戏流程
|
||||
var gameFlowSystem = architecture.GetSystem<GameFlowSystem>();
|
||||
gameFlowSystem.StartGame();
|
||||
```
|
||||
|
||||
## 运行结果
|
||||
|
||||
运行程序后,你将看到类似以下的输出:
|
||||
|
||||
```
|
||||
协程开始执行
|
||||
1 秒后执行
|
||||
下一帧执行
|
||||
5 帧后执行
|
||||
|
||||
使用技能: 火球术
|
||||
技能 火球术 开始冷却 3.0 秒
|
||||
技能 火球术 冷却中...
|
||||
技能 火球术 冷却完成
|
||||
使用技能: 火球术
|
||||
|
||||
任务开始: 击败史莱姆
|
||||
任务完成: 击败史莱姆
|
||||
发放任务 1 的奖励
|
||||
|
||||
=== 游戏开始 ===
|
||||
显示游戏标题...
|
||||
标题显示完成
|
||||
开始加载资源...
|
||||
加载纹理...
|
||||
加载音频...
|
||||
加载模型...
|
||||
模型加载完成
|
||||
纹理加载完成
|
||||
音频加载完成
|
||||
所有资源加载完成
|
||||
初始化玩家...
|
||||
玩家初始化完成
|
||||
播放开场动画...
|
||||
开场动画播放完成
|
||||
=== 游戏准备完成 ===
|
||||
```
|
||||
|
||||
**验证步骤**:
|
||||
|
||||
1. 协程按预期顺序执行
|
||||
2. 技能冷却系统正常工作
|
||||
3. 事件等待功能正确
|
||||
4. 并行加载资源成功
|
||||
|
||||
## 下一步
|
||||
|
||||
恭喜!你已经掌握了协程系统的基本用法。接下来可以学习:
|
||||
|
||||
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial) - 使用协程实现状态转换
|
||||
- [资源管理最佳实践](/zh-CN/tutorials/resource-management) - 在协程中加载资源
|
||||
- [使用事件系统](/zh-CN/core/events) - 协程与事件系统集成
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [协程系统](/zh-CN/core/coroutine) - 协程系统详细说明
|
||||
- [事件系统](/zh-CN/core/events) - 事件系统详解
|
||||
- [生命周期管理](/zh-CN/core/lifecycle) - 组件生命周期
|
||||
- [System 层](/zh-CN/core/system) - System 详细说明
|
||||
785
docs/zh-CN/tutorials/data-migration.md
Normal file
785
docs/zh-CN/tutorials/data-migration.md
Normal file
@ -0,0 +1,785 @@
|
||||
---
|
||||
title: 实现数据版本迁移
|
||||
description: 学习如何实现数据版本迁移系统,处理不同版本间的数据升级
|
||||
---
|
||||
|
||||
# 实现数据版本迁移
|
||||
|
||||
## 学习目标
|
||||
|
||||
完成本教程后,你将能够:
|
||||
|
||||
- 理解数据版本迁移的重要性和应用场景
|
||||
- 定义版本化数据结构
|
||||
- 实现数据迁移接口
|
||||
- 注册和管理迁移策略
|
||||
- 处理多版本连续升级
|
||||
- 测试迁移流程的正确性
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已安装 GFramework.Game NuGet 包
|
||||
- 了解 C# 基础语法和接口实现
|
||||
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
|
||||
- 了解[数据与存档系统](/zh-CN/game/data)
|
||||
- 建议先完成[实现存档系统](/zh-CN/tutorials/save-system)教程
|
||||
|
||||
## 为什么需要数据迁移
|
||||
|
||||
在游戏开发过程中,数据结构经常会发生变化:
|
||||
|
||||
- **新增功能**:添加新的游戏系统需要新的数据字段
|
||||
- **重构优化**:改进数据结构以提升性能或可维护性
|
||||
- **修复问题**:修正早期设计的缺陷
|
||||
- **平衡调整**:调整游戏数值和配置
|
||||
|
||||
数据迁移系统能够:
|
||||
|
||||
- 自动将旧版本数据升级到新版本
|
||||
- 保证玩家存档的兼容性
|
||||
- 避免数据丢失和游戏崩溃
|
||||
- 提供平滑的版本过渡体验
|
||||
|
||||
## 步骤 1:定义版本化数据结构
|
||||
|
||||
首先,让我们定义一个支持版本控制的游戏数据结构。
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MyGame.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家数据 - 版本 1(初始版本)
|
||||
/// </summary>
|
||||
public class PlayerSaveData : IVersionedData
|
||||
{
|
||||
// IVersionedData 接口要求的属性
|
||||
public int Version { get; set; } = 1;
|
||||
public DateTime LastModified { get; set; } = DateTime.Now;
|
||||
|
||||
// 基础数据
|
||||
public string PlayerName { get; set; } = "Player";
|
||||
public int Level { get; set; } = 1;
|
||||
public int Gold { get; set; } = 0;
|
||||
|
||||
// 版本 1 的简单位置数据
|
||||
public float PositionX { get; set; }
|
||||
public float PositionY { get; set; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 实现 `IVersionedData` 接口以支持版本管理
|
||||
- `Version` 属性标识当前数据版本(从 1 开始)
|
||||
- `LastModified` 记录最后修改时间
|
||||
- 初始版本使用简单的 X、Y 坐标表示位置
|
||||
|
||||
## 步骤 2:定义新版本数据结构
|
||||
|
||||
随着游戏开发,我们需要添加新功能,数据结构也需要升级。
|
||||
|
||||
```csharp
|
||||
namespace MyGame.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家数据 - 版本 2(添加 Z 轴和经验值)
|
||||
/// </summary>
|
||||
public class PlayerSaveDataV2 : IVersionedData
|
||||
{
|
||||
public int Version { get; set; } = 2;
|
||||
public DateTime LastModified { get; set; } = DateTime.Now;
|
||||
|
||||
// 基础数据
|
||||
public string PlayerName { get; set; } = "Player";
|
||||
public int Level { get; set; } = 1;
|
||||
public int Gold { get; set; } = 0;
|
||||
|
||||
// 版本 2:添加 Z 轴支持 3D 游戏
|
||||
public float PositionX { get; set; }
|
||||
public float PositionY { get; set; }
|
||||
public float PositionZ { get; set; } // 新增
|
||||
|
||||
// 版本 2:添加经验值系统
|
||||
public int Experience { get; set; } // 新增
|
||||
public int ExperienceToNextLevel { get; set; } = 100; // 新增
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 玩家数据 - 版本 3(重构为结构化数据)
|
||||
/// </summary>
|
||||
public class PlayerSaveDataV3 : IVersionedData
|
||||
{
|
||||
public int Version { get; set; } = 3;
|
||||
public DateTime LastModified { get; set; } = DateTime.Now;
|
||||
|
||||
// 基础数据
|
||||
public string PlayerName { get; set; } = "Player";
|
||||
public int Level { get; set; } = 1;
|
||||
public int Gold { get; set; } = 0;
|
||||
|
||||
// 版本 3:使用结构化的位置数据
|
||||
public Vector3Data Position { get; set; } = new();
|
||||
|
||||
// 版本 3:使用结构化的经验值数据
|
||||
public ExperienceData Experience { get; set; } = new();
|
||||
|
||||
// 版本 3:新增技能系统
|
||||
public List<string> UnlockedSkills { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 3D 位置数据
|
||||
/// </summary>
|
||||
public class Vector3Data
|
||||
{
|
||||
public float X { get; set; }
|
||||
public float Y { get; set; }
|
||||
public float Z { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 经验值数据
|
||||
/// </summary>
|
||||
public class ExperienceData
|
||||
{
|
||||
public int Current { get; set; }
|
||||
public int ToNextLevel { get; set; } = 100;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- **版本 2**:添加 Z 轴坐标和经验值系统
|
||||
- **版本 3**:重构为更清晰的结构化数据
|
||||
- 每个版本的 `Version` 属性递增
|
||||
- 保持向后兼容,新字段提供默认值
|
||||
|
||||
## 步骤 3:实现数据迁移器
|
||||
|
||||
创建迁移器来处理版本间的数据转换。
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.setting;
|
||||
using System;
|
||||
|
||||
namespace MyGame.Data.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// 从版本 1 迁移到版本 2
|
||||
/// </summary>
|
||||
public class PlayerDataMigration_V1_to_V2 : ISettingsMigration
|
||||
{
|
||||
public Type SettingsType => typeof(PlayerSaveData);
|
||||
public int FromVersion => 1;
|
||||
public int ToVersion => 2;
|
||||
|
||||
public ISettingsSection Migrate(ISettingsSection oldData)
|
||||
{
|
||||
if (oldData is not PlayerSaveData v1)
|
||||
{
|
||||
throw new ArgumentException($"Expected PlayerSaveData, got {oldData.GetType().Name}");
|
||||
}
|
||||
|
||||
Console.WriteLine($"[迁移] 版本 1 -> 2: {v1.PlayerName}");
|
||||
|
||||
// 创建版本 2 数据
|
||||
var v2 = new PlayerSaveDataV2
|
||||
{
|
||||
Version = 2,
|
||||
LastModified = DateTime.Now,
|
||||
|
||||
// 复制现有数据
|
||||
PlayerName = v1.PlayerName,
|
||||
Level = v1.Level,
|
||||
Gold = v1.Gold,
|
||||
PositionX = v1.PositionX,
|
||||
PositionY = v1.PositionY,
|
||||
|
||||
// 新字段:Z 轴默认为 0
|
||||
PositionZ = 0f,
|
||||
|
||||
// 新字段:根据等级计算经验值
|
||||
Experience = 0,
|
||||
ExperienceToNextLevel = 100 * v1.Level
|
||||
};
|
||||
|
||||
Console.WriteLine($" - 添加 Z 轴坐标: {v2.PositionZ}");
|
||||
Console.WriteLine($" - 初始化经验值系统: {v2.Experience}/{v2.ExperienceToNextLevel}");
|
||||
|
||||
return v2;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从版本 2 迁移到版本 3
|
||||
/// </summary>
|
||||
public class PlayerDataMigration_V2_to_V3 : ISettingsMigration
|
||||
{
|
||||
public Type SettingsType => typeof(PlayerSaveDataV2);
|
||||
public int FromVersion => 2;
|
||||
public int ToVersion => 3;
|
||||
|
||||
public ISettingsSection Migrate(ISettingsSection oldData)
|
||||
{
|
||||
if (oldData is not PlayerSaveDataV2 v2)
|
||||
{
|
||||
throw new ArgumentException($"Expected PlayerSaveDataV2, got {oldData.GetType().Name}");
|
||||
}
|
||||
|
||||
Console.WriteLine($"[迁移] 版本 2 -> 3: {v2.PlayerName}");
|
||||
|
||||
// 创建版本 3 数据
|
||||
var v3 = new PlayerSaveDataV3
|
||||
{
|
||||
Version = 3,
|
||||
LastModified = DateTime.Now,
|
||||
|
||||
// 复制基础数据
|
||||
PlayerName = v2.PlayerName,
|
||||
Level = v2.Level,
|
||||
Gold = v2.Gold,
|
||||
|
||||
// 迁移位置数据到结构化格式
|
||||
Position = new Vector3Data
|
||||
{
|
||||
X = v2.PositionX,
|
||||
Y = v2.PositionY,
|
||||
Z = v2.PositionZ
|
||||
},
|
||||
|
||||
// 迁移经验值数据到结构化格式
|
||||
Experience = new ExperienceData
|
||||
{
|
||||
Current = v2.Experience,
|
||||
ToNextLevel = v2.ExperienceToNextLevel
|
||||
},
|
||||
|
||||
// 新字段:根据等级解锁基础技能
|
||||
UnlockedSkills = GenerateDefaultSkills(v2.Level)
|
||||
};
|
||||
|
||||
Console.WriteLine($" - 重构位置数据: ({v3.Position.X}, {v3.Position.Y}, {v3.Position.Z})");
|
||||
Console.WriteLine($" - 重构经验值数据: {v3.Experience.Current}/{v3.Experience.ToNextLevel}");
|
||||
Console.WriteLine($" - 初始化技能系统: {v3.UnlockedSkills.Count} 个技能");
|
||||
|
||||
return v3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据等级生成默认技能
|
||||
/// </summary>
|
||||
private List<string> GenerateDefaultSkills(int level)
|
||||
{
|
||||
var skills = new List<string> { "basic_attack" };
|
||||
|
||||
if (level >= 5)
|
||||
skills.Add("power_strike");
|
||||
|
||||
if (level >= 10)
|
||||
skills.Add("shield_block");
|
||||
|
||||
if (level >= 15)
|
||||
skills.Add("ultimate_skill");
|
||||
|
||||
return skills;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 实现 `ISettingsMigration` 接口
|
||||
- `SettingsType` 指定要迁移的数据类型
|
||||
- `FromVersion` 和 `ToVersion` 定义迁移的版本范围
|
||||
- `Migrate` 方法执行实际的数据转换
|
||||
- 为新字段提供合理的默认值或计算值
|
||||
- 添加日志输出便于调试
|
||||
|
||||
## 步骤 4:注册迁移策略
|
||||
|
||||
创建迁移管理器来注册和执行迁移。
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.setting;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MyGame.Data.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据迁移管理器
|
||||
/// </summary>
|
||||
public class DataMigrationManager
|
||||
{
|
||||
private readonly Dictionary<(Type type, int from), ISettingsMigration> _migrations = new();
|
||||
|
||||
/// <summary>
|
||||
/// 注册迁移器
|
||||
/// </summary>
|
||||
public void RegisterMigration(ISettingsMigration migration)
|
||||
{
|
||||
var key = (migration.SettingsType, migration.FromVersion);
|
||||
|
||||
if (_migrations.ContainsKey(key))
|
||||
{
|
||||
Console.WriteLine($"警告: 迁移器已存在 {migration.SettingsType.Name} " +
|
||||
$"v{migration.FromVersion}->v{migration.ToVersion}");
|
||||
return;
|
||||
}
|
||||
|
||||
_migrations[key] = migration;
|
||||
Console.WriteLine($"注册迁移器: {migration.SettingsType.Name} " +
|
||||
$"v{migration.FromVersion} -> v{migration.ToVersion}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行迁移(支持跨多个版本)
|
||||
/// </summary>
|
||||
public ISettingsSection MigrateToLatest(ISettingsSection data, int targetVersion)
|
||||
{
|
||||
if (data is not IVersionedData versioned)
|
||||
{
|
||||
Console.WriteLine("数据不支持版本控制,跳过迁移");
|
||||
return data;
|
||||
}
|
||||
|
||||
var currentVersion = versioned.Version;
|
||||
|
||||
if (currentVersion == targetVersion)
|
||||
{
|
||||
Console.WriteLine($"数据已是最新版本 v{targetVersion}");
|
||||
return data;
|
||||
}
|
||||
|
||||
if (currentVersion > targetVersion)
|
||||
{
|
||||
Console.WriteLine($"警告: 数据版本 v{currentVersion} 高于目标版本 v{targetVersion}");
|
||||
return data;
|
||||
}
|
||||
|
||||
Console.WriteLine($"\n开始迁移: v{currentVersion} -> v{targetVersion}");
|
||||
|
||||
var current = data;
|
||||
var currentVer = currentVersion;
|
||||
|
||||
// 逐步迁移到目标版本
|
||||
while (currentVer < targetVersion)
|
||||
{
|
||||
var key = (current.GetType(), currentVer);
|
||||
|
||||
if (!_migrations.TryGetValue(key, out var migration))
|
||||
{
|
||||
Console.WriteLine($"错误: 找不到迁移器 {current.GetType().Name} v{currentVer}");
|
||||
break;
|
||||
}
|
||||
|
||||
current = migration.Migrate(current);
|
||||
currentVer = migration.ToVersion;
|
||||
Console.WriteLine($"迁移完成: v{currentVersion} -> v{currentVer}\n");
|
||||
return current;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取迁移路径
|
||||
/// </summary>
|
||||
public List<string> GetMigrationPath(Type dataType, int fromVersion, int toVersion)
|
||||
{
|
||||
var path = new List<string>();
|
||||
var currentVer = fromVersion;
|
||||
var currentType = dataType;
|
||||
|
||||
while (currentVer < toVersion)
|
||||
{
|
||||
var key = (currentType, currentVer);
|
||||
|
||||
if (!_migrations.TryGetValue(key, out var migration))
|
||||
{
|
||||
path.Add($"v{currentVer} -> ? (缺失迁移器)");
|
||||
break;
|
||||
}
|
||||
|
||||
path.Add($"v{migration.FromVersion} -> v{migration.ToVersion}");
|
||||
currentVer = migration.ToVersion;
|
||||
currentType = migration.SettingsType;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 使用字典存储迁移器,键为 (类型, 源版本)
|
||||
- `RegisterMigration` 注册单个迁移器
|
||||
- `MigrateToLatest` 自动执行多步迁移
|
||||
- `GetMigrationPath` 显示迁移路径,便于调试
|
||||
- 支持跨多个版本的连续迁移
|
||||
|
||||
## 步骤 5:测试迁移流程
|
||||
|
||||
创建完整的测试程序验证迁移功能。
|
||||
|
||||
```csharp
|
||||
using MyGame.Data;
|
||||
using MyGame.Data.Migrations;
|
||||
using System;
|
||||
|
||||
namespace MyGame
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("=== 数据迁移系统测试 ===\n");
|
||||
|
||||
// 1. 创建迁移管理器
|
||||
var migrationManager = new DataMigrationManager();
|
||||
|
||||
// 2. 注册所有迁移器
|
||||
Console.WriteLine("--- 注册迁移器 ---");
|
||||
migrationManager.RegisterMigration(new PlayerDataMigration_V1_to_V2());
|
||||
migrationManager.RegisterMigration(new PlayerDataMigration_V2_to_V3());
|
||||
Console.WriteLine();
|
||||
|
||||
// 3. 测试场景 1:从版本 1 迁移到版本 3
|
||||
Console.WriteLine("--- 测试 1: V1 -> V3 迁移 ---");
|
||||
var v1Data = new PlayerSaveData
|
||||
{
|
||||
Version = 1,
|
||||
PlayerName = "老玩家",
|
||||
Level = 12,
|
||||
Gold = 5000,
|
||||
PositionX = 100.5f,
|
||||
PositionY = 200.3f
|
||||
};
|
||||
|
||||
Console.WriteLine("原始数据 (V1):");
|
||||
Console.WriteLine($" 玩家: {v1Data.PlayerName}");
|
||||
Console.WriteLine($" 等级: {v1Data.Level}");
|
||||
Console.WriteLine($" 金币: {v1Data.Gold}");
|
||||
Console.WriteLine($" 位置: ({v1Data.PositionX}, {v1Data.PositionY})");
|
||||
Console.WriteLine();
|
||||
|
||||
// 显示迁移路径
|
||||
var path = migrationManager.GetMigrationPath(typeof(PlayerSaveData), 1, 3);
|
||||
Console.WriteLine("迁移路径:");
|
||||
foreach (var step in path)
|
||||
{
|
||||
Console.WriteLine($" {step}");
|
||||
}
|
||||
Console.WriteLine();
|
||||
|
||||
// 执行迁移
|
||||
var v3Data = (PlayerSaveDataV3)migrationManager.MigrateToLatest(v1Data, 3);
|
||||
|
||||
Console.WriteLine("迁移后数据 (V3):");
|
||||
Console.WriteLine($" 玩家: {v3Data.PlayerName}");
|
||||
Console.WriteLine($" 等级: {v3Data.Level}");
|
||||
Console.WriteLine($" 金币: {v3Data.Gold}");
|
||||
Console.WriteLine($" 位置: ({v3Data.Position.X}, {v3Data.Position.Y}, {v3Data.Position.Z})");
|
||||
Console.WriteLine($" 经验值: {v3Data.Experience.Current}/{v3Data.Experience.ToNextLevel}");
|
||||
Console.WriteLine($" 技能: {string.Join(", ", v3Data.UnlockedSkills)}");
|
||||
Console.WriteLine();
|
||||
|
||||
// 4. 测试场景 2:从版本 2 迁移到版本 3
|
||||
Console.WriteLine("--- 测试 2: V2 -> V3 迁移 ---");
|
||||
var v2Data = new PlayerSaveDataV2
|
||||
{
|
||||
Version = 2,
|
||||
PlayerName = "中期玩家",
|
||||
Level = 8,
|
||||
Gold = 2000,
|
||||
PositionX = 50.0f,
|
||||
PositionY = 75.0f,
|
||||
PositionZ = 10.0f,
|
||||
Experience = 350,
|
||||
ExperienceToNextLevel = 800
|
||||
};
|
||||
|
||||
Console.WriteLine("原始数据 (V2):");
|
||||
Console.WriteLine($" 玩家: {v2Data.PlayerName}");
|
||||
Console.WriteLine($" 等级: {v2Data.Level}");
|
||||
Console.WriteLine($" 位置: ({v2Data.PositionX}, {v2Data.PositionY}, {v2Data.PositionZ})");
|
||||
Console.WriteLine($" 经验值: {v2Data.Experience}/{v2Data.ExperienceToNextLevel}");
|
||||
Console.WriteLine();
|
||||
|
||||
var v3Data2 = (PlayerSaveDataV3)migrationManager.MigrateToLatest(v2Data, 3);
|
||||
|
||||
Console.WriteLine("迁移后数据 (V3):");
|
||||
Console.WriteLine($" 玩家: {v3Data2.PlayerName}");
|
||||
Console.WriteLine($" 等级: {v3Data2.Level}");
|
||||
Console.WriteLine($" 位置: ({v3Data2.Position.X}, {v3Data2.Position.Y}, {v3Data2.Position.Z})");
|
||||
Console.WriteLine($" 经验值: {v3Data2.Experience.Current}/{v3Data2.Experience.ToNextLevel}");
|
||||
Console.WriteLine($" 技能: {string.Join(", ", v3Data2.UnlockedSkills)}");
|
||||
Console.WriteLine();
|
||||
|
||||
// 5. 测试场景 3:已是最新版本
|
||||
Console.WriteLine("--- 测试 3: 已是最新版本 ---");
|
||||
var v3DataLatest = new PlayerSaveDataV3
|
||||
{
|
||||
Version = 3,
|
||||
PlayerName = "新玩家",
|
||||
Level = 1
|
||||
};
|
||||
|
||||
migrationManager.MigrateToLatest(v3DataLatest, 3);
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("=== 测试完成 ===");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 创建不同版本的测试数据
|
||||
- 测试单步迁移(V2 -> V3)
|
||||
- 测试多步迁移(V1 -> V3)
|
||||
- 测试已是最新版本的情况
|
||||
- 显示迁移前后的数据对比
|
||||
|
||||
## 完整代码
|
||||
|
||||
所有代码文件已在上述步骤中提供。项目结构如下:
|
||||
|
||||
```
|
||||
MyGame/
|
||||
├── Data/
|
||||
│ ├── PlayerSaveData.cs # 版本 1 数据结构
|
||||
│ ├── PlayerSaveDataV2.cs # 版本 2 数据结构
|
||||
│ ├── PlayerSaveDataV3.cs # 版本 3 数据结构
|
||||
│ └── Migrations/
|
||||
│ ├── PlayerDataMigration_V1_to_V2.cs
|
||||
│ ├── PlayerDataMigration_V2_to_V3.cs
|
||||
│ └── DataMigrationManager.cs
|
||||
└── Program.cs
|
||||
```
|
||||
|
||||
## 运行结果
|
||||
|
||||
运行程序后,你将看到类似以下的输出:
|
||||
|
||||
```
|
||||
=== 数据迁移系统测试 ===
|
||||
|
||||
--- 注册迁移器 ---
|
||||
注册迁移器: PlayerSaveData v1 -> v2
|
||||
注册迁移器: PlayerSaveDataV2 v2 -> v3
|
||||
|
||||
--- 测试 1: V1 -> V3 迁移 ---
|
||||
原始数据 (V1):
|
||||
玩家: 老玩家
|
||||
等级: 12
|
||||
金币: 5000
|
||||
位置: (100.5, 200.3)
|
||||
|
||||
迁移路径:
|
||||
v1 -> v2
|
||||
v2 -> v3
|
||||
|
||||
开始迁移: v1 -> v3
|
||||
[迁移] 版本 1 -> 2: 老玩家
|
||||
- 添加 Z 轴坐标: 0
|
||||
- 初始化经验值系统: 0/1200
|
||||
[迁移] 版本 2 -> 3: 老玩家
|
||||
- 重构位置数据: (100.5, 200.3, 0)
|
||||
- 重构经验值数据: 0/1200
|
||||
- 初始化技能系统: 3 个技能
|
||||
迁移完成: v1 -> v3
|
||||
|
||||
迁移后数据 (V3):
|
||||
玩家: 老玩家
|
||||
等级: 12
|
||||
金币: 5000
|
||||
位置: (100.5, 200.3, 0)
|
||||
经验值: 0/1200
|
||||
技能: basic_attack, power_strike, shield_block
|
||||
|
||||
--- 测试 2: V2 -> V3 迁移 ---
|
||||
原始数据 (V2):
|
||||
玩家: 中期玩家
|
||||
等级: 8
|
||||
金币: 2000
|
||||
位置: (50, 75, 10)
|
||||
经验值: 350/800
|
||||
|
||||
开始迁移: v2 -> v3
|
||||
[迁移] 版本 2 -> 3: 中期玩家
|
||||
- 重构位置数据: (50, 75, 10)
|
||||
- 重构经验值数据: 350/800
|
||||
- 初始化技能系统: 2 个技能
|
||||
迁移完成: v2 -> v3
|
||||
|
||||
迁移后数据 (V3):
|
||||
玩家: 中期玩家
|
||||
等级: 8
|
||||
位置: (50, 75, 10)
|
||||
经验值: 350/800
|
||||
技能: basic_attack, power_strike
|
||||
|
||||
--- 测试 3: 已是最新版本 ---
|
||||
数据已是最新版本 v3
|
||||
|
||||
=== 测试完成 ===
|
||||
```
|
||||
|
||||
**验证步骤**:
|
||||
|
||||
1. 迁移器成功注册
|
||||
2. V1 数据正确迁移到 V3
|
||||
3. V2 数据正确迁移到 V3
|
||||
4. 新字段获得合理的默认值
|
||||
5. 已是最新版本的数据不会重复迁移
|
||||
6. 迁移路径清晰可追踪
|
||||
|
||||
## 下一步
|
||||
|
||||
恭喜!你已经实现了一个完整的数据版本迁移系统。接下来可以学习:
|
||||
|
||||
- [实现存档系统](/zh-CN/tutorials/save-system) - 结合存档系统使用迁移
|
||||
- [Godot 完整项目搭建](/zh-CN/tutorials/godot-complete-project) - 在实际项目中应用
|
||||
- [数据与存档系统](/zh-CN/game/data) - 深入了解数据系统
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 版本号管理
|
||||
|
||||
```csharp
|
||||
// 使用常量管理版本号
|
||||
public static class DataVersions
|
||||
{
|
||||
public const int PlayerData_V1 = 1;
|
||||
public const int PlayerData_V2 = 2;
|
||||
public const int PlayerData_V3 = 3;
|
||||
public const int PlayerData_Latest = PlayerData_V3;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 迁移测试
|
||||
|
||||
```csharp
|
||||
// 为每个迁移器编写单元测试
|
||||
[Test]
|
||||
public void TestMigration_V1_to_V2()
|
||||
{
|
||||
var v1 = new PlayerSaveData { Level = 10 };
|
||||
var migration = new PlayerDataMigration_V1_to_V2();
|
||||
var v2 = (PlayerSaveDataV2)migration.Migrate(v1);
|
||||
|
||||
Assert.AreEqual(10, v2.Level);
|
||||
Assert.AreEqual(0, v2.PositionZ);
|
||||
Assert.AreEqual(1000, v2.ExperienceToNextLevel);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 数据备份
|
||||
|
||||
```csharp
|
||||
// 迁移前自动备份
|
||||
public ISettingsSection MigrateWithBackup(ISettingsSection data)
|
||||
{
|
||||
// 备份原始数据
|
||||
var backup = SerializeData(data);
|
||||
SaveBackup(backup);
|
||||
|
||||
try
|
||||
{
|
||||
// 执行迁移
|
||||
var migrated = MigrateToLatest(data, targetVersion);
|
||||
return migrated;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 迁移失败,恢复备份
|
||||
RestoreBackup(backup);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 迁移日志
|
||||
|
||||
```csharp
|
||||
// 记录详细的迁移日志
|
||||
public class MigrationLogger
|
||||
{
|
||||
public void LogMigration(string playerName, int from, int to)
|
||||
{
|
||||
var log = $"[{DateTime.Now}] {playerName}: v{from} -> v{to}";
|
||||
File.AppendAllText("migration.log", log + "\n");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 向后兼容
|
||||
|
||||
- 新版本保留所有旧字段
|
||||
- 为新字段提供合理的默认值
|
||||
- 避免删除或重命名字段
|
||||
- 使用 `[Obsolete]` 标记废弃字段
|
||||
|
||||
### 6. 性能优化
|
||||
|
||||
```csharp
|
||||
// 批量迁移优化
|
||||
public async Task<List<ISettingsSection>> MigrateBatchAsync(
|
||||
List<ISettingsSection> dataList,
|
||||
int targetVersion)
|
||||
{
|
||||
var tasks = dataList.Select(data =>
|
||||
Task.Run(() => MigrateToLatest(data, targetVersion)));
|
||||
|
||||
return (await Task.WhenAll(tasks)).ToList();
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 如何处理跨多个版本的迁移?
|
||||
|
||||
迁移管理器会自动按顺序应用所有必要的迁移。例如从 V1 到 V3,会先执行 V1->V2,再执行 V2->V3。
|
||||
|
||||
### 2. 迁移失败如何处理?
|
||||
|
||||
建议在迁移前备份原始数据,迁移失败时可以恢复。同时在迁移过程中添加详细的日志记录。
|
||||
|
||||
### 3. 如何处理不兼容的数据变更?
|
||||
|
||||
对于破坏性变更,建议:
|
||||
|
||||
- 提供数据转换工具
|
||||
- 在迁移中添加数据验证
|
||||
- 通知用户可能的数据丢失
|
||||
- 提供回滚机制
|
||||
|
||||
### 4. 是否需要保留所有历史版本的数据结构?
|
||||
|
||||
建议保留,这样可以:
|
||||
|
||||
- 支持从任意旧版本迁移
|
||||
- 便于调试和测试
|
||||
- 作为文档记录数据演变
|
||||
|
||||
### 5. 如何测试迁移功能?
|
||||
|
||||
- 创建各个版本的测试数据
|
||||
- 验证迁移后的数据完整性
|
||||
- 测试迁移链的正确性
|
||||
- 使用真实的历史数据进行测试
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [数据与存档系统](/zh-CN/game/data) - 数据系统详细说明
|
||||
- [实现存档系统](/zh-CN/tutorials/save-system) - 存档系统教程
|
||||
- [架构系统](/zh-CN/core/architecture) - 架构设计原则
|
||||
822
docs/zh-CN/tutorials/functional-programming.md
Normal file
822
docs/zh-CN/tutorials/functional-programming.md
Normal file
@ -0,0 +1,822 @@
|
||||
---
|
||||
title: 函数式编程实践
|
||||
description: 学习如何在实际项目中使用 Option、Result 和管道操作等函数式编程特性
|
||||
---
|
||||
|
||||
# 函数式编程实践
|
||||
|
||||
## 学习目标
|
||||
|
||||
完成本教程后,你将能够:
|
||||
|
||||
- 理解函数式编程的核心概念和优势
|
||||
- 使用 Option 类型安全地处理可空值
|
||||
- 使用 Result 类型进行优雅的错误处理
|
||||
- 使用管道操作构建流式的数据处理流程
|
||||
- 组合多个函数式操作实现复杂的业务逻辑
|
||||
- 在实际游戏开发中应用函数式编程模式
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已安装 GFramework.Core NuGet 包
|
||||
- 了解 C# 基础语法和泛型
|
||||
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
|
||||
- 了解 Lambda 表达式和 LINQ
|
||||
|
||||
## 步骤 1:使用 Option 处理可空值
|
||||
|
||||
首先,让我们学习如何使用 Option 类型替代传统的 null 检查,使代码更加安全和优雅。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.functional;
|
||||
using GFramework.Core.functional.pipe;
|
||||
|
||||
namespace MyGame.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家数据服务
|
||||
/// </summary>
|
||||
public class PlayerDataService
|
||||
{
|
||||
private readonly Dictionary<int, PlayerData> _players = new();
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 查找玩家(返回 Option)
|
||||
/// </summary>
|
||||
public Option<PlayerData> FindPlayerById(int playerId)
|
||||
{
|
||||
// 使用 Option 包装可能不存在的值
|
||||
return _players.TryGetValue(playerId, out var player)
|
||||
? Option<PlayerData>.Some(player)
|
||||
: Option<PlayerData>.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取玩家名称(安全处理)
|
||||
/// </summary>
|
||||
public string GetPlayerName(int playerId)
|
||||
{
|
||||
// 使用 Match 模式匹配处理有值和无值的情况
|
||||
return FindPlayerById(playerId).Match(
|
||||
some: player => player.Name,
|
||||
none: () => "未知玩家"
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取玩家等级(使用默认值)
|
||||
/// </summary>
|
||||
public int GetPlayerLevel(int playerId)
|
||||
{
|
||||
// 使用 GetOrElse 提供默认值
|
||||
return FindPlayerById(playerId)
|
||||
.Map(player => player.Level)
|
||||
.GetOrElse(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找高级玩家
|
||||
/// </summary>
|
||||
public Option<PlayerData> FindAdvancedPlayer(int playerId)
|
||||
{
|
||||
// 使用 Filter 过滤值
|
||||
return FindPlayerById(playerId)
|
||||
.Filter(player => player.Level >= 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取玩家公会名称(链式调用)
|
||||
/// </summary>
|
||||
public string GetPlayerGuildName(int playerId)
|
||||
{
|
||||
// 使用 Bind 处理嵌套的 Option
|
||||
return FindPlayerById(playerId)
|
||||
.Bind(player => player.Guild) // Guild 也是 Option<Guild>
|
||||
.Map(guild => guild.Name)
|
||||
.GetOrElse("无公会");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 玩家数据
|
||||
/// </summary>
|
||||
public class PlayerData
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public int Level { get; set; }
|
||||
public Option<Guild> Guild { get; set; } = Option<Guild>.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公会数据
|
||||
/// </summary>
|
||||
public class Guild
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- `Option<T>` 明确表示值可能不存在,避免 NullReferenceException
|
||||
- `Match` 强制处理两种情况,不会遗漏 null 检查
|
||||
- `Map` 和 `Bind` 实现链式转换,代码更简洁
|
||||
- `Filter` 可以安全地过滤值
|
||||
- `GetOrElse` 提供默认值,避免空值传播
|
||||
|
||||
## 步骤 2:使用 Result 进行错误处理
|
||||
|
||||
接下来,学习如何使用 Result 类型替代异常处理,实现更可控的错误管理。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.functional;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MyGame.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 存档服务
|
||||
/// </summary>
|
||||
public class SaveService
|
||||
{
|
||||
private readonly string _saveDirectory = "./saves";
|
||||
|
||||
/// <summary>
|
||||
/// 保存游戏数据
|
||||
/// </summary>
|
||||
public Result<string> SaveGame(GameSaveData data)
|
||||
{
|
||||
// 使用 Result.Try 自动捕获异常
|
||||
return Result<string>.Try(() =>
|
||||
{
|
||||
// 验证数据
|
||||
if (string.IsNullOrEmpty(data.PlayerName))
|
||||
throw new ArgumentException("玩家名称不能为空");
|
||||
|
||||
// 创建保存目录
|
||||
if (!Directory.Exists(_saveDirectory))
|
||||
Directory.CreateDirectory(_saveDirectory);
|
||||
|
||||
// 序列化数据
|
||||
var json = JsonSerializer.Serialize(data);
|
||||
var fileName = $"save_{data.PlayerId}_{DateTime.Now:yyyyMMdd_HHmmss}.json";
|
||||
var filePath = Path.Combine(_saveDirectory, fileName);
|
||||
|
||||
// 写入文件
|
||||
File.WriteAllText(filePath, json);
|
||||
|
||||
return filePath;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载游戏数据
|
||||
/// </summary>
|
||||
public Result<GameSaveData> LoadGame(int playerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 查找最新的存档文件
|
||||
var files = Directory.GetFiles(_saveDirectory, $"save_{playerId}_*.json");
|
||||
|
||||
if (files.Length == 0)
|
||||
return Result<GameSaveData>.Failure("未找到存档文件");
|
||||
|
||||
var latestFile = files.OrderByDescending(f => f).First();
|
||||
var json = File.ReadAllText(latestFile);
|
||||
var data = JsonSerializer.Deserialize<GameSaveData>(json);
|
||||
|
||||
return data != null
|
||||
? Result<GameSaveData>.Success(data)
|
||||
: Result<GameSaveData>.Failure("存档数据解析失败");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<GameSaveData>.Failure(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存并加载游戏(链式操作)
|
||||
/// </summary>
|
||||
public Result<GameSaveData> SaveAndReload(GameSaveData data)
|
||||
{
|
||||
// 使用 Bind 链接多个 Result 操作
|
||||
return SaveGame(data)
|
||||
.Bind(_ => LoadGame(data.PlayerId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取存档信息(使用 Match)
|
||||
/// </summary>
|
||||
public string GetSaveInfo(int playerId)
|
||||
{
|
||||
return LoadGame(playerId).Match(
|
||||
succ: data => $"存档加载成功: {data.PlayerName}, 等级 {data.Level}",
|
||||
fail: ex => $"加载失败: {ex.Message}"
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安全加载游戏(提供默认值)
|
||||
/// </summary>
|
||||
public GameSaveData LoadGameOrDefault(int playerId)
|
||||
{
|
||||
return LoadGame(playerId).IfFail(new GameSaveData
|
||||
{
|
||||
PlayerId = playerId,
|
||||
PlayerName = "新玩家",
|
||||
Level = 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏存档数据
|
||||
/// </summary>
|
||||
public class GameSaveData
|
||||
{
|
||||
public int PlayerId { get; set; }
|
||||
public string PlayerName { get; set; } = "";
|
||||
public int Level { get; set; }
|
||||
public int Gold { get; set; }
|
||||
public DateTime SaveTime { get; set; } = DateTime.Now;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- `Result<T>` 将错误作为值返回,而不是抛出异常
|
||||
- `Result.Try` 自动捕获异常并转换为 Result
|
||||
- `Bind` 可以链接多个可能失败的操作
|
||||
- `Match` 强制处理成功和失败两种情况
|
||||
- `IfFail` 提供失败时的默认值
|
||||
|
||||
## 步骤 3:使用管道操作组合函数
|
||||
|
||||
学习如何使用管道操作符构建流式的数据处理流程。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.functional.pipe;
|
||||
using GFramework.Core.functional.functions;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// 物品处理系统
|
||||
/// </summary>
|
||||
public class ItemProcessingSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理物品掉落
|
||||
/// </summary>
|
||||
public ItemDrop ProcessItemDrop(Enemy enemy, Player player)
|
||||
{
|
||||
// 使用管道操作构建处理流程
|
||||
return enemy
|
||||
.Pipe(e => CalculateDropRate(e, player))
|
||||
.Tap(rate => Console.WriteLine($"掉落率: {rate:P}"))
|
||||
.Pipe(rate => GenerateItems(rate))
|
||||
.Tap(items => Console.WriteLine($"生成 {items.Count} 个物品"))
|
||||
.Pipe(items => ApplyLuckBonus(items, player))
|
||||
.Pipe(items => FilterByQuality(items))
|
||||
.Tap(items => Console.WriteLine($"过滤后剩余 {items.Count} 个物品"))
|
||||
.Let(items => new ItemDrop
|
||||
{
|
||||
Items = items,
|
||||
TotalValue = items.Sum(i => i.Value)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算掉落率
|
||||
/// </summary>
|
||||
private double CalculateDropRate(Enemy enemy, Player player)
|
||||
{
|
||||
return (enemy.Level * 0.1 + player.Luck * 0.05)
|
||||
.Pipe(rate => Math.Min(rate, 1.0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成物品
|
||||
/// </summary>
|
||||
private List<Item> GenerateItems(double dropRate)
|
||||
{
|
||||
var random = new Random();
|
||||
var itemCount = random.NextDouble() < dropRate ? random.Next(1, 5) : 0;
|
||||
|
||||
return Enumerable.Range(0, itemCount)
|
||||
.Select(_ => new Item
|
||||
{
|
||||
Id = random.Next(1000),
|
||||
Name = $"物品_{random.Next(100)}",
|
||||
Quality = (ItemQuality)random.Next(0, 4),
|
||||
Value = random.Next(10, 100)
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用幸运加成
|
||||
/// </summary>
|
||||
private List<Item> ApplyLuckBonus(List<Item> items, Player player)
|
||||
{
|
||||
return items
|
||||
.Select(item => item.Also(i =>
|
||||
{
|
||||
if (player.Luck > 50)
|
||||
i.Quality = (ItemQuality)Math.Min((int)i.Quality + 1, 3);
|
||||
}))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按品质过滤
|
||||
/// </summary>
|
||||
private List<Item> FilterByQuality(List<Item> items)
|
||||
{
|
||||
return items
|
||||
.Where(item => item.Quality >= ItemQuality.Uncommon)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 条件处理物品
|
||||
/// </summary>
|
||||
public string ProcessItemConditionally(Item item, bool isVip)
|
||||
{
|
||||
return item.PipeIf(
|
||||
predicate: i => isVip,
|
||||
ifTrue: i => $"VIP 物品: {i.Name} (价值 {i.Value * 2})",
|
||||
ifFalse: i => $"普通物品: {i.Name} (价值 {i.Value})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class Enemy
|
||||
{
|
||||
public int Level { get; set; }
|
||||
}
|
||||
|
||||
public class Player
|
||||
{
|
||||
public int Luck { get; set; }
|
||||
}
|
||||
|
||||
public class Item
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public ItemQuality Quality { get; set; }
|
||||
public int Value { get; set; }
|
||||
}
|
||||
|
||||
public enum ItemQuality
|
||||
{
|
||||
Common,
|
||||
Uncommon,
|
||||
Rare,
|
||||
Epic
|
||||
}
|
||||
|
||||
public class ItemDrop
|
||||
{
|
||||
public List<Item> Items { get; set; } = new();
|
||||
public int TotalValue { get; set; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- `Pipe` 将值传递给函数,构建流式处理链
|
||||
- `Tap` 执行副作用(如日志)但不改变值
|
||||
- `Let` 在作用域内转换值
|
||||
- `Also` 对值执行操作后返回原值
|
||||
- `PipeIf` 根据条件选择不同的处理路径
|
||||
|
||||
## 步骤 4:实现完整的数据处理流程
|
||||
|
||||
现在让我们结合 Option、Result 和管道操作,实现一个完整的游戏功能。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.functional;
|
||||
using GFramework.Core.functional.pipe;
|
||||
using GFramework.Core.functional.control;
|
||||
|
||||
namespace MyGame.Features
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务系统
|
||||
/// </summary>
|
||||
public class QuestSystem
|
||||
{
|
||||
private readonly Dictionary<int, Quest> _quests = new();
|
||||
private readonly Dictionary<int, List<int>> _playerQuests = new();
|
||||
|
||||
/// <summary>
|
||||
/// 接受任务(完整流程)
|
||||
/// </summary>
|
||||
public Result<QuestAcceptResult> AcceptQuest(int playerId, int questId)
|
||||
{
|
||||
return FindQuest(questId)
|
||||
// 转换 Option 为 Result
|
||||
.ToResult("任务不存在")
|
||||
// 验证任务等级要求
|
||||
.Bind(quest => ValidateQuestLevel(playerId, quest))
|
||||
// 检查前置任务
|
||||
.Bind(quest => CheckPrerequisites(playerId, quest))
|
||||
// 检查任务槽位
|
||||
.Bind(quest => CheckQuestSlots(playerId))
|
||||
// 添加到玩家任务列表
|
||||
.Map(quest => AddQuestToPlayer(playerId, quest))
|
||||
// 记录日志
|
||||
.Tap(result => Console.WriteLine($"玩家 {playerId} 接受任务: {result.QuestName}"))
|
||||
// 发放初始奖励
|
||||
.Map(result => GiveInitialRewards(result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找任务
|
||||
/// </summary>
|
||||
private Option<Quest> FindQuest(int questId)
|
||||
{
|
||||
return _quests.TryGetValue(questId, out var quest)
|
||||
? Option<Quest>.Some(quest)
|
||||
: Option<Quest>.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证任务等级
|
||||
/// </summary>
|
||||
private Result<Quest> ValidateQuestLevel(int playerId, Quest quest)
|
||||
{
|
||||
var playerLevel = GetPlayerLevel(playerId);
|
||||
|
||||
return playerLevel >= quest.RequiredLevel
|
||||
? Result<Quest>.Success(quest)
|
||||
: Result<Quest>.Failure($"等级不足,需要 {quest.RequiredLevel} 级");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查前置任务
|
||||
/// </summary>
|
||||
private Result<Quest> CheckPrerequisites(int playerId, Quest quest)
|
||||
{
|
||||
if (quest.PrerequisiteQuestIds.Count == 0)
|
||||
return Result<Quest>.Success(quest);
|
||||
|
||||
var completedQuests = GetCompletedQuests(playerId);
|
||||
var hasAllPrerequisites = quest.PrerequisiteQuestIds
|
||||
.All(id => completedQuests.Contains(id));
|
||||
|
||||
return hasAllPrerequisites
|
||||
? Result<Quest>.Success(quest)
|
||||
: Result<Quest>.Failure("未完成前置任务");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查任务槽位
|
||||
/// </summary>
|
||||
private Result<Quest> CheckQuestSlots(int playerId)
|
||||
{
|
||||
var activeQuests = GetActiveQuests(playerId);
|
||||
|
||||
return activeQuests.Count < 10
|
||||
? Result<Quest>.Success(default!)
|
||||
: Result<Quest>.Failure("任务栏已满");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加任务到玩家
|
||||
/// </summary>
|
||||
private QuestAcceptResult AddQuestToPlayer(int playerId, Quest quest)
|
||||
{
|
||||
if (!_playerQuests.ContainsKey(playerId))
|
||||
_playerQuests[playerId] = new List<int>();
|
||||
|
||||
_playerQuests[playerId].Add(quest.Id);
|
||||
|
||||
return new QuestAcceptResult
|
||||
{
|
||||
QuestId = quest.Id,
|
||||
QuestName = quest.Name,
|
||||
Description = quest.Description,
|
||||
Rewards = quest.Rewards
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发放初始奖励
|
||||
/// </summary>
|
||||
private QuestAcceptResult GiveInitialRewards(QuestAcceptResult result)
|
||||
{
|
||||
// 某些任务接受时就有奖励
|
||||
if (result.Rewards.Gold > 0)
|
||||
{
|
||||
Console.WriteLine($"获得金币: {result.Rewards.Gold}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 完成任务(使用函数组合)
|
||||
/// </summary>
|
||||
public Result<QuestCompleteResult> CompleteQuest(int playerId, int questId)
|
||||
{
|
||||
return FindQuest(questId)
|
||||
.ToResult("任务不存在")
|
||||
.Bind(quest => ValidateQuestOwnership(playerId, quest))
|
||||
.Bind(quest => ValidateQuestObjectives(quest))
|
||||
.Map(quest => RemoveQuestFromPlayer(playerId, quest))
|
||||
.Map(quest => CalculateRewards(quest))
|
||||
.Tap(result => Console.WriteLine($"任务完成: {result.QuestName}"))
|
||||
.Map(result => GiveRewards(playerId, result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证任务所有权
|
||||
/// </summary>
|
||||
private Result<Quest> ValidateQuestOwnership(int playerId, Quest quest)
|
||||
{
|
||||
var activeQuests = GetActiveQuests(playerId);
|
||||
|
||||
return activeQuests.Contains(quest.Id)
|
||||
? Result<Quest>.Success(quest)
|
||||
: Result<Quest>.Failure("玩家未接受此任务");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证任务目标
|
||||
/// </summary>
|
||||
private Result<Quest> ValidateQuestObjectives(Quest quest)
|
||||
{
|
||||
return quest.IsCompleted
|
||||
? Result<Quest>.Success(quest)
|
||||
: Result<Quest>.Failure("任务目标未完成");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从玩家移除任务
|
||||
/// </summary>
|
||||
private Quest RemoveQuestFromPlayer(int playerId, Quest quest)
|
||||
{
|
||||
if (_playerQuests.ContainsKey(playerId))
|
||||
{
|
||||
_playerQuests[playerId].Remove(quest.Id);
|
||||
}
|
||||
|
||||
return quest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算奖励
|
||||
/// </summary>
|
||||
private QuestCompleteResult CalculateRewards(Quest quest)
|
||||
{
|
||||
return new QuestCompleteResult
|
||||
{
|
||||
QuestId = quest.Id,
|
||||
QuestName = quest.Name,
|
||||
Rewards = quest.Rewards,
|
||||
BonusRewards = CalculateBonusRewards(quest)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算额外奖励
|
||||
/// </summary>
|
||||
private QuestRewards CalculateBonusRewards(Quest quest)
|
||||
{
|
||||
// 根据任务难度给予额外奖励
|
||||
return new QuestRewards
|
||||
{
|
||||
Gold = quest.Rewards.Gold / 10,
|
||||
Experience = quest.Rewards.Experience / 10
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发放奖励
|
||||
/// </summary>
|
||||
private QuestCompleteResult GiveRewards(int playerId, QuestCompleteResult result)
|
||||
{
|
||||
var totalGold = result.Rewards.Gold + result.BonusRewards.Gold;
|
||||
var totalExp = result.Rewards.Experience + result.BonusRewards.Experience;
|
||||
|
||||
Console.WriteLine($"获得金币: {totalGold}");
|
||||
Console.WriteLine($"获得经验: {totalExp}");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 辅助方法
|
||||
private int GetPlayerLevel(int playerId) => 10;
|
||||
private List<int> GetCompletedQuests(int playerId) => new();
|
||||
private List<int> GetActiveQuests(int playerId) =>
|
||||
_playerQuests.GetValueOrDefault(playerId, new List<int>());
|
||||
}
|
||||
|
||||
public class Quest
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public int RequiredLevel { get; set; }
|
||||
public List<int> PrerequisiteQuestIds { get; set; } = new();
|
||||
public bool IsCompleted { get; set; }
|
||||
public QuestRewards Rewards { get; set; } = new();
|
||||
}
|
||||
|
||||
public class QuestRewards
|
||||
{
|
||||
public int Gold { get; set; }
|
||||
public int Experience { get; set; }
|
||||
}
|
||||
|
||||
public class QuestAcceptResult
|
||||
{
|
||||
public int QuestId { get; set; }
|
||||
public string QuestName { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public QuestRewards Rewards { get; set; } = new();
|
||||
}
|
||||
|
||||
public class QuestCompleteResult
|
||||
{
|
||||
public int QuestId { get; set; }
|
||||
public string QuestName { get; set; } = "";
|
||||
public QuestRewards Rewards { get; set; } = new();
|
||||
public QuestRewards BonusRewards { get; set; } = new();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 使用 `Option.ToResult` 将可选值转换为结果
|
||||
- 使用 `Bind` 链接多个验证步骤
|
||||
- 使用 `Map` 转换成功的值
|
||||
- 使用 `Tap` 添加日志而不中断流程
|
||||
- 每个步骤都是纯函数,易于测试和维护
|
||||
|
||||
## 完整代码
|
||||
|
||||
### Program.cs
|
||||
|
||||
```csharp
|
||||
using MyGame.Services;
|
||||
using MyGame.Systems;
|
||||
using MyGame.Features;
|
||||
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("=== 函数式编程实践示例 ===\n");
|
||||
|
||||
// 测试 Option
|
||||
TestOptionUsage();
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
// 测试 Result
|
||||
TestResultUsage();
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
// 测试管道操作
|
||||
TestPipelineUsage();
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
// 测试完整流程
|
||||
TestCompleteWorkflow();
|
||||
|
||||
Console.WriteLine("\n=== 测试完成 ===");
|
||||
}
|
||||
|
||||
static void TestOptionUsage()
|
||||
{
|
||||
Console.WriteLine("--- 测试 Option ---");
|
||||
|
||||
var service = new PlayerDataService();
|
||||
|
||||
// 测试查找存在的玩家
|
||||
Console.WriteLine(service.GetPlayerName(1));
|
||||
|
||||
// 测试查找不存在的玩家
|
||||
Console.WriteLine(service.GetPlayerName(999));
|
||||
|
||||
// 测试获取等级
|
||||
Console.WriteLine($"玩家等级: {service.GetPlayerLevel(1)}");
|
||||
}
|
||||
|
||||
static void TestResultUsage()
|
||||
{
|
||||
Console.WriteLine("--- 测试 Result ---");
|
||||
|
||||
var saveService = new SaveService();
|
||||
|
||||
var saveData = new GameSaveData
|
||||
{
|
||||
PlayerId = 1,
|
||||
PlayerName = "测试玩家",
|
||||
Level = 10,
|
||||
Gold = 1000
|
||||
};
|
||||
|
||||
// 测试保存
|
||||
var saveResult = saveService.SaveGame(saveData);
|
||||
saveResult.Match(
|
||||
succ: path => Console.WriteLine($"保存成功: {path}"),
|
||||
fail: ex => Console.WriteLine($"保存失败: {ex.Message}")
|
||||
);
|
||||
|
||||
// 测试加载
|
||||
Console.WriteLine(saveService.GetSaveInfo(1));
|
||||
}
|
||||
|
||||
static void TestPipelineUsage()
|
||||
{
|
||||
Console.WriteLine("--- 测试管道操作 ---");
|
||||
|
||||
var itemSystem = new ItemProcessingSystem();
|
||||
|
||||
var enemy = new Enemy { Level = 5 };
|
||||
var player = new Player { Luck = 60 };
|
||||
|
||||
var drop = itemSystem.ProcessItemDrop(enemy, player);
|
||||
Console.WriteLine($"掉落总价值: {drop.TotalValue}");
|
||||
}
|
||||
|
||||
static void TestCompleteWorkflow()
|
||||
{
|
||||
Console.WriteLine("--- 测试完整工作流 ---");
|
||||
|
||||
var questSystem = new QuestSystem();
|
||||
|
||||
// 测试接受任务
|
||||
var acceptResult = questSystem.AcceptQuest(1, 101);
|
||||
acceptResult.Match(
|
||||
succ: result => Console.WriteLine($"接受任务成功: {result.QuestName}"),
|
||||
fail: ex => Console.WriteLine($"接受任务失败: {ex.Message}")
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 运行结果
|
||||
|
||||
运行程序后,你将看到类似以下的输出:
|
||||
|
||||
```
|
||||
=== 函数式编程实践示例 ===
|
||||
|
||||
--- 测试 Option ---
|
||||
未知玩家
|
||||
未知玩家
|
||||
玩家等级: 1
|
||||
|
||||
--- 测试 Result ---
|
||||
保存成功: ./saves/save_1_20260307_143022.json
|
||||
存档加载成功: 测试玩家, 等级 10
|
||||
|
||||
--- 测试管道操作 ---
|
||||
掉落率: 35.00%
|
||||
生成 3 个物品
|
||||
过滤后剩余 2 个物品
|
||||
掉落总价值: 150
|
||||
|
||||
--- 测试完整工作流 ---
|
||||
玩家 1 接受任务: 新手任务
|
||||
接受任务成功: 新手任务
|
||||
|
||||
=== 测试完成 ===
|
||||
```
|
||||
|
||||
**验证步骤**:
|
||||
|
||||
1. Option 正确处理了不存在的值
|
||||
2. Result 成功捕获和传播错误
|
||||
3. 管道操作构建了清晰的处理流程
|
||||
4. 完整工作流展示了多种技术的组合使用
|
||||
|
||||
## 下一步
|
||||
|
||||
恭喜!你已经掌握了函数式编程的核心技术。接下来可以学习:
|
||||
|
||||
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial) - 结合函数式编程和协程
|
||||
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial) - 在状态机中应用函数式模式
|
||||
- [资源管理最佳实践](/zh-CN/tutorials/resource-management) - 使用 Result 处理资源加载
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [扩展方法](/zh-CN/core/extensions) - 更多函数式扩展方法
|
||||
- [架构组件](/zh-CN/core/architecture) - 在架构中使用函数式编程
|
||||
- [最佳实践](/zh-CN/best-practices/architecture-patterns) - 函数式编程最佳实践
|
||||
813
docs/zh-CN/tutorials/godot-complete-project.md
Normal file
813
docs/zh-CN/tutorials/godot-complete-project.md
Normal file
@ -0,0 +1,813 @@
|
||||
---
|
||||
title: Godot 完整项目搭建
|
||||
description: 从零开始使用 GFramework 构建一个完整的 Godot 游戏项目
|
||||
---
|
||||
|
||||
# Godot 完整项目搭建
|
||||
|
||||
## 学习目标
|
||||
|
||||
完成本教程后,你将能够:
|
||||
|
||||
- 在 Godot 项目中集成 GFramework
|
||||
- 创建完整的游戏架构
|
||||
- 实现场景管理和 UI 系统
|
||||
- 使用协程和事件系统
|
||||
- 实现游戏存档功能
|
||||
- 构建一个可运行的完整游戏
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已安装 Godot 4.x
|
||||
- 已安装 .NET SDK 8.0+
|
||||
- 了解 C# 和 Godot 基础
|
||||
- 阅读过前面的教程:
|
||||
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial)
|
||||
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial)
|
||||
- [实现存档系统](/zh-CN/tutorials/save-system)
|
||||
|
||||
## 项目概述
|
||||
|
||||
我们将创建一个简单的 2D 射击游戏,包含以下功能:
|
||||
|
||||
- 主菜单和游戏场景
|
||||
- 玩家控制和射击
|
||||
- 敌人生成和 AI
|
||||
- 分数和生命值系统
|
||||
- 游戏存档和加载
|
||||
- 暂停菜单
|
||||
|
||||
## 步骤 1:创建 Godot 项目并配置
|
||||
|
||||
首先创建 Godot 项目并添加 GFramework 依赖。
|
||||
|
||||
### 1.1 创建项目
|
||||
|
||||
1. 打开 Godot,创建新项目 "MyShooterGame"
|
||||
2. 选择 C# 作为脚本语言
|
||||
3. 创建项目后,在项目根目录创建 `.csproj` 文件
|
||||
|
||||
### 1.2 添加 NuGet 包
|
||||
|
||||
编辑 `MyShooterGame.csproj`:
|
||||
|
||||
```xml
|
||||
<Project Sdk="Godot.NET.Sdk/4.3.0">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- GFramework 包 -->
|
||||
<PackageReference Include="GFramework.Core" Version="1.0.0" />
|
||||
<PackageReference Include="GFramework.Game" Version="1.0.0" />
|
||||
<PackageReference Include="GFramework.Godot" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
### 1.3 创建项目结构
|
||||
|
||||
```
|
||||
MyShooterGame/
|
||||
├── Scripts/
|
||||
│ ├── Architecture/
|
||||
│ │ └── GameArchitecture.cs
|
||||
│ ├── Models/
|
||||
│ │ ├── PlayerModel.cs
|
||||
│ │ └── GameModel.cs
|
||||
│ ├── Systems/
|
||||
│ │ ├── GameplaySystem.cs
|
||||
│ │ └── SpawnSystem.cs
|
||||
│ ├── Controllers/
|
||||
│ │ └── PlayerController.cs
|
||||
│ └── Data/
|
||||
│ └── GameSaveData.cs
|
||||
├── Scenes/
|
||||
│ ├── Main.tscn
|
||||
│ ├── Menu.tscn
|
||||
│ ├── Game.tscn
|
||||
│ ├── Player.tscn
|
||||
│ └── Enemy.tscn
|
||||
└── UI/
|
||||
├── MainMenu.tscn
|
||||
├── HUD.tscn
|
||||
└── PauseMenu.tscn
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 使用 Godot.NET.Sdk 4.3.0
|
||||
- 添加 GFramework 的三个核心包
|
||||
- 按功能组织代码结构
|
||||
|
||||
## 步骤 2:创建游戏架构
|
||||
|
||||
实现游戏的核心架构和数据模型。
|
||||
|
||||
### 2.1 定义数据模型
|
||||
|
||||
```csharp
|
||||
// Scripts/Models/PlayerModel.cs
|
||||
using GFramework.Core.model;
|
||||
using GFramework.Core.Abstractions.property;
|
||||
|
||||
namespace MyShooterGame.Models
|
||||
{
|
||||
public class PlayerModel : AbstractModel
|
||||
{
|
||||
public BindableProperty<int> Health { get; } = new(100);
|
||||
public BindableProperty<int> MaxHealth { get; } = new(100);
|
||||
public BindableProperty<int> Score { get; } = new(0);
|
||||
public BindableProperty<int> Lives { get; } = new(3);
|
||||
public BindableProperty<bool> IsAlive { get; } = new(true);
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 监听生命值变化
|
||||
Health.RegisterOnValueChanged(health =>
|
||||
{
|
||||
if (health <= 0)
|
||||
{
|
||||
IsAlive.Value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Health.Value = MaxHealth.Value;
|
||||
Score.Value = 0;
|
||||
Lives.Value = 3;
|
||||
IsAlive.Value = true;
|
||||
}
|
||||
|
||||
public void TakeDamage(int damage)
|
||||
{
|
||||
Health.Value = Math.Max(0, Health.Value - damage);
|
||||
}
|
||||
|
||||
public void AddScore(int points)
|
||||
{
|
||||
Score.Value += points;
|
||||
}
|
||||
|
||||
public void LoseLife()
|
||||
{
|
||||
Lives.Value = Math.Max(0, Lives.Value - 1);
|
||||
if (Lives.Value > 0)
|
||||
{
|
||||
Health.Value = MaxHealth.Value;
|
||||
IsAlive.Value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// Scripts/Models/GameModel.cs
|
||||
using GFramework.Core.model;
|
||||
using GFramework.Core.Abstractions.property;
|
||||
|
||||
namespace MyShooterGame.Models
|
||||
{
|
||||
public class GameModel : AbstractModel
|
||||
{
|
||||
public BindableProperty<bool> IsPlaying { get; } = new(false);
|
||||
public BindableProperty<bool> IsPaused { get; } = new(false);
|
||||
public BindableProperty<int> CurrentWave { get; } = new(1);
|
||||
public BindableProperty<int> EnemiesAlive { get; } = new(0);
|
||||
public BindableProperty<float> GameTime { get; } = new(0f);
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
// 初始化
|
||||
}
|
||||
|
||||
public void StartGame()
|
||||
{
|
||||
IsPlaying.Value = true;
|
||||
IsPaused.Value = false;
|
||||
CurrentWave.Value = 1;
|
||||
EnemiesAlive.Value = 0;
|
||||
GameTime.Value = 0f;
|
||||
}
|
||||
|
||||
public void PauseGame()
|
||||
{
|
||||
IsPaused.Value = true;
|
||||
}
|
||||
|
||||
public void ResumeGame()
|
||||
{
|
||||
IsPaused.Value = false;
|
||||
}
|
||||
|
||||
public void EndGame()
|
||||
{
|
||||
IsPlaying.Value = false;
|
||||
IsPaused.Value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 定义存档数据
|
||||
|
||||
```csharp
|
||||
// Scripts/Data/GameSaveData.cs
|
||||
using GFramework.Game.Abstractions.data;
|
||||
using System;
|
||||
|
||||
namespace MyShooterGame.Data
|
||||
{
|
||||
public class GameSaveData : IVersionedData
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public DateTime SaveTime { get; set; }
|
||||
|
||||
// 玩家数据
|
||||
public int HighScore { get; set; }
|
||||
public int TotalKills { get; set; }
|
||||
public float TotalPlayTime { get; set; }
|
||||
|
||||
// 设置
|
||||
public float MusicVolume { get; set; } = 0.8f;
|
||||
public float SfxVolume { get; set; } = 1.0f;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 创建游戏架构
|
||||
|
||||
```csharp
|
||||
// Scripts/Architecture/GameArchitecture.cs
|
||||
using GFramework.Godot.architecture;
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Game.Abstractions.data;
|
||||
using GFramework.Game.Abstractions.storage;
|
||||
using GFramework.Game.data;
|
||||
using GFramework.Game.storage;
|
||||
using MyShooterGame.Models;
|
||||
using MyShooterGame.Systems;
|
||||
using MyShooterGame.Data;
|
||||
using Godot;
|
||||
|
||||
namespace MyShooterGame.Architecture
|
||||
{
|
||||
public class GameArchitecture : AbstractArchitecture
|
||||
{
|
||||
public static GameArchitecture Interface { get; private set; }
|
||||
|
||||
public GameArchitecture()
|
||||
{
|
||||
Interface = this;
|
||||
}
|
||||
|
||||
protected override void InstallModules()
|
||||
{
|
||||
GD.Print("=== 初始化游戏架构 ===");
|
||||
|
||||
// 注册存储系统
|
||||
var storage = new FileStorage("user://saves");
|
||||
RegisterUtility<IFileStorage>(storage);
|
||||
|
||||
// 注册存档仓库
|
||||
var saveConfig = new SaveConfiguration
|
||||
{
|
||||
SaveRoot = "",
|
||||
SaveSlotPrefix = "save_",
|
||||
SaveFileName = "data.json"
|
||||
};
|
||||
var saveRepo = new SaveRepository<GameSaveData>(storage, saveConfig);
|
||||
RegisterUtility<ISaveRepository<GameSaveData>>(saveRepo);
|
||||
|
||||
// 注册 Model
|
||||
RegisterModel(new PlayerModel());
|
||||
RegisterModel(new GameModel());
|
||||
|
||||
// 注册 System
|
||||
RegisterSystem(new GameplaySystem());
|
||||
RegisterSystem(new SpawnSystem());
|
||||
|
||||
GD.Print("游戏架构初始化完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- `PlayerModel` 管理玩家状态
|
||||
- `GameModel` 管理游戏状态
|
||||
- `GameSaveData` 定义存档结构
|
||||
- `GameArchitecture` 注册所有组件
|
||||
|
||||
## 步骤 3:实现游戏系统
|
||||
|
||||
创建游戏逻辑系统。
|
||||
|
||||
### 3.1 游戏逻辑系统
|
||||
|
||||
```csharp
|
||||
// Scripts/Systems/GameplaySystem.cs
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Core.extensions;
|
||||
using MyShooterGame.Models;
|
||||
using Godot;
|
||||
|
||||
namespace MyShooterGame.Systems
|
||||
{
|
||||
public class GameplaySystem : AbstractSystem
|
||||
{
|
||||
public void StartNewGame()
|
||||
{
|
||||
GD.Print("开始新游戏");
|
||||
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
|
||||
// 重置数据
|
||||
playerModel.Reset();
|
||||
gameModel.StartGame();
|
||||
}
|
||||
|
||||
public void GameOver()
|
||||
{
|
||||
GD.Print("游戏结束");
|
||||
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
gameModel.EndGame();
|
||||
|
||||
// 保存最高分
|
||||
SaveHighScore();
|
||||
}
|
||||
|
||||
public void PauseGame()
|
||||
{
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
gameModel.PauseGame();
|
||||
GetTree().Paused = true;
|
||||
}
|
||||
|
||||
public void ResumeGame()
|
||||
{
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
gameModel.ResumeGame();
|
||||
GetTree().Paused = false;
|
||||
}
|
||||
|
||||
private void SaveHighScore()
|
||||
{
|
||||
// 实现最高分保存逻辑
|
||||
}
|
||||
|
||||
private SceneTree GetTree()
|
||||
{
|
||||
return (SceneTree)Engine.GetMainLoop();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 敌人生成系统
|
||||
|
||||
```csharp
|
||||
// Scripts/Systems/SpawnSystem.cs
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Core.extensions;
|
||||
using GFramework.Core.Abstractions.coroutine;
|
||||
using GFramework.Core.coroutine;
|
||||
using MyShooterGame.Models;
|
||||
using Godot;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MyShooterGame.Systems
|
||||
{
|
||||
public class SpawnSystem : AbstractSystem
|
||||
{
|
||||
private PackedScene _enemyScene;
|
||||
private Node2D _spawnRoot;
|
||||
private CoroutineHandle? _spawnCoroutine;
|
||||
|
||||
public void Initialize(Node2D spawnRoot, PackedScene enemyScene)
|
||||
{
|
||||
_spawnRoot = spawnRoot;
|
||||
_enemyScene = enemyScene;
|
||||
}
|
||||
|
||||
public void StartSpawning()
|
||||
{
|
||||
if (_spawnCoroutine.HasValue)
|
||||
{
|
||||
this.StopCoroutine(_spawnCoroutine.Value);
|
||||
}
|
||||
|
||||
_spawnCoroutine = this.StartCoroutine(SpawnEnemiesCoroutine());
|
||||
}
|
||||
|
||||
public void StopSpawning()
|
||||
{
|
||||
if (_spawnCoroutine.HasValue)
|
||||
{
|
||||
this.StopCoroutine(_spawnCoroutine.Value);
|
||||
_spawnCoroutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator<IYieldInstruction> SpawnEnemiesCoroutine()
|
||||
{
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
|
||||
while (gameModel.IsPlaying.Value)
|
||||
{
|
||||
// 等待 2 秒
|
||||
yield return CoroutineHelper.WaitForSeconds(2.0);
|
||||
|
||||
// 生成敌人
|
||||
if (!gameModel.IsPaused.Value)
|
||||
{
|
||||
SpawnEnemy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnEnemy()
|
||||
{
|
||||
if (_enemyScene == null || _spawnRoot == null)
|
||||
return;
|
||||
|
||||
var enemy = _enemyScene.Instantiate<Node2D>();
|
||||
_spawnRoot.AddChild(enemy);
|
||||
|
||||
// 随机位置
|
||||
var random = new Random();
|
||||
enemy.Position = new Vector2(
|
||||
random.Next(100, 900),
|
||||
-50
|
||||
);
|
||||
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
gameModel.EnemiesAlive.Value++;
|
||||
|
||||
GD.Print($"生成敌人,当前数量: {gameModel.EnemiesAlive.Value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- `GameplaySystem` 管理游戏流程
|
||||
- `SpawnSystem` 使用协程定时生成敌人
|
||||
- 系统之间通过 Model 共享数据
|
||||
|
||||
## 步骤 4:创建玩家控制器
|
||||
|
||||
实现玩家的移动和射击。
|
||||
|
||||
```csharp
|
||||
// Scripts/Controllers/PlayerController.cs
|
||||
using GFramework.Core.Abstractions.controller;
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Core.extensions;
|
||||
using MyShooterGame.Architecture;
|
||||
using MyShooterGame.Models;
|
||||
using Godot;
|
||||
|
||||
namespace MyShooterGame.Controllers
|
||||
{
|
||||
public partial class PlayerController : CharacterBody2D, IController
|
||||
{
|
||||
[Export] public float Speed = 300f;
|
||||
[Export] public PackedScene BulletScene;
|
||||
|
||||
private float _shootCooldown = 0f;
|
||||
private const float ShootInterval = 0.2f;
|
||||
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 监听玩家死亡
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
playerModel.IsAlive.RegisterOnValueChanged(isAlive =>
|
||||
{
|
||||
if (!isAlive)
|
||||
{
|
||||
OnPlayerDied();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
_shootCooldown -= (float)delta;
|
||||
|
||||
// 射击
|
||||
if (Input.IsActionPressed("shoot") && _shootCooldown <= 0)
|
||||
{
|
||||
Shoot();
|
||||
_shootCooldown = ShootInterval;
|
||||
}
|
||||
}
|
||||
|
||||
public override void _PhysicsProcess(double delta)
|
||||
{
|
||||
// 移动
|
||||
var velocity = Vector2.Zero;
|
||||
|
||||
if (Input.IsActionPressed("move_left"))
|
||||
velocity.X -= 1;
|
||||
if (Input.IsActionPressed("move_right"))
|
||||
velocity.X += 1;
|
||||
if (Input.IsActionPressed("move_up"))
|
||||
velocity.Y -= 1;
|
||||
if (Input.IsActionPressed("move_down"))
|
||||
velocity.Y += 1;
|
||||
|
||||
Velocity = velocity.Normalized() * Speed;
|
||||
MoveAndSlide();
|
||||
|
||||
// 限制在屏幕内
|
||||
var screenSize = GetViewportRect().Size;
|
||||
Position = new Vector2(
|
||||
Mathf.Clamp(Position.X, 0, screenSize.X),
|
||||
Mathf.Clamp(Position.Y, 0, screenSize.Y)
|
||||
);
|
||||
}
|
||||
|
||||
private void Shoot()
|
||||
{
|
||||
if (BulletScene == null)
|
||||
return;
|
||||
|
||||
var bullet = BulletScene.Instantiate<Node2D>();
|
||||
GetParent().AddChild(bullet);
|
||||
bullet.GlobalPosition = GlobalPosition + new Vector2(0, -20);
|
||||
|
||||
GD.Print("发射子弹");
|
||||
}
|
||||
|
||||
public void TakeDamage(int damage)
|
||||
{
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
playerModel.TakeDamage(damage);
|
||||
|
||||
GD.Print($"玩家受伤,剩余生命: {playerModel.Health.Value}");
|
||||
}
|
||||
|
||||
private void OnPlayerDied()
|
||||
{
|
||||
GD.Print("玩家死亡");
|
||||
|
||||
var playerModel = this.GetModel<PlayerModel>();
|
||||
playerModel.LoseLife();
|
||||
|
||||
if (playerModel.Lives.Value > 0)
|
||||
{
|
||||
// 重生
|
||||
Position = new Vector2(400, 500);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 游戏结束
|
||||
var gameplaySystem = this.GetSystem<GameplaySystem>();
|
||||
gameplaySystem.GameOver();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 实现 `IController` 接口访问架构
|
||||
- 使用 Godot 的输入系统
|
||||
- 通过 Model 更新游戏状态
|
||||
- 监听属性变化响应事件
|
||||
|
||||
## 步骤 5:创建游戏场景
|
||||
|
||||
### 5.1 主场景 (Main.tscn)
|
||||
|
||||
创建主场景并添加架构初始化脚本:
|
||||
|
||||
```csharp
|
||||
// Scripts/Main.cs
|
||||
using Godot;
|
||||
using MyShooterGame.Architecture;
|
||||
|
||||
public partial class Main : Node
|
||||
{
|
||||
private GameArchitecture _architecture;
|
||||
|
||||
public override async void _Ready()
|
||||
{
|
||||
GD.Print("初始化游戏");
|
||||
|
||||
// 创建并初始化架构
|
||||
_architecture = new GameArchitecture();
|
||||
await _architecture.InitializeAsync();
|
||||
|
||||
GD.Print("架构初始化完成,切换到菜单");
|
||||
|
||||
// 加载菜单场景
|
||||
GetTree().ChangeSceneToFile("res://Scenes/Menu.tscn");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 菜单场景 (Menu.tscn)
|
||||
|
||||
创建菜单UI并添加控制脚本:
|
||||
|
||||
```csharp
|
||||
// Scripts/UI/MenuController.cs
|
||||
using Godot;
|
||||
using GFramework.Core.Abstractions.controller;
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Core.extensions;
|
||||
using MyShooterGame.Architecture;
|
||||
using MyShooterGame.Systems;
|
||||
|
||||
public partial class MenuController : Control, IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 连接按钮信号
|
||||
GetNode<Button>("VBoxContainer/StartButton").Pressed += OnStartPressed;
|
||||
GetNode<Button>("VBoxContainer/QuitButton").Pressed += OnQuitPressed;
|
||||
}
|
||||
|
||||
private void OnStartPressed()
|
||||
{
|
||||
GD.Print("开始游戏");
|
||||
|
||||
// 初始化游戏
|
||||
var gameplaySystem = this.GetSystem<GameplaySystem>();
|
||||
gameplaySystem.StartNewGame();
|
||||
|
||||
// 切换到游戏场景
|
||||
GetTree().ChangeSceneToFile("res://Scenes/Game.tscn");
|
||||
}
|
||||
|
||||
private void OnQuitPressed()
|
||||
{
|
||||
GetTree().Quit();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 游戏场景 (Game.tscn)
|
||||
|
||||
创建游戏场景并添加控制脚本:
|
||||
|
||||
```csharp
|
||||
// Scripts/GameScene.cs
|
||||
using Godot;
|
||||
using GFramework.Core.Abstractions.controller;
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Core.extensions;
|
||||
using MyShooterGame.Architecture;
|
||||
using MyShooterGame.Systems;
|
||||
using MyShooterGame.Models;
|
||||
|
||||
public partial class GameScene : Node2D, IController
|
||||
{
|
||||
[Export] public PackedScene EnemyScene;
|
||||
|
||||
private SpawnSystem _spawnSystem;
|
||||
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
// 初始化生成系统
|
||||
_spawnSystem = this.GetSystem<SpawnSystem>();
|
||||
_spawnSystem.Initialize(this, EnemyScene);
|
||||
_spawnSystem.StartSpawning();
|
||||
|
||||
// 监听游戏状态
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
gameModel.IsPlaying.RegisterOnValueChanged(isPlaying =>
|
||||
{
|
||||
if (!isPlaying)
|
||||
{
|
||||
OnGameOver();
|
||||
}
|
||||
});
|
||||
|
||||
GD.Print("游戏场景已加载");
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
// 更新游戏时间
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
if (gameModel.IsPlaying.Value && !gameModel.IsPaused.Value)
|
||||
{
|
||||
gameModel.GameTime.Value += (float)delta;
|
||||
}
|
||||
|
||||
// 暂停
|
||||
if (Input.IsActionJustPressed("ui_cancel"))
|
||||
{
|
||||
TogglePause();
|
||||
}
|
||||
}
|
||||
|
||||
private void TogglePause()
|
||||
{
|
||||
var gameplaySystem = this.GetSystem<GameplaySystem>();
|
||||
var gameModel = this.GetModel<GameModel>();
|
||||
|
||||
if (gameModel.IsPaused.Value)
|
||||
{
|
||||
gameplaySystem.ResumeGame();
|
||||
}
|
||||
else
|
||||
{
|
||||
gameplaySystem.PauseGame();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGameOver()
|
||||
{
|
||||
GD.Print("游戏结束,返回菜单");
|
||||
_spawnSystem.StopSpawning();
|
||||
|
||||
// 延迟返回菜单
|
||||
GetTree().CreateTimer(2.0).Timeout += () =>
|
||||
{
|
||||
GetTree().ChangeSceneToFile("res://Scenes/Menu.tscn");
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- `Main` 初始化架构
|
||||
- `MenuController` 处理菜单交互
|
||||
- `GameScene` 管理游戏场景
|
||||
- 所有脚本通过架构访问系统和模型
|
||||
|
||||
## 完整代码
|
||||
|
||||
项目结构和所有代码文件已在上述步骤中提供。
|
||||
|
||||
## 运行结果
|
||||
|
||||
运行游戏后,你将看到:
|
||||
|
||||
1. **启动**:
|
||||
- 架构初始化
|
||||
- 自动进入主菜单
|
||||
|
||||
2. **主菜单**:
|
||||
- 显示开始和退出按钮
|
||||
- 点击开始进入游戏
|
||||
|
||||
3. **游戏场景**:
|
||||
- 玩家可以移动和射击
|
||||
- 敌人定时生成
|
||||
- HUD 显示分数和生命值
|
||||
- 按 ESC 暂停游戏
|
||||
|
||||
4. **游戏结束**:
|
||||
- 玩家生命值为 0 时游戏结束
|
||||
- 显示最终分数
|
||||
- 自动返回主菜单
|
||||
|
||||
**验证步骤**:
|
||||
|
||||
1. 架构正确初始化
|
||||
2. 场景切换正常
|
||||
3. 玩家控制响应
|
||||
4. 敌人生成系统工作
|
||||
5. 数据模型正确更新
|
||||
6. 暂停功能正常
|
||||
|
||||
## 下一步
|
||||
|
||||
恭喜!你已经完成了一个基础的 Godot 游戏项目。接下来可以:
|
||||
|
||||
- 添加更多游戏功能(道具、关卡等)
|
||||
- 实现完整的 UI 系统
|
||||
- 添加音效和音乐
|
||||
- 优化性能和体验
|
||||
- 发布到不同平台
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [Godot 架构集成](/zh-CN/godot/architecture) - 架构详细说明
|
||||
- [Godot 场景系统](/zh-CN/godot/scene) - 场景管理
|
||||
- [Godot UI 系统](/zh-CN/godot/ui) - UI 管理
|
||||
- [Godot 扩展](/zh-CN/godot/extensions) - 扩展功能
|
||||
@ -71,6 +71,50 @@
|
||||
|
||||
---
|
||||
|
||||
### 系统实现教程
|
||||
|
||||
#### [使用协程系统](./coroutine-tutorial.md)
|
||||
|
||||
> 学习如何使用协程系统实现异步操作和时间控制。
|
||||
|
||||
**学习内容**:创建协程、等待指令、事件等待、协程组合
|
||||
|
||||
**预计时间**:1-2 小时
|
||||
|
||||
#### [实现状态机](./state-machine-tutorial.md)
|
||||
|
||||
> 学习如何使用状态机系统管理游戏状态和场景切换。
|
||||
|
||||
**学习内容**:定义状态、状态转换、异步状态、状态历史
|
||||
|
||||
**预计时间**:1-2 小时
|
||||
|
||||
#### [实现暂停系统](./pause-system.md)
|
||||
|
||||
> 学习如何使用暂停系统实现多层暂停管理和游戏流程控制。
|
||||
|
||||
**学习内容**:基本暂停、分组暂停、暂停栈、自定义处理器
|
||||
|
||||
**预计时间**:1-2 小时
|
||||
|
||||
#### [资源管理最佳实践](./resource-management.md)
|
||||
|
||||
> 学习如何高效管理游戏资源的加载、缓存和释放。
|
||||
|
||||
**学习内容**:资源加载、缓存策略、释放策略、内存优化
|
||||
|
||||
**预计时间**:1-2 小时
|
||||
|
||||
#### [实现存档系统](./save-system.md)
|
||||
|
||||
> 学习如何实现完整的游戏存档和读档系统。
|
||||
|
||||
**学习内容**:数据序列化、存档管理、版本控制、加密保护
|
||||
|
||||
**预计时间**:1-2 小时
|
||||
|
||||
---
|
||||
|
||||
## 🎯 学习路径建议
|
||||
|
||||
### 路径一:快速上手(推荐新手)
|
||||
|
||||
1641
docs/zh-CN/tutorials/large-project-organization.md
Normal file
1641
docs/zh-CN/tutorials/large-project-organization.md
Normal file
File diff suppressed because it is too large
Load Diff
1135
docs/zh-CN/tutorials/pause-system.md
Normal file
1135
docs/zh-CN/tutorials/pause-system.md
Normal file
File diff suppressed because it is too large
Load Diff
814
docs/zh-CN/tutorials/resource-management.md
Normal file
814
docs/zh-CN/tutorials/resource-management.md
Normal file
@ -0,0 +1,814 @@
|
||||
---
|
||||
title: 资源管理最佳实践
|
||||
description: 学习如何高效管理游戏资源,避免内存泄漏和性能问题
|
||||
---
|
||||
|
||||
# 资源管理最佳实践
|
||||
|
||||
## 学习目标
|
||||
|
||||
完成本教程后,你将能够:
|
||||
|
||||
- 理解资源管理的核心概念和重要性
|
||||
- 实现自定义资源加载器
|
||||
- 使用资源句柄管理资源生命周期
|
||||
- 实现资源预加载和延迟加载
|
||||
- 选择合适的资源释放策略
|
||||
- 避免常见的资源管理陷阱
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已安装 GFramework.Core NuGet 包
|
||||
- 了解 C# 基础语法和 async/await
|
||||
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
|
||||
- 了解[协程系统](/zh-CN/core/coroutine)
|
||||
|
||||
## 步骤 1:创建资源类型和加载器
|
||||
|
||||
首先,让我们定义游戏中常用的资源类型,并为它们实现加载器。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.resource;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MyGame.Resources
|
||||
{
|
||||
// ===== 资源类型定义 =====
|
||||
|
||||
/// <summary>
|
||||
/// 纹理资源
|
||||
/// </summary>
|
||||
public class Texture : IDisposable
|
||||
{
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public int Width { get; set; }
|
||||
public int Height { get; set; }
|
||||
public byte[]? Data { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Data = null;
|
||||
Console.WriteLine($"纹理已释放: {Path}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 音频资源
|
||||
/// </summary>
|
||||
public class AudioClip : IDisposable
|
||||
{
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public double Duration { get; set; }
|
||||
public byte[]? Data { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Data = null;
|
||||
Console.WriteLine($"音频已释放: {Path}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置文件资源
|
||||
/// </summary>
|
||||
public class ConfigData
|
||||
{
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public Dictionary<string, string> Data { get; set; } = new();
|
||||
}
|
||||
|
||||
// ===== 资源加载器实现 =====
|
||||
|
||||
/// <summary>
|
||||
/// 纹理加载器
|
||||
/// </summary>
|
||||
public class TextureLoader : IResourceLoader<Texture>
|
||||
{
|
||||
public Texture Load(string path)
|
||||
{
|
||||
Console.WriteLine($"同步加载纹理: {path}");
|
||||
|
||||
// 模拟加载纹理
|
||||
Thread.Sleep(100); // 模拟 I/O 延迟
|
||||
|
||||
return new Texture
|
||||
{
|
||||
Path = path,
|
||||
Width = 512,
|
||||
Height = 512,
|
||||
Data = new byte[512 * 512 * 4] // RGBA
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<Texture> LoadAsync(string path)
|
||||
{
|
||||
Console.WriteLine($"异步加载纹理: {path}");
|
||||
|
||||
// 模拟异步加载
|
||||
await Task.Delay(100);
|
||||
|
||||
return new Texture
|
||||
{
|
||||
Path = path,
|
||||
Width = 512,
|
||||
Height = 512,
|
||||
Data = new byte[512 * 512 * 4]
|
||||
};
|
||||
}
|
||||
|
||||
public void Unload(Texture resource)
|
||||
{
|
||||
resource?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 音频加载器
|
||||
/// </summary>
|
||||
public class AudioLoader : IResourceLoader<AudioClip>
|
||||
{
|
||||
public AudioClip Load(string path)
|
||||
{
|
||||
Console.WriteLine($"同步加载音频: {path}");
|
||||
Thread.Sleep(150);
|
||||
|
||||
return new AudioClip
|
||||
{
|
||||
Path = path,
|
||||
Duration = 30.0,
|
||||
Data = new byte[1024 * 1024] // 1MB
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<AudioClip> LoadAsync(string path)
|
||||
{
|
||||
Console.WriteLine($"异步加载音频: {path}");
|
||||
await Task.Delay(150);
|
||||
|
||||
return new AudioClip
|
||||
{
|
||||
Path = path,
|
||||
Duration = 30.0,
|
||||
Data = new byte[1024 * 1024]
|
||||
};
|
||||
}
|
||||
|
||||
public void Unload(AudioClip resource)
|
||||
{
|
||||
resource?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置文件加载器
|
||||
/// </summary>
|
||||
public class ConfigLoader : IResourceLoader<ConfigData>
|
||||
{
|
||||
public ConfigData Load(string path)
|
||||
{
|
||||
Console.WriteLine($"加载配置: {path}");
|
||||
|
||||
// 模拟解析配置文件
|
||||
return new ConfigData
|
||||
{
|
||||
Path = path,
|
||||
Data = new Dictionary<string, string>
|
||||
{
|
||||
["version"] = "1.0",
|
||||
["difficulty"] = "normal"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ConfigData> LoadAsync(string path)
|
||||
{
|
||||
await Task.Delay(50);
|
||||
return Load(path);
|
||||
}
|
||||
|
||||
public void Unload(ConfigData resource)
|
||||
{
|
||||
resource.Data.Clear();
|
||||
Console.WriteLine($"配置已释放: {resource.Path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 定义了三种常见资源类型:纹理、音频、配置
|
||||
- 实现 `IResourceLoader<T>` 接口提供加载逻辑
|
||||
- 同步和异步加载方法分别处理不同场景
|
||||
- `Unload` 方法负责资源清理
|
||||
|
||||
## 步骤 2:注册资源管理器
|
||||
|
||||
在架构中注册资源管理器和所有加载器。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.architecture;
|
||||
using GFramework.Core.Abstractions.resource;
|
||||
using GFramework.Core.resource;
|
||||
using MyGame.Resources;
|
||||
|
||||
namespace MyGame
|
||||
{
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
public static IArchitecture Interface { get; private set; }
|
||||
|
||||
protected override void Init()
|
||||
{
|
||||
Interface = this;
|
||||
|
||||
// 创建资源管理器
|
||||
var resourceManager = new ResourceManager();
|
||||
|
||||
// 注册资源加载器
|
||||
resourceManager.RegisterLoader(new TextureLoader());
|
||||
resourceManager.RegisterLoader(new AudioLoader());
|
||||
resourceManager.RegisterLoader(new ConfigLoader());
|
||||
|
||||
// 设置释放策略(默认手动释放)
|
||||
resourceManager.SetReleaseStrategy(new ManualReleaseStrategy());
|
||||
|
||||
// 注册到架构
|
||||
RegisterUtility<IResourceManager>(resourceManager);
|
||||
|
||||
Console.WriteLine("资源管理器初始化完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 创建 `ResourceManager` 实例
|
||||
- 为每种资源类型注册对应的加载器
|
||||
- 设置资源释放策略
|
||||
- 将资源管理器注册为 Utility
|
||||
|
||||
## 步骤 3:实现资源预加载系统
|
||||
|
||||
创建一个系统来管理资源的预加载和卸载。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Core.Abstractions.resource;
|
||||
using GFramework.Core.extensions;
|
||||
using MyGame.Resources;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// 资源预加载系统
|
||||
/// </summary>
|
||||
public class ResourcePreloadSystem : AbstractSystem
|
||||
{
|
||||
// 场景资源配置
|
||||
private readonly Dictionary<string, List<string>> _sceneResources = new()
|
||||
{
|
||||
["Menu"] = new List<string>
|
||||
{
|
||||
"textures/menu_bg.png",
|
||||
"textures/button.png",
|
||||
"audio/menu_bgm.mp3"
|
||||
},
|
||||
["Gameplay"] = new List<string>
|
||||
{
|
||||
"textures/player.png",
|
||||
"textures/enemy.png",
|
||||
"textures/bullet.png",
|
||||
"audio/game_bgm.mp3",
|
||||
"audio/shoot.mp3",
|
||||
"config/level_1.cfg"
|
||||
},
|
||||
["GameOver"] = new List<string>
|
||||
{
|
||||
"textures/gameover_bg.png",
|
||||
"audio/gameover.mp3"
|
||||
}
|
||||
};
|
||||
|
||||
// 当前场景的资源句柄
|
||||
private readonly List<IDisposable> _currentHandles = new();
|
||||
|
||||
/// <summary>
|
||||
/// 预加载场景资源
|
||||
/// </summary>
|
||||
public async Task PreloadSceneAsync(string sceneName)
|
||||
{
|
||||
if (!_sceneResources.TryGetValue(sceneName, out var resources))
|
||||
{
|
||||
Console.WriteLine($"场景 {sceneName} 没有配置资源");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"\n=== 开始预加载场景: {sceneName} ===");
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
// 并行加载所有资源
|
||||
var tasks = new List<Task>();
|
||||
|
||||
foreach (var path in resources)
|
||||
{
|
||||
if (path.EndsWith(".png"))
|
||||
{
|
||||
tasks.Add(resourceManager.PreloadAsync<Texture>(path));
|
||||
}
|
||||
else if (path.EndsWith(".mp3"))
|
||||
{
|
||||
tasks.Add(resourceManager.PreloadAsync<AudioClip>(path));
|
||||
}
|
||||
else if (path.EndsWith(".cfg"))
|
||||
{
|
||||
tasks.Add(resourceManager.PreloadAsync<ConfigData>(path));
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
Console.WriteLine($"场景 {sceneName} 资源预加载完成\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预加载场景资源(带进度)
|
||||
/// </summary>
|
||||
public async Task PreloadSceneWithProgressAsync(
|
||||
string sceneName,
|
||||
Action<float> onProgress)
|
||||
{
|
||||
if (!_sceneResources.TryGetValue(sceneName, out var resources))
|
||||
return;
|
||||
|
||||
Console.WriteLine($"\n=== 开始预加载场景: {sceneName} ===");
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
int totalCount = resources.Count;
|
||||
int loadedCount = 0;
|
||||
|
||||
foreach (var path in resources)
|
||||
{
|
||||
// 根据扩展名加载不同类型的资源
|
||||
if (path.EndsWith(".png"))
|
||||
{
|
||||
await resourceManager.PreloadAsync<Texture>(path);
|
||||
}
|
||||
else if (path.EndsWith(".mp3"))
|
||||
{
|
||||
await resourceManager.PreloadAsync<AudioClip>(path);
|
||||
}
|
||||
else if (path.EndsWith(".cfg"))
|
||||
{
|
||||
await resourceManager.PreloadAsync<ConfigData>(path);
|
||||
}
|
||||
|
||||
loadedCount++;
|
||||
float progress = (float)loadedCount / totalCount;
|
||||
onProgress?.Invoke(progress);
|
||||
|
||||
Console.WriteLine($"加载进度: {progress * 100:F0}% ({loadedCount}/{totalCount})");
|
||||
}
|
||||
|
||||
Console.WriteLine($"场景 {sceneName} 资源预加载完成\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取场景资源句柄
|
||||
/// </summary>
|
||||
public void AcquireSceneResources(string sceneName)
|
||||
{
|
||||
if (!_sceneResources.TryGetValue(sceneName, out var resources))
|
||||
return;
|
||||
|
||||
Console.WriteLine($"获取场景 {sceneName} 的资源句柄");
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
// 清理旧句柄
|
||||
ReleaseCurrentResources();
|
||||
|
||||
// 获取新句柄
|
||||
foreach (var path in resources)
|
||||
{
|
||||
IDisposable? handle = null;
|
||||
|
||||
if (path.EndsWith(".png"))
|
||||
{
|
||||
handle = resourceManager.GetHandle<Texture>(path);
|
||||
}
|
||||
else if (path.EndsWith(".mp3"))
|
||||
{
|
||||
handle = resourceManager.GetHandle<AudioClip>(path);
|
||||
}
|
||||
else if (path.EndsWith(".cfg"))
|
||||
{
|
||||
handle = resourceManager.GetHandle<ConfigData>(path);
|
||||
}
|
||||
|
||||
if (handle != null)
|
||||
{
|
||||
_currentHandles.Add(handle);
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"已获取 {_currentHandles.Count} 个资源句柄");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放当前场景资源
|
||||
/// </summary>
|
||||
public void ReleaseCurrentResources()
|
||||
{
|
||||
Console.WriteLine($"释放 {_currentHandles.Count} 个资源句柄");
|
||||
|
||||
foreach (var handle in _currentHandles)
|
||||
{
|
||||
handle.Dispose();
|
||||
}
|
||||
|
||||
_currentHandles.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 卸载场景资源
|
||||
/// </summary>
|
||||
public void UnloadSceneResources(string sceneName)
|
||||
{
|
||||
if (!_sceneResources.TryGetValue(sceneName, out var resources))
|
||||
return;
|
||||
|
||||
Console.WriteLine($"\n卸载场景 {sceneName} 的资源");
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
foreach (var path in resources)
|
||||
{
|
||||
resourceManager.Unload(path);
|
||||
}
|
||||
|
||||
Console.WriteLine($"场景 {sceneName} 资源已卸载\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示资源状态
|
||||
/// </summary>
|
||||
public void ShowResourceStatus()
|
||||
{
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
Console.WriteLine("\n=== 资源状态 ===");
|
||||
Console.WriteLine($"已加载资源数: {resourceManager.LoadedResourceCount}");
|
||||
Console.WriteLine("已加载资源列表:");
|
||||
|
||||
foreach (var path in resourceManager.GetLoadedResourcePaths())
|
||||
{
|
||||
Console.WriteLine($" - {path}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 使用字典配置每个场景需要的资源
|
||||
- `PreloadSceneAsync` 并行预加载所有资源
|
||||
- `PreloadSceneWithProgressAsync` 提供加载进度回调
|
||||
- `AcquireSceneResources` 获取资源句柄防止被释放
|
||||
- `ReleaseCurrentResources` 释放不再使用的资源
|
||||
|
||||
## 步骤 4:实现资源使用示例
|
||||
|
||||
创建一个游戏系统展示如何正确使用资源。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Core.Abstractions.resource;
|
||||
using GFramework.Core.extensions;
|
||||
using MyGame.Resources;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// 游戏系统示例
|
||||
/// </summary>
|
||||
public class GameplaySystem : AbstractSystem
|
||||
{
|
||||
private IResourceHandle<Texture>? _playerTexture;
|
||||
private IResourceHandle<AudioClip>? _bgmClip;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化游戏
|
||||
/// </summary>
|
||||
public void InitializeGame()
|
||||
{
|
||||
Console.WriteLine("\n=== 初始化游戏 ===");
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
// 获取玩家纹理句柄
|
||||
_playerTexture = resourceManager.GetHandle<Texture>("textures/player.png");
|
||||
if (_playerTexture?.Resource != null)
|
||||
{
|
||||
Console.WriteLine($"玩家纹理已加载: {_playerTexture.Resource.Width}x{_playerTexture.Resource.Height}");
|
||||
}
|
||||
|
||||
// 获取背景音乐句柄
|
||||
_bgmClip = resourceManager.GetHandle<AudioClip>("audio/game_bgm.mp3");
|
||||
if (_bgmClip?.Resource != null)
|
||||
{
|
||||
Console.WriteLine($"背景音乐已加载: {_bgmClip.Resource.Duration}秒");
|
||||
}
|
||||
|
||||
Console.WriteLine("游戏初始化完成\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用临时资源(使用 using 语句)
|
||||
/// </summary>
|
||||
public void SpawnBullet()
|
||||
{
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
// 使用 using 语句自动管理资源生命周期
|
||||
using var bulletTexture = resourceManager.GetHandle<Texture>("textures/bullet.png");
|
||||
|
||||
if (bulletTexture?.Resource != null)
|
||||
{
|
||||
Console.WriteLine("创建子弹,使用纹理");
|
||||
// 使用纹理创建子弹...
|
||||
}
|
||||
|
||||
// 离开作用域后自动释放句柄
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 播放音效(临时资源)
|
||||
/// </summary>
|
||||
public void PlayShootSound()
|
||||
{
|
||||
var resourceManager = this.GetUtility<IResourceManager>();
|
||||
|
||||
using var shootSound = resourceManager.GetHandle<AudioClip>("audio/shoot.mp3");
|
||||
|
||||
if (shootSound?.Resource != null)
|
||||
{
|
||||
Console.WriteLine("播放射击音效");
|
||||
// 播放音效...
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理游戏资源
|
||||
/// </summary>
|
||||
public void CleanupGame()
|
||||
{
|
||||
Console.WriteLine("\n=== 清理游戏资源 ===");
|
||||
|
||||
// 释放长期持有的资源句柄
|
||||
_playerTexture?.Dispose();
|
||||
_playerTexture = null;
|
||||
|
||||
_bgmClip?.Dispose();
|
||||
_bgmClip = null;
|
||||
|
||||
Console.WriteLine("游戏资源已清理\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 长期使用的资源(玩家纹理、BGM)保存句柄
|
||||
- 临时资源(子弹纹理、音效)使用 `using` 语句
|
||||
- 在清理时释放所有持有的句柄
|
||||
- 展示了正确的资源生命周期管理
|
||||
|
||||
## 步骤 5:测试资源管理
|
||||
|
||||
编写测试代码验证资源管理功能。
|
||||
|
||||
```csharp
|
||||
using MyGame;
|
||||
using MyGame.Systems;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
class Program
|
||||
{
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("=== 资源管理最佳实践测试 ===\n");
|
||||
|
||||
// 1. 初始化架构
|
||||
var architecture = new GameArchitecture();
|
||||
architecture.Initialize();
|
||||
await architecture.WaitUntilReadyAsync();
|
||||
|
||||
// 2. 获取系统
|
||||
var preloadSystem = architecture.GetSystem<ResourcePreloadSystem>();
|
||||
var gameplaySystem = architecture.GetSystem<GameplaySystem>();
|
||||
|
||||
// 3. 测试场景资源预加载
|
||||
Console.WriteLine("--- 测试 1: 预加载菜单场景 ---");
|
||||
await preloadSystem.PreloadSceneWithProgressAsync("Menu", progress =>
|
||||
{
|
||||
// 进度回调
|
||||
});
|
||||
preloadSystem.ShowResourceStatus();
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
// 4. 切换到游戏场景
|
||||
Console.WriteLine("--- 测试 2: 切换到游戏场景 ---");
|
||||
await preloadSystem.PreloadSceneWithProgressAsync("Gameplay", progress =>
|
||||
{
|
||||
// 进度回调
|
||||
});
|
||||
preloadSystem.ShowResourceStatus();
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
// 5. 获取场景资源句柄
|
||||
Console.WriteLine("--- 测试 3: 获取游戏场景资源句柄 ---");
|
||||
preloadSystem.AcquireSceneResources("Gameplay");
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
// 6. 初始化游戏
|
||||
Console.WriteLine("--- 测试 4: 初始化游戏 ---");
|
||||
gameplaySystem.InitializeGame();
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
// 7. 使用临时资源
|
||||
Console.WriteLine("--- 测试 5: 使用临时资源 ---");
|
||||
gameplaySystem.SpawnBullet();
|
||||
gameplaySystem.PlayShootSound();
|
||||
preloadSystem.ShowResourceStatus();
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
// 8. 清理游戏
|
||||
Console.WriteLine("--- 测试 6: 清理游戏 ---");
|
||||
gameplaySystem.CleanupGame();
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
// 9. 释放场景资源句柄
|
||||
Console.WriteLine("--- 测试 7: 释放场景资源句柄 ---");
|
||||
preloadSystem.ReleaseCurrentResources();
|
||||
preloadSystem.ShowResourceStatus();
|
||||
|
||||
await Task.Delay(500);
|
||||
|
||||
// 10. 卸载旧场景资源
|
||||
Console.WriteLine("--- 测试 8: 卸载菜单场景资源 ---");
|
||||
preloadSystem.UnloadSceneResources("Menu");
|
||||
preloadSystem.ShowResourceStatus();
|
||||
|
||||
Console.WriteLine("=== 测试完成 ===");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 测试资源预加载和进度回调
|
||||
- 测试场景切换时的资源管理
|
||||
- 测试资源句柄的获取和释放
|
||||
- 测试临时资源的自动管理
|
||||
- 验证资源状态和内存清理
|
||||
|
||||
## 完整代码
|
||||
|
||||
所有代码文件已在上述步骤中提供。项目结构如下:
|
||||
|
||||
```
|
||||
MyGame/
|
||||
├── Resources/
|
||||
│ ├── Texture.cs
|
||||
│ ├── AudioClip.cs
|
||||
│ ├── ConfigData.cs
|
||||
│ ├── TextureLoader.cs
|
||||
│ ├── AudioLoader.cs
|
||||
│ └── ConfigLoader.cs
|
||||
├── Systems/
|
||||
│ ├── ResourcePreloadSystem.cs
|
||||
│ └── GameplaySystem.cs
|
||||
├── GameArchitecture.cs
|
||||
└── Program.cs
|
||||
```
|
||||
|
||||
## 运行结果
|
||||
|
||||
运行程序后,你将看到类似以下的输出:
|
||||
|
||||
```
|
||||
=== 资源管理最佳实践测试 ===
|
||||
|
||||
资源管理器初始化完成
|
||||
|
||||
--- 测试 1: 预加载菜单场景 ---
|
||||
|
||||
=== 开始预加载场景: Menu ===
|
||||
异步加载纹理: textures/menu_bg.png
|
||||
异步加载纹理: textures/button.png
|
||||
异步加载音频: audio/menu_bgm.mp3
|
||||
加载进度: 33% (1/3)
|
||||
加载进度: 67% (2/3)
|
||||
加载进度: 100% (3/3)
|
||||
场景 Menu 资源预加载完成
|
||||
|
||||
=== 资源状态 ===
|
||||
已加载资源数: 3
|
||||
已加载资源列表:
|
||||
- textures/menu_bg.png
|
||||
- textures/button.png
|
||||
- audio/menu_bgm.mp3
|
||||
|
||||
--- 测试 2: 切换到游戏场景 ---
|
||||
|
||||
=== 开始预加载场景: Gameplay ===
|
||||
异步加载纹理: textures/player.png
|
||||
异步加载纹理: textures/enemy.png
|
||||
异步加载纹理: textures/bullet.png
|
||||
异步加载音频: audio/game_bgm.mp3
|
||||
异步加载音频: audio/shoot.mp3
|
||||
加载配置: config/level_1.cfg
|
||||
加载进度: 17% (1/6)
|
||||
加载进度: 33% (2/6)
|
||||
加载进度: 50% (3/6)
|
||||
加载进度: 67% (4/6)
|
||||
加载进度: 83% (5/6)
|
||||
加载进度: 100% (6/6)
|
||||
场景 Gameplay 资源预加载完成
|
||||
|
||||
=== 资源状态 ===
|
||||
已加载资源数: 9
|
||||
|
||||
--- 测试 3: 获取游戏场景资源句柄 ---
|
||||
获取场景 Gameplay 的资源句柄
|
||||
已获取 6 个资源句柄
|
||||
|
||||
--- 测试 4: 初始化游戏 ===
|
||||
|
||||
=== 初始化游戏 ===
|
||||
玩家纹理已加载: 512x512
|
||||
背景音乐已加载: 30秒
|
||||
游戏初始化完成
|
||||
|
||||
--- 测试 5: 使用临时资源 ---
|
||||
创建子弹,使用纹理
|
||||
播放射击音效
|
||||
|
||||
--- 测试 6: 清理游戏 ---
|
||||
|
||||
=== 清理游戏资源 ===
|
||||
游戏资源已清理
|
||||
|
||||
--- 测试 7: 释放场景资源句柄 ---
|
||||
释放 6 个资源句柄
|
||||
|
||||
--- 测试 8: 卸载菜单场景资源 ---
|
||||
|
||||
卸载场景 Menu 的资源
|
||||
纹理已释放: textures/menu_bg.png
|
||||
纹理已释放: textures/button.png
|
||||
音频已释放: audio/menu_bgm.mp3
|
||||
场景 Menu 资源已卸载
|
||||
|
||||
=== 资源状态 ===
|
||||
已加载资源数: 6
|
||||
|
||||
=== 测试完成 ===
|
||||
```
|
||||
|
||||
**验证步骤**:
|
||||
|
||||
1. 资源预加载正常工作
|
||||
2. 加载进度正确显示
|
||||
3. 资源句柄管理正确
|
||||
4. 临时资源自动释放
|
||||
5. 资源卸载成功执行
|
||||
6. 内存正确清理
|
||||
|
||||
## 下一步
|
||||
|
||||
恭喜!你已经掌握了资源管理的最佳实践。接下来可以学习:
|
||||
|
||||
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial) - 在协程中加载资源
|
||||
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial) - 在状态切换时管理资源
|
||||
- [实现存档系统](/zh-CN/tutorials/save-system) - 保存和加载游戏数据
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [资源管理系统](/zh-CN/core/resource) - 资源系统详细说明
|
||||
- [对象池系统](/zh-CN/core/pool) - 结合对象池复用资源
|
||||
- [协程系统](/zh-CN/core/coroutine) - 异步加载资源
|
||||
- [System 层](/zh-CN/core/system) - System 详细说明
|
||||
966
docs/zh-CN/tutorials/save-system.md
Normal file
966
docs/zh-CN/tutorials/save-system.md
Normal file
@ -0,0 +1,966 @@
|
||||
---
|
||||
title: 实现存档系统
|
||||
description: 学习如何实现完整的游戏存档系统,支持多槽位和自动保存
|
||||
---
|
||||
|
||||
# 实现存档系统
|
||||
|
||||
## 学习目标
|
||||
|
||||
完成本教程后,你将能够:
|
||||
|
||||
- 理解游戏存档系统的设计原则
|
||||
- 定义存档数据结构
|
||||
- 实现多槽位存档管理
|
||||
- 实现自动保存功能
|
||||
- 处理存档加载和保存错误
|
||||
- 实现存档列表和删除功能
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已安装 GFramework.Game NuGet 包
|
||||
- 了解 C# 基础语法和 async/await
|
||||
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
|
||||
- 了解[数据与存档系统](/zh-CN/game/data)
|
||||
|
||||
## 步骤 1:定义存档数据结构
|
||||
|
||||
首先,让我们定义游戏存档需要保存的数据结构。
|
||||
|
||||
```csharp
|
||||
using GFramework.Game.Abstractions.data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MyGame.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家数据
|
||||
/// </summary>
|
||||
public class PlayerData
|
||||
{
|
||||
public string Name { get; set; } = "Player";
|
||||
public int Level { get; set; } = 1;
|
||||
public int Experience { get; set; } = 0;
|
||||
public int Health { get; set; } = 100;
|
||||
public int MaxHealth { get; set; } = 100;
|
||||
public int Gold { get; set; } = 0;
|
||||
public Vector3 Position { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 位置数据
|
||||
/// </summary>
|
||||
public class Vector3
|
||||
{
|
||||
public float X { get; set; }
|
||||
public float Y { get; set; }
|
||||
public float Z { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关卡进度数据
|
||||
/// </summary>
|
||||
public class ProgressData
|
||||
{
|
||||
public int CurrentLevel { get; set; } = 1;
|
||||
public List<int> CompletedLevels { get; set; } = new();
|
||||
public Dictionary<string, bool> Achievements { get; set; } = new();
|
||||
public float PlayTime { get; set; } = 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 物品数据
|
||||
/// </summary>
|
||||
public class InventoryData
|
||||
{
|
||||
public List<ItemData> Items { get; set; } = new();
|
||||
public List<string> EquippedItems { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单个物品数据
|
||||
/// </summary>
|
||||
public class ItemData
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; } = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 完整的存档数据
|
||||
/// </summary>
|
||||
public class GameSaveData : IVersionedData
|
||||
{
|
||||
// 数据版本号
|
||||
public int Version { get; set; } = 1;
|
||||
|
||||
// 存档元数据
|
||||
public DateTime SaveTime { get; set; }
|
||||
public string SaveName { get; set; } = "New Save";
|
||||
public float TotalPlayTime { get; set; }
|
||||
|
||||
// 游戏数据
|
||||
public PlayerData Player { get; set; } = new();
|
||||
public ProgressData Progress { get; set; } = new();
|
||||
public InventoryData Inventory { get; set; } = new();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- `GameSaveData` 实现 `IVersionedData` 支持版本管理
|
||||
- 将数据分为玩家、进度、物品等模块
|
||||
- 包含存档元数据(保存时间、名称等)
|
||||
- 使用属性初始化器设置默认值
|
||||
|
||||
## 步骤 2:创建存档管理系统
|
||||
|
||||
实现一个系统来管理存档的创建、加载和保存。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Core.Abstractions.resource;
|
||||
using GFramework.Core.extensions;
|
||||
using GFramework.Game.Abstractions.data;
|
||||
using MyGame.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// 存档管理系统
|
||||
/// </summary>
|
||||
public class SaveSystem : AbstractSystem
|
||||
{
|
||||
private GameSaveData? _currentSave;
|
||||
private int _currentSlot = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 创建新存档
|
||||
/// </summary>
|
||||
public GameSaveData CreateNewSave(string saveName)
|
||||
{
|
||||
Console.WriteLine($"创建新存档: {saveName}");
|
||||
|
||||
var saveData = new GameSaveData
|
||||
{
|
||||
SaveName = saveName,
|
||||
SaveTime = DateTime.Now,
|
||||
TotalPlayTime = 0f,
|
||||
Player = new PlayerData
|
||||
{
|
||||
Name = "Player",
|
||||
Level = 1,
|
||||
Experience = 0,
|
||||
Health = 100,
|
||||
MaxHealth = 100,
|
||||
Gold = 0
|
||||
},
|
||||
Progress = new ProgressData
|
||||
{
|
||||
CurrentLevel = 1,
|
||||
CompletedLevels = new List<int>(),
|
||||
Achievements = new Dictionary<string, bool>(),
|
||||
PlayTime = 0f
|
||||
},
|
||||
Inventory = new InventoryData
|
||||
{
|
||||
Items = new List<ItemData>(),
|
||||
EquippedItems = new List<string>()
|
||||
}
|
||||
};
|
||||
|
||||
_currentSave = saveData;
|
||||
return saveData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存游戏
|
||||
/// </summary>
|
||||
public async Task<bool> SaveGameAsync(int slot)
|
||||
{
|
||||
if (_currentSave == null)
|
||||
{
|
||||
Console.WriteLine("错误: 没有当前存档数据");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"\n=== 保存游戏到槽位 {slot} ===");
|
||||
|
||||
var saveRepo = this.GetUtility<ISaveRepository<GameSaveData>>();
|
||||
|
||||
// 更新保存时间
|
||||
_currentSave.SaveTime = DateTime.Now;
|
||||
_currentSlot = slot;
|
||||
|
||||
// 保存到存储
|
||||
await saveRepo.SaveAsync(slot, _currentSave);
|
||||
|
||||
Console.WriteLine($"存档已保存: {_currentSave.SaveName}");
|
||||
Console.WriteLine($"玩家: {_currentSave.Player.Name}, 等级 {_currentSave.Player.Level}");
|
||||
Console.WriteLine($"游戏时间: {_currentSave.TotalPlayTime:F1} 小时");
|
||||
Console.WriteLine($"保存时间: {_currentSave.SaveTime}\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"保存失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载游戏
|
||||
/// </summary>
|
||||
public async Task<bool> LoadGameAsync(int slot)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"\n=== 从槽位 {slot} 加载游戏 ===");
|
||||
|
||||
var saveRepo = this.GetUtility<ISaveRepository<GameSaveData>>();
|
||||
|
||||
// 检查存档是否存在
|
||||
if (!await saveRepo.ExistsAsync(slot))
|
||||
{
|
||||
Console.WriteLine($"槽位 {slot} 不存在存档\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 加载存档
|
||||
_currentSave = await saveRepo.LoadAsync(slot);
|
||||
_currentSlot = slot;
|
||||
|
||||
Console.WriteLine($"存档已加载: {_currentSave.SaveName}");
|
||||
Console.WriteLine($"玩家: {_currentSave.Player.Name}, 等级 {_currentSave.Player.Level}");
|
||||
Console.WriteLine($"当前关卡: {_currentSave.Progress.CurrentLevel}");
|
||||
Console.WriteLine($"金币: {_currentSave.Player.Gold}");
|
||||
Console.WriteLine($"物品数量: {_currentSave.Inventory.Items.Count}");
|
||||
Console.WriteLine($"保存时间: {_currentSave.SaveTime}\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"加载失败: {ex.Message}\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除存档
|
||||
/// </summary>
|
||||
public async Task<bool> DeleteSaveAsync(int slot)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"\n删除槽位 {slot} 的存档");
|
||||
|
||||
var saveRepo = this.GetUtility<ISaveRepository<GameSaveData>>();
|
||||
|
||||
if (!await saveRepo.ExistsAsync(slot))
|
||||
{
|
||||
Console.WriteLine($"槽位 {slot} 不存在存档\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
await saveRepo.DeleteAsync(slot);
|
||||
|
||||
if (_currentSlot == slot)
|
||||
{
|
||||
_currentSave = null;
|
||||
_currentSlot = -1;
|
||||
}
|
||||
|
||||
Console.WriteLine($"槽位 {slot} 的存档已删除\n");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"删除失败: {ex.Message}\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 列出所有存档
|
||||
/// </summary>
|
||||
public async Task<List<SaveSlotInfo>> ListSavesAsync()
|
||||
{
|
||||
var result = new List<SaveSlotInfo>();
|
||||
|
||||
try
|
||||
{
|
||||
var saveRepo = this.GetUtility<ISaveRepository<GameSaveData>>();
|
||||
var slots = await saveRepo.ListSlotsAsync();
|
||||
|
||||
Console.WriteLine($"\n=== 存档列表 ({slots.Count} 个) ===");
|
||||
|
||||
foreach (var slot in slots)
|
||||
{
|
||||
var saveData = await saveRepo.LoadAsync(slot);
|
||||
|
||||
var info = new SaveSlotInfo
|
||||
{
|
||||
Slot = slot,
|
||||
SaveName = saveData.SaveName,
|
||||
PlayerName = saveData.Player.Name,
|
||||
Level = saveData.Player.Level,
|
||||
SaveTime = saveData.SaveTime,
|
||||
PlayTime = saveData.TotalPlayTime
|
||||
};
|
||||
|
||||
result.Add(info);
|
||||
|
||||
Console.WriteLine($"槽位 {slot}: {info.SaveName}");
|
||||
Console.WriteLine($" 玩家: {info.PlayerName}, 等级 {info.Level}");
|
||||
Console.WriteLine($" 保存时间: {info.SaveTime}");
|
||||
Console.WriteLine($" 游戏时间: {info.PlayTime:F1} 小时");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"列出存档失败: {ex.Message}\n");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前存档
|
||||
/// </summary>
|
||||
public GameSaveData? GetCurrentSave() => _currentSave;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前槽位
|
||||
/// </summary>
|
||||
public int GetCurrentSlot() => _currentSlot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 存档槽位信息
|
||||
/// </summary>
|
||||
public class SaveSlotInfo
|
||||
{
|
||||
public int Slot { get; set; }
|
||||
public string SaveName { get; set; } = string.Empty;
|
||||
public string PlayerName { get; set; } = string.Empty;
|
||||
public int Level { get; set; }
|
||||
public DateTime SaveTime { get; set; }
|
||||
public float PlayTime { get; set; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- `CreateNewSave` 创建新的存档数据
|
||||
- `SaveGameAsync` 保存当前存档到指定槽位
|
||||
- `LoadGameAsync` 从槽位加载存档
|
||||
- `DeleteSaveAsync` 删除指定槽位的存档
|
||||
- `ListSavesAsync` 列出所有存档信息
|
||||
- 使用 try-catch 处理异常
|
||||
|
||||
## 步骤 3:实现自动保存功能
|
||||
|
||||
创建自动保存系统,定期保存游戏进度。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Core.extensions;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// 自动保存系统
|
||||
/// </summary>
|
||||
public class AutoSaveSystem : AbstractSystem
|
||||
{
|
||||
private CancellationTokenSource? _autoSaveCts;
|
||||
private bool _isAutoSaveEnabled;
|
||||
private TimeSpan _autoSaveInterval = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// 启动自动保存
|
||||
/// </summary>
|
||||
public void StartAutoSave(int slot, TimeSpan? interval = null)
|
||||
{
|
||||
if (_isAutoSaveEnabled)
|
||||
{
|
||||
Console.WriteLine("自动保存已在运行");
|
||||
return;
|
||||
}
|
||||
|
||||
if (interval.HasValue)
|
||||
{
|
||||
_autoSaveInterval = interval.Value;
|
||||
}
|
||||
|
||||
_autoSaveCts = new CancellationTokenSource();
|
||||
_isAutoSaveEnabled = true;
|
||||
|
||||
Console.WriteLine($"\n启动自动保存 (间隔: {_autoSaveInterval.TotalSeconds} 秒)");
|
||||
|
||||
// 在后台线程运行自动保存
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (!_autoSaveCts.Token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 等待指定间隔
|
||||
await Task.Delay(_autoSaveInterval, _autoSaveCts.Token);
|
||||
|
||||
// 执行自动保存
|
||||
await PerformAutoSaveAsync(slot);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 正常取消
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"自动保存错误: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}, _autoSaveCts.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止自动保存
|
||||
/// </summary>
|
||||
public void StopAutoSave()
|
||||
{
|
||||
if (!_isAutoSaveEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("停止自动保存");
|
||||
|
||||
_autoSaveCts?.Cancel();
|
||||
_autoSaveCts?.Dispose();
|
||||
_autoSaveCts = null;
|
||||
_isAutoSaveEnabled = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行自动保存
|
||||
/// </summary>
|
||||
private async Task PerformAutoSaveAsync(int slot)
|
||||
{
|
||||
Console.WriteLine($"\n[自动保存] 保存到槽位 {slot}...");
|
||||
|
||||
var saveSystem = this.GetSystem<SaveSystem>();
|
||||
var success = await saveSystem.SaveGameAsync(slot);
|
||||
|
||||
if (success)
|
||||
{
|
||||
Console.WriteLine("[自动保存] 完成");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("[自动保存] 失败");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查自动保存是否启用
|
||||
/// </summary>
|
||||
public bool IsAutoSaveEnabled() => _isAutoSaveEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// 设置自动保存间隔
|
||||
/// </summary>
|
||||
public void SetAutoSaveInterval(TimeSpan interval)
|
||||
{
|
||||
_autoSaveInterval = interval;
|
||||
Console.WriteLine($"自动保存间隔已设置为 {interval.TotalSeconds} 秒");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 使用 `CancellationTokenSource` 控制后台任务
|
||||
- `StartAutoSave` 启动定时保存任务
|
||||
- `StopAutoSave` 停止自动保存
|
||||
- 使用 `Task.Delay` 实现定时触发
|
||||
- 捕获并处理异常,避免自动保存失败影响游戏
|
||||
|
||||
## 步骤 4:实现游戏数据更新
|
||||
|
||||
创建一个系统来模拟游戏数据的变化。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.system;
|
||||
using GFramework.Core.extensions;
|
||||
using MyGame.Data;
|
||||
using System;
|
||||
|
||||
namespace MyGame.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// 游戏逻辑系统(模拟)
|
||||
/// </summary>
|
||||
public class GameLogicSystem : AbstractSystem
|
||||
{
|
||||
private Random _random = new();
|
||||
|
||||
/// <summary>
|
||||
/// 模拟玩家升级
|
||||
/// </summary>
|
||||
public void LevelUp()
|
||||
{
|
||||
var saveSystem = this.GetSystem<SaveSystem>();
|
||||
var save = saveSystem.GetCurrentSave();
|
||||
|
||||
if (save == null)
|
||||
{
|
||||
Console.WriteLine("没有当前存档");
|
||||
return;
|
||||
}
|
||||
|
||||
save.Player.Level++;
|
||||
save.Player.Experience = 0;
|
||||
save.Player.MaxHealth += 10;
|
||||
save.Player.Health = save.Player.MaxHealth;
|
||||
|
||||
Console.WriteLine($"\n玩家升级! 当前等级: {save.Player.Level}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟获得金币
|
||||
/// </summary>
|
||||
public void AddGold(int amount)
|
||||
{
|
||||
var saveSystem = this.GetSystem<SaveSystem>();
|
||||
var save = saveSystem.GetCurrentSave();
|
||||
|
||||
if (save == null) return;
|
||||
|
||||
save.Player.Gold += amount;
|
||||
Console.WriteLine($"\n获得金币 +{amount}, 当前: {save.Player.Gold}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟完成关卡
|
||||
/// </summary>
|
||||
public void CompleteLevel(int level)
|
||||
{
|
||||
var saveSystem = this.GetSystem<SaveSystem>();
|
||||
var save = saveSystem.GetCurrentSave();
|
||||
|
||||
if (save == null) return;
|
||||
|
||||
if (!save.Progress.CompletedLevels.Contains(level))
|
||||
{
|
||||
save.Progress.CompletedLevels.Add(level);
|
||||
Console.WriteLine($"\n完成关卡 {level}!");
|
||||
}
|
||||
|
||||
save.Progress.CurrentLevel = level + 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟获得物品
|
||||
/// </summary>
|
||||
public void AddItem(string itemId, string itemName, int quantity = 1)
|
||||
{
|
||||
var saveSystem = this.GetSystem<SaveSystem>();
|
||||
var save = saveSystem.GetCurrentSave();
|
||||
|
||||
if (save == null) return;
|
||||
|
||||
// 查找已有物品
|
||||
var existingItem = save.Inventory.Items.Find(i => i.Id == itemId);
|
||||
|
||||
if (existingItem != null)
|
||||
{
|
||||
existingItem.Quantity += quantity;
|
||||
}
|
||||
else
|
||||
{
|
||||
save.Inventory.Items.Add(new ItemData
|
||||
{
|
||||
Id = itemId,
|
||||
Name = itemName,
|
||||
Quantity = quantity
|
||||
});
|
||||
}
|
||||
|
||||
Console.WriteLine($"\n获得物品: {itemName} x{quantity}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟游戏进行
|
||||
/// </summary>
|
||||
public void SimulateGameplay()
|
||||
{
|
||||
Console.WriteLine("\n=== 模拟游戏进行 ===");
|
||||
|
||||
// 随机事件
|
||||
int eventType = _random.Next(0, 4);
|
||||
|
||||
switch (eventType)
|
||||
{
|
||||
case 0:
|
||||
AddGold(_random.Next(10, 100));
|
||||
break;
|
||||
case 1:
|
||||
LevelUp();
|
||||
break;
|
||||
case 2:
|
||||
CompleteLevel(_random.Next(1, 10));
|
||||
break;
|
||||
case 3:
|
||||
AddItem($"item_{_random.Next(1, 5)}", $"物品 {_random.Next(1, 5)}", 1);
|
||||
break;
|
||||
}
|
||||
|
||||
// 增加游戏时间
|
||||
var saveSystem = this.GetSystem<SaveSystem>();
|
||||
var save = saveSystem.GetCurrentSave();
|
||||
if (save != null)
|
||||
{
|
||||
save.TotalPlayTime += 0.1f;
|
||||
save.Progress.PlayTime += 0.1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 提供各种游戏事件的模拟方法
|
||||
- 直接修改当前存档数据
|
||||
- 用于测试存档系统功能
|
||||
|
||||
## 步骤 5:注册系统并测试
|
||||
|
||||
在架构中注册所有系统并进行测试。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.architecture;
|
||||
using GFramework.Game.Abstractions.data;
|
||||
using GFramework.Game.Abstractions.storage;
|
||||
using GFramework.Game.data;
|
||||
using GFramework.Game.storage;
|
||||
using MyGame.Data;
|
||||
using MyGame.Systems;
|
||||
|
||||
namespace MyGame
|
||||
{
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
public static IArchitecture Interface { get; private set; }
|
||||
|
||||
protected override void Init()
|
||||
{
|
||||
Interface = this;
|
||||
|
||||
// 注册存储系统
|
||||
var storage = new FileStorage("./game_data");
|
||||
RegisterUtility<IFileStorage>(storage);
|
||||
|
||||
// 注册存档仓库
|
||||
var saveConfig = new SaveConfiguration
|
||||
{
|
||||
SaveRoot = "saves",
|
||||
SaveSlotPrefix = "slot_",
|
||||
SaveFileName = "save.json"
|
||||
};
|
||||
|
||||
var saveRepo = new SaveRepository<GameSaveData>(storage, saveConfig);
|
||||
RegisterUtility<ISaveRepository<GameSaveData>>(saveRepo);
|
||||
|
||||
// 注册系统
|
||||
RegisterSystem(new SaveSystem());
|
||||
RegisterSystem(new AutoSaveSystem());
|
||||
RegisterSystem(new GameLogicSystem());
|
||||
|
||||
Console.WriteLine("游戏架构初始化完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 测试代码
|
||||
|
||||
```csharp
|
||||
using MyGame;
|
||||
using MyGame.Systems;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
class Program
|
||||
{
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("=== 存档系统测试 ===\n");
|
||||
|
||||
// 1. 初始化架构
|
||||
var architecture = new GameArchitecture();
|
||||
architecture.Initialize();
|
||||
await architecture.WaitUntilReadyAsync();
|
||||
|
||||
// 2. 获取系统
|
||||
var saveSystem = architecture.GetSystem<SaveSystem>();
|
||||
var autoSaveSystem = architecture.GetSystem<AutoSaveSystem>();
|
||||
var gameLogic = architecture.GetSystem<GameLogicSystem>();
|
||||
|
||||
// 3. 创建新存档
|
||||
Console.WriteLine("--- 测试 1: 创建新存档 ---");
|
||||
saveSystem.CreateNewSave("我的冒险");
|
||||
await saveSystem.SaveGameAsync(1);
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 4. 模拟游戏进行
|
||||
Console.WriteLine("--- 测试 2: 游戏进行 ---");
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
gameLogic.SimulateGameplay();
|
||||
await Task.Delay(500);
|
||||
}
|
||||
|
||||
// 5. 手动保存
|
||||
Console.WriteLine("\n--- 测试 3: 手动保存 ---");
|
||||
await saveSystem.SaveGameAsync(1);
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 6. 创建第二个存档
|
||||
Console.WriteLine("--- 测试 4: 创建第二个存档 ---");
|
||||
saveSystem.CreateNewSave("新的旅程");
|
||||
gameLogic.AddGold(500);
|
||||
gameLogic.LevelUp();
|
||||
await saveSystem.SaveGameAsync(2);
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 7. 列出所有存档
|
||||
Console.WriteLine("--- 测试 5: 列出所有存档 ---");
|
||||
await saveSystem.ListSavesAsync();
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 8. 加载第一个存档
|
||||
Console.WriteLine("--- 测试 6: 加载存档 ---");
|
||||
await saveSystem.LoadGameAsync(1);
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 9. 启动自动保存
|
||||
Console.WriteLine("--- 测试 7: 启动自动保存 ---");
|
||||
autoSaveSystem.StartAutoSave(1, TimeSpan.FromSeconds(3));
|
||||
|
||||
// 模拟游戏进行
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
gameLogic.SimulateGameplay();
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
// 10. 停止自动保存
|
||||
autoSaveSystem.StopAutoSave();
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 11. 删除存档
|
||||
Console.WriteLine("--- 测试 8: 删除存档 ---");
|
||||
await saveSystem.DeleteSaveAsync(2);
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 12. 最终存档列表
|
||||
Console.WriteLine("--- 测试 9: 最终存档列表 ---");
|
||||
await saveSystem.ListSavesAsync();
|
||||
|
||||
Console.WriteLine("=== 测试完成 ===");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 注册存储系统和存档仓库
|
||||
- 注册所有游戏系统
|
||||
- 测试存档的创建、保存、加载、删除
|
||||
- 测试自动保存功能
|
||||
- 测试多槽位管理
|
||||
|
||||
## 完整代码
|
||||
|
||||
所有代码文件已在上述步骤中提供。项目结构如下:
|
||||
|
||||
```
|
||||
MyGame/
|
||||
├── Data/
|
||||
│ ├── PlayerData.cs
|
||||
│ ├── ProgressData.cs
|
||||
│ ├── InventoryData.cs
|
||||
│ └── GameSaveData.cs
|
||||
├── Systems/
|
||||
│ ├── SaveSystem.cs
|
||||
│ ├── AutoSaveSystem.cs
|
||||
│ └── GameLogicSystem.cs
|
||||
├── GameArchitecture.cs
|
||||
└── Program.cs
|
||||
```
|
||||
|
||||
## 运行结果
|
||||
|
||||
运行程序后,你将看到类似以下的输出:
|
||||
|
||||
```
|
||||
=== 存档系统测试 ===
|
||||
|
||||
游戏架构初始化完成
|
||||
|
||||
--- 测试 1: 创建新存档 ---
|
||||
创建新存档: 我的冒险
|
||||
|
||||
=== 保存游戏到槽位 1 ===
|
||||
存档已保存: 我的冒险
|
||||
玩家: Player, 等级 1
|
||||
游戏时间: 0.0 小时
|
||||
保存时间: 2026-03-07 10:30:00
|
||||
|
||||
--- 测试 2: 游戏进行 ---
|
||||
|
||||
=== 模拟游戏进行 ===
|
||||
|
||||
获得金币 +45, 当前: 45
|
||||
|
||||
=== 模拟游戏进行 ===
|
||||
|
||||
玩家升级! 当前等级: 2
|
||||
|
||||
=== 模拟游戏进行 ===
|
||||
|
||||
完成关卡 3!
|
||||
|
||||
=== 模拟游戏进行 ===
|
||||
|
||||
获得物品: 物品 2 x1
|
||||
|
||||
=== 模拟游戏进行 ===
|
||||
|
||||
获得金币 +78, 当前: 123
|
||||
|
||||
--- 测试 3: 手动保存 ---
|
||||
|
||||
=== 保存游戏到槽位 1 ===
|
||||
存档已保存: 我的冒险
|
||||
玩家: Player, 等级 2
|
||||
游戏时间: 0.5 小时
|
||||
保存时间: 2026-03-07 10:30:05
|
||||
|
||||
--- 测试 4: 创建第二个存档 ---
|
||||
创建新存档: 新的旅程
|
||||
|
||||
获得金币 +500, 当前: 500
|
||||
|
||||
玩家升级! 当前等级: 2
|
||||
|
||||
=== 保存游戏到槽位 2 ===
|
||||
存档已保存: 新的旅程
|
||||
玩家: Player, 等级 2
|
||||
游戏时间: 0.0 小时
|
||||
保存时间: 2026-03-07 10:30:06
|
||||
|
||||
--- 测试 5: 列出所有存档 ---
|
||||
|
||||
=== 存档列表 (2 个) ===
|
||||
槽位 1: 我的冒险
|
||||
玩家: Player, 等级 2
|
||||
保存时间: 2026-03-07 10:30:05
|
||||
游戏时间: 0.5 小时
|
||||
槽位 2: 新的旅程
|
||||
玩家: Player, 等级 2
|
||||
保存时间: 2026-03-07 10:30:06
|
||||
游戏时间: 0.0 小时
|
||||
|
||||
--- 测试 6: 加载存档 ---
|
||||
|
||||
=== 从槽位 1 加载游戏 ===
|
||||
存档已加载: 我的冒险
|
||||
玩家: Player, 等级 2
|
||||
当前关卡: 4
|
||||
金币: 123
|
||||
物品数量: 1
|
||||
保存时间: 2026-03-07 10:30:05
|
||||
|
||||
--- 测试 7: 启动自动保存 ---
|
||||
|
||||
启动自动保存 (间隔: 3 秒)
|
||||
|
||||
=== 模拟游戏进行 ===
|
||||
...
|
||||
|
||||
[自动保存] 保存到槽位 1...
|
||||
|
||||
=== 保存游戏到槽位 1 ===
|
||||
存档已保存: 我的冒险
|
||||
...
|
||||
[自动保存] 完成
|
||||
|
||||
停止自动保存
|
||||
|
||||
--- 测试 8: 删除存档 ---
|
||||
|
||||
删除槽位 2 的存档
|
||||
槽位 2 的存档已删除
|
||||
|
||||
--- 测试 9: 最终存档列表 ---
|
||||
|
||||
=== 存档列表 (1 个) ===
|
||||
槽位 1: 我的冒险
|
||||
玩家: Player, 等级 3
|
||||
保存时间: 2026-03-07 10:30:15
|
||||
游戏时间: 1.5 小时
|
||||
|
||||
=== 测试完成 ===
|
||||
```
|
||||
|
||||
**验证步骤**:
|
||||
|
||||
1. 存档创建和保存成功
|
||||
2. 游戏数据正确更新
|
||||
3. 多槽位管理正常
|
||||
4. 存档加载恢复数据
|
||||
5. 自动保存定时触发
|
||||
6. 存档删除功能正常
|
||||
7. 存档列表显示正确
|
||||
|
||||
## 下一步
|
||||
|
||||
恭喜!你已经实现了一个完整的存档系统。接下来可以学习:
|
||||
|
||||
- [Godot 完整项目搭建](/zh-CN/tutorials/godot-complete-project) - 在 Godot 中使用存档系统
|
||||
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial) - 异步加载存档
|
||||
- [数据与存档系统](/zh-CN/game/data) - 数据系统详细说明
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [数据与存档系统](/zh-CN/game/data) - 数据系统详细说明
|
||||
- [对象池系统](/zh-CN/core/pool) - 结合对象池复用资源
|
||||
- [协程系统](/zh-CN/core/coroutine) - 异步加载资源
|
||||
- [System 层](/zh-CN/core/system) - System 详细说明
|
||||
746
docs/zh-CN/tutorials/state-machine-tutorial.md
Normal file
746
docs/zh-CN/tutorials/state-machine-tutorial.md
Normal file
@ -0,0 +1,746 @@
|
||||
---
|
||||
title: 实现状态机
|
||||
description: 学习如何使用状态机系统管理游戏状态和场景切换
|
||||
---
|
||||
|
||||
# 实现状态机
|
||||
|
||||
## 学习目标
|
||||
|
||||
完成本教程后,你将能够:
|
||||
|
||||
- 理解状态机的概念和应用场景
|
||||
- 创建自定义游戏状态
|
||||
- 实现状态之间的转换和验证
|
||||
- 使用异步状态处理加载操作
|
||||
- 在状态中访问架构组件
|
||||
- 实现完整的游戏流程控制
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 已安装 GFramework.Core NuGet 包
|
||||
- 了解 C# 基础语法和 async/await
|
||||
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
|
||||
- 了解[生命周期管理](/zh-CN/core/lifecycle)
|
||||
|
||||
## 步骤 1:定义游戏状态
|
||||
|
||||
首先,让我们为一个简单的游戏定义几个基本状态:主菜单、加载、游戏中、暂停和游戏结束。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.state;
|
||||
using GFramework.Core.state;
|
||||
|
||||
namespace MyGame.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 主菜单状态
|
||||
/// </summary>
|
||||
public class MenuState : ContextAwareStateBase
|
||||
{
|
||||
public override void OnEnter(IState? from)
|
||||
{
|
||||
Console.WriteLine("=== 进入主菜单 ===");
|
||||
|
||||
// 显示菜单 UI
|
||||
ShowMenuUI();
|
||||
|
||||
// 播放菜单音乐
|
||||
PlayMenuMusic();
|
||||
}
|
||||
|
||||
public override void OnExit(IState? to)
|
||||
{
|
||||
Console.WriteLine("退出主菜单");
|
||||
|
||||
// 隐藏菜单 UI
|
||||
HideMenuUI();
|
||||
}
|
||||
|
||||
public override bool CanTransitionTo(IState target)
|
||||
{
|
||||
// 菜单只能切换到加载状态
|
||||
return target is LoadingState;
|
||||
}
|
||||
|
||||
private void ShowMenuUI()
|
||||
{
|
||||
Console.WriteLine("显示菜单界面");
|
||||
}
|
||||
|
||||
pd HideMenuUI()
|
||||
{
|
||||
Console.WriteLine("隐藏菜单界面");
|
||||
}
|
||||
|
||||
private void PlayMenuMusic()
|
||||
{
|
||||
Console.WriteLine("播放菜单音乐");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏中状态
|
||||
/// </summary>
|
||||
public class GameplayState : ContextAwareStateBase
|
||||
{
|
||||
public override void OnEnter(IState? from)
|
||||
{
|
||||
Console.WriteLine("=== 开始游戏 ===");
|
||||
|
||||
// 初始化游戏场景
|
||||
InitializeGameScene();
|
||||
|
||||
// 重置玩家数据
|
||||
ResetPlayerData();
|
||||
|
||||
// 播放游戏音乐
|
||||
PlayGameMusic();
|
||||
}
|
||||
|
||||
public override void OnExit(IState? to)
|
||||
{
|
||||
Console.WriteLine("结束游戏");
|
||||
|
||||
// 保存游戏进度(如果不是游戏结束)
|
||||
if (to is not GameOverState)
|
||||
{
|
||||
SaveGameProgress();
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanTransitionTo(IState target)
|
||||
{
|
||||
// 游戏中可以切换到暂停或游戏结束状态
|
||||
return target is PauseState or GameOverState;
|
||||
}
|
||||
|
||||
private void InitializeGameScene()
|
||||
{
|
||||
Console.WriteLine("初始化游戏场景");
|
||||
}
|
||||
|
||||
private void ResetPlayerData()
|
||||
{
|
||||
Console.WriteLine("重置玩家数据");
|
||||
}
|
||||
|
||||
private void PlayGameMusic()
|
||||
{
|
||||
Console.WriteLine("播放游戏音乐");
|
||||
}
|
||||
|
||||
private void SaveGameProgress()
|
||||
{
|
||||
Console.WriteLine("保存游戏进度");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停状态
|
||||
/// </summary>
|
||||
public class PauseState : ContextAwareStateBase
|
||||
{
|
||||
public override void OnEnter(IState? from)
|
||||
{
|
||||
Console.WriteLine("=== 游戏暂停 ===");
|
||||
|
||||
// 显示暂停菜单
|
||||
ShowPauseMenu();
|
||||
|
||||
// 暂停游戏逻辑
|
||||
PauseGameLogic();
|
||||
}
|
||||
|
||||
public override void OnExit(IState? to)
|
||||
{
|
||||
Console.WriteLine("取消暂停");
|
||||
|
||||
// 隐藏暂停菜单
|
||||
HidePauseMenu();
|
||||
|
||||
// 恢复游戏逻辑
|
||||
ResumeGameLogic();
|
||||
}
|
||||
|
||||
public override bool CanTransitionTo(IState target)
|
||||
{
|
||||
// 暂停状态可以返回游戏或退出到菜单
|
||||
return target is GameplayState or MenuState;
|
||||
}
|
||||
|
||||
private void ShowPauseMenu()
|
||||
{
|
||||
Console.WriteLine("显示暂停菜单");
|
||||
}
|
||||
|
||||
private void HidePauseMenu()
|
||||
{
|
||||
Console.WriteLine("隐藏暂停菜单");
|
||||
}
|
||||
|
||||
private void PauseGameLogic()
|
||||
{
|
||||
Console.WriteLine("暂停游戏逻辑");
|
||||
}
|
||||
|
||||
private void ResumeGameLogic()
|
||||
{
|
||||
Console.WriteLine("恢复游戏逻辑");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏结束状态
|
||||
/// </summary>
|
||||
public class GameOverState : ContextAwareStateBase
|
||||
{
|
||||
public bool IsVictory { get; set; }
|
||||
|
||||
public override void OnEnter(IState? from)
|
||||
{
|
||||
Console.WriteLine(IsVictory
|
||||
? "=== 游戏胜利 ==="
|
||||
: "=== 游戏失败 ===");
|
||||
|
||||
// 显示结算界面
|
||||
ShowGameOverUI();
|
||||
|
||||
// 播放结算音乐
|
||||
PlayGameOverMusic();
|
||||
}
|
||||
|
||||
public override void OnExit(IState? to)
|
||||
{
|
||||
Console.WriteLine("退出结算界面");
|
||||
|
||||
// 隐藏结算界面
|
||||
HideGameOverUI();
|
||||
}
|
||||
|
||||
public override bool CanTransitionTo(IState target)
|
||||
{
|
||||
// 游戏结束只能返回菜单
|
||||
return target is MenuState;
|
||||
}
|
||||
|
||||
private void ShowGameOverUI()
|
||||
{
|
||||
Console.WriteLine("显示结算界面");
|
||||
}
|
||||
|
||||
private void HideGameOverUI()
|
||||
{
|
||||
Console.WriteLine("隐藏结算界面");
|
||||
}
|
||||
|
||||
private void PlayGameOverMusic()
|
||||
{
|
||||
Console.WriteLine("播放结算音乐");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 继承 `ContextAwareStateBase` 创建状态
|
||||
- `OnEnter` 在进入状态时调用,用于初始化
|
||||
- `OnExit` 在退出状态时调用,用于清理
|
||||
- `CanTransitionTo` 定义允许的状态转换规则
|
||||
- 每个状态职责单一,逻辑清晰
|
||||
|
||||
## 步骤 2:创建异步加载状态
|
||||
|
||||
实现一个异步加载状态,用于加载游戏资源。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.state;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MyGame.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 加载状态(异步)
|
||||
/// </summary>
|
||||
public class LoadingState : AsyncContextAwareStateBase
|
||||
{
|
||||
public int TargetLevel { get; set; } = 1;
|
||||
|
||||
public override async Task OnEnterAsync(IState? from)
|
||||
{
|
||||
Console.WriteLine($"=== 开始加载关卡 {TargetLevel} ===");
|
||||
|
||||
// 显示加载界面
|
||||
ShowLoadingUI();
|
||||
|
||||
// 异步加载资源
|
||||
await LoadResourcesAsync();
|
||||
|
||||
// 加载完成后自动切换到游戏状态
|
||||
Console.WriteLine("加载完成,进入游戏");
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
await stateMachine.ChangeToAsync<GameplayState>();
|
||||
}
|
||||
|
||||
public override async Task OnExitAsync(IState? to)
|
||||
{
|
||||
Console.WriteLine("退出加载状态");
|
||||
|
||||
// 隐藏加载界面
|
||||
HideLoadingUI();
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override bool CanTransitionTo(IState target)
|
||||
{
|
||||
// 加载状态只能切换到游戏状态
|
||||
return target is GameplayState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步加载资源
|
||||
/// </summary>
|
||||
private async Task LoadResourcesAsync()
|
||||
{
|
||||
// 加载纹理
|
||||
Console.WriteLine("加载纹理资源...");
|
||||
await Task.Delay(500);
|
||||
Console.WriteLine("纹理加载完成 (33%)");
|
||||
|
||||
// 加载音频
|
||||
Console.WriteLine("加载音频资源...");
|
||||
await Task.Delay(500);
|
||||
Console.WriteLine("音频加载完成 (66%)");
|
||||
|
||||
// 加载关卡数据
|
||||
Console.WriteLine("加载关卡数据...");
|
||||
await Task.Delay(500);
|
||||
Console.WriteLine("关卡数据加载完成 (100%)");
|
||||
}
|
||||
|
||||
private void ShowLoadingUI()
|
||||
{
|
||||
Console.WriteLine("显示加载界面");
|
||||
}
|
||||
|
||||
private void HideLoadingUI()
|
||||
{
|
||||
Console.WriteLine("隐藏加载界面");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 继承 `AsyncContextAwareStateBase` 支持异步操作
|
||||
- `OnEnterAsync` 和 `OnExitAsync` 是异步方法
|
||||
- 可以在状态中使用 `await` 等待异步操作
|
||||
- 加载完成后自动切换到下一个状态
|
||||
|
||||
## 步骤 3:注册状态机系统
|
||||
|
||||
在架构中注册状态机系统和所有状态。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.architecture;
|
||||
using GFramework.Core.Abstractions.state;
|
||||
using GFramework.Core.state;
|
||||
using MyGame.States;
|
||||
|
||||
namespace MyGame
|
||||
{
|
||||
public class GameArchitecture : Architecture
|
||||
{
|
||||
public static IArchitecture Interface { get; private set; }
|
||||
|
||||
protected override void Init()
|
||||
{
|
||||
Interface = this;
|
||||
|
||||
// 创建状态机系统
|
||||
var stateMachine = new StateMachineSystem();
|
||||
|
||||
// 注册所有状态
|
||||
stateMachine
|
||||
.Register(new MenuState())
|
||||
.Register(new LoadingState())
|
||||
.Register(new GameplayState())
|
||||
.Register(new PauseState())
|
||||
.Register(new GameOverState());
|
||||
|
||||
// 注册状态机系统到架构
|
||||
RegisterSystem<IStateMachineSystem>(stateMachine);
|
||||
|
||||
Console.WriteLine("状态机系统初始化完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 创建 `StateMachineSystem` 实例
|
||||
- 使用链式调用注册所有状态
|
||||
- 将状态机注册为 `IStateMachineSystem` 服务
|
||||
- 状态会自动获得架构上下文
|
||||
|
||||
## 步骤 4:创建游戏控制器
|
||||
|
||||
创建控制器来管理游戏流程和状态切换。
|
||||
|
||||
```csharp
|
||||
using GFramework.Core.Abstractions.architecture;
|
||||
using GFramework.Core.Abstractions.controller;
|
||||
using GFramework.Core.Abstractions.state;
|
||||
using GFramework.Core.extensions;
|
||||
using MyGame.States;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MyGame.Controllers
|
||||
{
|
||||
public class GameFlowController : IController
|
||||
{
|
||||
public IArchitecture GetArchitecture() => GameArchitecture.Interface;
|
||||
|
||||
/// <summary>
|
||||
/// 开始游戏
|
||||
/// </summary>
|
||||
public async Task StartGame(int level = 1)
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
|
||||
// 设置加载状态的目标关卡
|
||||
var loadingState = stateMachine.GetState<LoadingState>();
|
||||
if (loadingState != null)
|
||||
{
|
||||
loadingState.TargetLevel = level;
|
||||
}
|
||||
|
||||
// 切换到加载状态
|
||||
var success = await stateMachine.ChangeToAsync<LoadingState>();
|
||||
|
||||
if (!success)
|
||||
{
|
||||
Console.WriteLine("无法开始游戏:状态切换失败");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停游戏
|
||||
/// </summary>
|
||||
public async Task PauseGame()
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
|
||||
// 检查当前是否在游戏状态
|
||||
if (stateMachine.Current is not GameplayState)
|
||||
{
|
||||
Console.WriteLine("当前不在游戏中,无法暂停");
|
||||
return;
|
||||
}
|
||||
|
||||
// 切换到暂停状态
|
||||
await stateMachine.ChangeToAsync<PauseState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复游戏
|
||||
/// </summary>
|
||||
public async Task ResumeGame()
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
|
||||
// 检查当前是否在暂停状态
|
||||
if (stateMachine.Current is not PauseState)
|
||||
{
|
||||
Console.WriteLine("当前不在暂停状态");
|
||||
return;
|
||||
}
|
||||
|
||||
// 返回游戏状态
|
||||
await stateMachine.ChangeToAsync<GameplayState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏结束
|
||||
/// </summary>
|
||||
public async Task EndGame(bool isVictory)
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
|
||||
// 设置游戏结束状态的数据
|
||||
var gameOverState = stateMachine.GetState<GameOverState>();
|
||||
if (gameOverState != null)
|
||||
{
|
||||
gameOverState.IsVictory = isVictory;
|
||||
}
|
||||
|
||||
// 切换到游戏结束状态
|
||||
await stateMachine.ChangeToAsync<GameOverState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回主菜单
|
||||
/// </summary>
|
||||
public async Task ReturnToMenu()
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
|
||||
// 切换到菜单状态
|
||||
var success = await stateMachine.ChangeToAsync<MenuState>();
|
||||
|
||||
if (!success)
|
||||
{
|
||||
Console.WriteLine("无法返回菜单:状态切换被拒绝");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示当前状态
|
||||
/// </summary>
|
||||
public void ShowCurrentState()
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
var currentState = stateMachine.Current;
|
||||
|
||||
if (currentState != null)
|
||||
{
|
||||
Console.WriteLine($"当前状态: {currentState.GetType().Name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("当前没有活动状态");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示状态历史
|
||||
/// </summary>
|
||||
public void ShowStateHistory()
|
||||
{
|
||||
var stateMachine = this.GetSystem<IStateMachineSystem>();
|
||||
var history = stateMachine.GetStateHistory();
|
||||
|
||||
Console.WriteLine("状态历史:");
|
||||
foreach (var state in history)
|
||||
{
|
||||
Console.WriteLine($" - {state.GetType().Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 实现 `IController` 接口
|
||||
- 提供各种游戏流程控制方法
|
||||
- 使用 `GetState<T>()` 获取状态实例并设置数据
|
||||
- 使用 `ChangeToAsync<T>()` 切换状态
|
||||
- 检查切换结果并处理失败情况
|
||||
|
||||
## 步骤 5:测试状态机
|
||||
|
||||
编写测试代码验证状态机功能。
|
||||
|
||||
```csharp
|
||||
using MyGame;
|
||||
using MyGame.Controllers;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
class Program
|
||||
{
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("=== 状态机系统测试 ===\n");
|
||||
|
||||
// 1. 初始化架构
|
||||
var architecture = new GameArchitecture();
|
||||
architecture.Initialize();
|
||||
await architecture.WaitUntilReadyAsync();
|
||||
|
||||
// 2. 创建游戏流程控制器
|
||||
var gameFlow = new GameFlowController();
|
||||
|
||||
// 3. 切换到主菜单
|
||||
Console.WriteLine("\n--- 测试 1: 进入主菜单 ---");
|
||||
await gameFlow.ReturnToMenu();
|
||||
gameFlow.ShowCurrentState();
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 4. 开始游戏(会经过加载状态)
|
||||
Console.WriteLine("\n--- 测试 2: 开始游戏 ---");
|
||||
await gameFlow.StartGame(level: 1);
|
||||
await Task.Delay(2000); // 等待加载完成
|
||||
gameFlow.ShowCurrentState();
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 5. 暂停游戏
|
||||
Console.WriteLine("\n--- 测试 3: 暂停游戏 ---");
|
||||
await gameFlow.PauseGame();
|
||||
gameFlow.ShowCurrentState();
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 6. 恢复游戏
|
||||
Console.WriteLine("\n--- 测试 4: 恢复游戏 ---");
|
||||
await gameFlow.ResumeGame();
|
||||
gameFlow.ShowCurrentState();
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 7. 游戏胜利
|
||||
Console.WriteLine("\n--- 测试 5: 游戏胜利 ---");
|
||||
await gameFlow.EndGame(isVictory: true);
|
||||
gameFlow.ShowCurrentState();
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
// 8. 返回菜单
|
||||
Console.WriteLine("\n--- 测试 6: 返回菜单 ---");
|
||||
await gameFlow.ReturnToMenu();
|
||||
gameFlow.ShowCurrentState();
|
||||
|
||||
// 9. 显示状态历史
|
||||
Console.WriteLine("\n--- 状态历史 ---");
|
||||
gameFlow.ShowStateHistory();
|
||||
|
||||
Console.WriteLine("\n=== 测试完成 ===");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代码说明**:
|
||||
|
||||
- 初始化架构并等待就绪
|
||||
- 按顺序测试各种状态切换
|
||||
- 使用 `Task.Delay` 模拟游戏运行
|
||||
- 验证状态转换规则和历史记录
|
||||
|
||||
## 完整代码
|
||||
|
||||
所有代码文件已在上述步骤中提供。项目结构如下:
|
||||
|
||||
```
|
||||
MyGame/
|
||||
├── States/
|
||||
│ ├── MenuState.cs
|
||||
│ ├── LoadingState.cs
|
||||
│ ├── GameplayState.cs
|
||||
│ ├── PauseState.cs
|
||||
│ └── GameOverState.cs
|
||||
├── Controllers/
|
||||
│ └── GameFlowController.cs
|
||||
├── GameArchitecture.cs
|
||||
└── Program.cs
|
||||
```
|
||||
|
||||
## 运行结果
|
||||
|
||||
运行程序后,你将看到类似以下的输出:
|
||||
|
||||
```
|
||||
=== 状态机系统测试 ===
|
||||
|
||||
状态机系统初始化完成
|
||||
|
||||
--- 测试 1: 进入主菜单 ---
|
||||
=== 进入主菜单 ===
|
||||
显示菜单界面
|
||||
播放菜单音乐
|
||||
当前状态: MenuState
|
||||
|
||||
--- 测试 2: 开始游戏 ---
|
||||
退出主菜单
|
||||
隐藏菜单界面
|
||||
=== 开始加载关卡 1 ===
|
||||
显示加载界面
|
||||
加载纹理资源...
|
||||
纹理加载完成 (33%)
|
||||
加载音频资源...
|
||||
音频加载完成 (66%)
|
||||
加载关卡数据...
|
||||
关卡数据加载完成 (100%)
|
||||
加载完成,进入游戏
|
||||
退出加载状态
|
||||
隐藏加载界面
|
||||
=== 开始游戏 ===
|
||||
初始化游戏场景
|
||||
重置玩家数据
|
||||
播放游戏音乐
|
||||
当前状态: GameplayState
|
||||
|
||||
--- 测试 3: 暂停游戏 ---
|
||||
结束游戏
|
||||
保存游戏进度
|
||||
=== 游戏暂停 ===
|
||||
显示暂停菜单
|
||||
暂停游戏逻辑
|
||||
当前状态: PauseState
|
||||
|
||||
--- 测试 4: 恢复游戏 ---
|
||||
取消暂停
|
||||
隐藏暂停菜单
|
||||
恢复游戏逻辑
|
||||
=== 开始游戏 ===
|
||||
初始化游戏场景
|
||||
重置玩家数据
|
||||
播放游戏音乐
|
||||
当前状态: GameplayState
|
||||
|
||||
--- 测试 5: 游戏胜利 ---
|
||||
结束游戏
|
||||
=== 游戏胜利 ===
|
||||
显示结算界面
|
||||
播放结算音乐
|
||||
当前状态: GameOverState
|
||||
|
||||
--- 测试 6: 返回菜单 ---
|
||||
退出结算界面
|
||||
隐藏结算界面
|
||||
=== 进入主菜单 ===
|
||||
显示菜单界面
|
||||
播放菜单音乐
|
||||
当前状态: MenuState
|
||||
|
||||
--- 状态历史 ---
|
||||
状态历史:
|
||||
- MenuState
|
||||
- LoadingState
|
||||
- GameplayState
|
||||
- PauseState
|
||||
- GameplayState
|
||||
- GameOverState
|
||||
- MenuState
|
||||
|
||||
=== 测试完成 ===
|
||||
```
|
||||
|
||||
**验证步骤**:
|
||||
|
||||
1. 所有状态切换成功执行
|
||||
2. 状态转换规则正确生效
|
||||
3. 异步加载状态正常工作
|
||||
4. 状态历史记录完整
|
||||
5. 状态数据传递正确
|
||||
|
||||
## 下一步
|
||||
|
||||
恭喜!你已经掌握了状态机系统的使用。接下来可以学习:
|
||||
|
||||
- [使用协程系统](/zh-CN/tutorials/coroutine-tutorial) - 在状态中使用协程
|
||||
- [资源管理最佳实践](/zh-CN/tutorials/resource-management) - 在加载状态中管理资源
|
||||
- [实现存档系统](/zh-CN/tutorials/save-system) - 保存和恢复游戏状态
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [状态机系统](/zh-CN/core/state-machine) - 状态机详细说明
|
||||
- [生命周期管理](/zh-CN/core/lifecycle) - 组件生命周期
|
||||
- [System 层](/zh-CN/core/system) - System 详细说明
|
||||
- [架构组件](/zh-CN/core/architecture) - 架构基础
|
||||
1147
docs/zh-CN/tutorials/unit-testing.md
Normal file
1147
docs/zh-CN/tutorials/unit-testing.md
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user