448 lines
12 KiB
Markdown
448 lines
12 KiB
Markdown
# FlowScope Game Core 设计文档
|
||
|
||
## 定位
|
||
|
||
面向休闲/超休闲游戏的 Unity 客户端通用框架。业务逻辑层保持纯 C#,基础设施层直接使用 Unity API。
|
||
|
||
## 整体架构
|
||
|
||
```
|
||
FlowScope Game Core
|
||
│
|
||
├── 基础设施层
|
||
│ ├── Container — 自研轻量 DI(scope/resolve/dispose,无反射)
|
||
│ └── R3 — ReactiveProperty、事件流、集合订阅
|
||
│
|
||
├── 核心服务层(全部接口化,实现可替换)
|
||
│ ├── IResourceService — 资源加载/引用计数/分组释放
|
||
│ ├── ISaveService — 存档/读档持久化
|
||
│ ├── IConfigProvider — 配置表加载/查询
|
||
│ ├── IAudioService — BGM/SFX 池化/音量控制
|
||
│ ├── INetworkService — 网络请求/推送
|
||
│ └── UIManager — MVVM Panel 生命周期/页面栈/层级
|
||
│
|
||
├── 数据层(按业务域拆分,ReactiveProperty 驱动)
|
||
│ └── 各游戏按需定义自己的 Data 类
|
||
│
|
||
├── 业务特性层(自包含,独立生命周期)
|
||
│ └── 各 Feature: Load → Enter → Exit → Dispose
|
||
│
|
||
└── 生命周期编排
|
||
└── GameFlow — 启动/切换 Feature/关闭的全局时间线
|
||
```
|
||
|
||
## 设计原则
|
||
|
||
1. 接口化:核心服务全部走接口,实现可替换
|
||
2. 显式优于隐式:DI 不做反射,手动注册手动传参
|
||
3. 按域拆分:数据类按业务域拆分,不建大 Data 类
|
||
4. 特性自包含:每个 Feature 自带资源组、订阅、ViewModel,切换时干净释放
|
||
5. 框架只提供工具:不预设具体 Data 类,游戏自己定义
|
||
|
||
## 依赖方向(单向,不反向)
|
||
|
||
```
|
||
Feature → Core Services → Interfaces → Infrastructure (Container + R3)
|
||
↓
|
||
Data Classes(纯 C#,无依赖)
|
||
```
|
||
|
||
---
|
||
|
||
## 基础设施层
|
||
|
||
### Container — 自研轻量 DI
|
||
|
||
不做反射、不做构造函数自动注入、不做标签注入。显式注册,显式解析。
|
||
|
||
```csharp
|
||
public class Container : IDisposable
|
||
{
|
||
void RegisterInstance<T>(T instance);
|
||
void RegisterFactory<T>(Func<T> factory);
|
||
T Resolve<T>();
|
||
bool TryResolve<T>(out T value);
|
||
Container CreateScope(); // 子 scope 可访问父级,可覆盖父级
|
||
void Dispose(); // 释放 scope 内的 IDisposable
|
||
}
|
||
```
|
||
|
||
注册方式:
|
||
|
||
```csharp
|
||
container.RegisterInstance<IConfig>(config);
|
||
container.RegisterFactory<ISaveService>(() => new FileSaveService());
|
||
```
|
||
|
||
### R3
|
||
|
||
直接使用 R3 库提供:
|
||
|
||
- ReactiveProperty — 可观察属性,MVVM 绑定基石
|
||
- ReactiveCollection — 可观察列表
|
||
- Subject — 事件流
|
||
- Observable.Timer / EveryUpdate — 计时和帧调度
|
||
- CompositeDisposable — 批量释放订阅
|
||
|
||
---
|
||
|
||
## 核心服务层
|
||
|
||
### IResourceService — 资源管理
|
||
|
||
```csharp
|
||
public interface IResourceService
|
||
{
|
||
Task<IResourceHandle<T>> LoadAsync<T>(string key) where T : class;
|
||
IResourceGroup CreateGroup();
|
||
}
|
||
|
||
public interface IResourceHandle<T> : IDisposable
|
||
{
|
||
T Asset { get; }
|
||
}
|
||
|
||
public interface IResourceGroup : IDisposable
|
||
{
|
||
void Add<T>(IResourceHandle<T> handle);
|
||
}
|
||
```
|
||
|
||
引用计数机制:同一 key 多次 Load 只加载一次,最后一个 handle Dispose 时才真正卸载。
|
||
|
||
默认实现:AddressablesResourceService。
|
||
|
||
### ISaveService — 持久化
|
||
|
||
```csharp
|
||
public interface ISaveService
|
||
{
|
||
Task SaveAsync<T>(string key, T data);
|
||
Task<T> LoadAsync<T>(string key, T defaultValue = default);
|
||
void Delete(string key);
|
||
}
|
||
```
|
||
|
||
默认实现可选 FileSaveService(JSON 文件)或 PlayerPrefsSaveService。
|
||
|
||
### IConfigProvider — 配置管理
|
||
|
||
```csharp
|
||
public interface IConfigRow
|
||
{
|
||
int Id { get; }
|
||
}
|
||
|
||
public interface IConfigProvider
|
||
{
|
||
Task LoadAllAsync();
|
||
T Get<T>(int id) where T : IConfigRow;
|
||
IReadOnlyList<T> GetAll<T>() where T : IConfigRow;
|
||
}
|
||
```
|
||
|
||
不绑定数据源(JSON / CSV / Luban),由实现决定。默认 JsonConfigProvider。
|
||
|
||
### IAudioService — 音频管理
|
||
|
||
```csharp
|
||
public interface IAudioService
|
||
{
|
||
void PlayBgm(string key, bool loop = true);
|
||
void StopBgm(float fadeOut = 0.5f);
|
||
void PlaySfx(string key);
|
||
void SetBgmVolume(float volume);
|
||
void SetSfxVolume(float volume);
|
||
void SetMute(bool mute);
|
||
}
|
||
```
|
||
|
||
内部:AudioSource 对象池,BGM 单独一个,SFX 池化复用。
|
||
|
||
### INetworkService — 网络管理
|
||
|
||
```csharp
|
||
public interface INetworkService
|
||
{
|
||
Task<TResponse> SendAsync<TRequest, TResponse>(string path, TRequest data);
|
||
void On<TEvent>(Action<TEvent> handler);
|
||
}
|
||
```
|
||
|
||
初期可能用不上,但留好接口。默认 HttpNetworkService。
|
||
|
||
### UIManager — UI 管理
|
||
|
||
MVVM 模式的 Panel 生命周期管理。
|
||
|
||
```csharp
|
||
public class UIManager
|
||
{
|
||
Task OpenAsync<TPanel, TViewModel>(TViewModel viewModel) where TPanel : IPanel;
|
||
void Close<TPanel>();
|
||
void CloseAll();
|
||
}
|
||
|
||
public interface IPanel
|
||
{
|
||
void Bind(object viewModel);
|
||
void Unbind();
|
||
}
|
||
```
|
||
|
||
Panel 生命周期:Instantiate → Bind → Show → ... → Hide → Unbind → Destroy。
|
||
|
||
层级管理:Bottom(基础层)、Middle(弹窗层)、Top(提示层)。
|
||
|
||
ViewModel 示例(纯 C#):
|
||
|
||
```csharp
|
||
public class ShopViewModel
|
||
{
|
||
public ReactiveProperty<string> Title { get; } = new("商店");
|
||
public ReactiveCollection<ShopItemViewModel> Items { get; } = new();
|
||
|
||
private readonly PlayerData _player;
|
||
private readonly IConfigProvider _config;
|
||
|
||
public void BuyItem(int itemId)
|
||
{
|
||
var config = _config.Get<ItemConfig>(itemId);
|
||
if (_player.Gold.Value < config.Price) return;
|
||
_player.Gold.Value -= config.Price;
|
||
}
|
||
}
|
||
```
|
||
|
||
Panel 示例(Unity MonoBehaviour):
|
||
|
||
```csharp
|
||
public class ShopPanel : MonoBehaviour, IPanel
|
||
{
|
||
[SerializeField] private Text titleText;
|
||
[SerializeField] private Transform itemListRoot;
|
||
|
||
private ShopViewModel _vm;
|
||
private CompositeDisposable _disposables = new();
|
||
|
||
public void Bind(object viewModel)
|
||
{
|
||
_vm = (ShopViewModel)viewModel;
|
||
_vm.Title.Subscribe(t => titleText.text = t).AddTo(_disposables);
|
||
_vm.Items.ObserveAdd().Subscribe(e => CreateItemCell(e.Value)).AddTo(_disposables);
|
||
}
|
||
|
||
public void Unbind()
|
||
{
|
||
_disposables.Clear();
|
||
_vm = null;
|
||
}
|
||
|
||
private void OnDestroy() => Unbind();
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 数据层
|
||
|
||
框架不提供任何具体 Data 类,只提供 ReactiveProperty / ReactiveCollection 工具。
|
||
|
||
每个游戏按业务域自己定义:
|
||
|
||
```csharp
|
||
// 游戏需要金币?建一个
|
||
public class PlayerData
|
||
{
|
||
public ReactiveProperty<int> Level { get; } = new(1);
|
||
public ReactiveProperty<int> Gold { get; } = new(0);
|
||
}
|
||
|
||
// 不需要背包?就不建 InventoryData
|
||
// 需要设置?按需加
|
||
public class SettingsData
|
||
{
|
||
public ReactiveProperty<float> BgmVolume { get; } = new(1f);
|
||
public ReactiveProperty<string> Language { get; } = new("zh");
|
||
}
|
||
```
|
||
|
||
原则:一个 Data 类 = 一个业务域,新业务加新类。
|
||
|
||
---
|
||
|
||
## 业务特性层
|
||
|
||
### IFeature
|
||
|
||
```csharp
|
||
public interface IFeature : IDisposable
|
||
{
|
||
Task LoadAsync(); // 加载资源、创建 ViewModel
|
||
Task EnterAsync(); // 显示 UI、订阅事件
|
||
Task ExitAsync(); // 隐藏 UI、取消订阅
|
||
void Dispose(); // 释放资源
|
||
}
|
||
```
|
||
|
||
每个 Feature 自包含:自己的资源组、订阅、ViewModel。切换时干净释放。
|
||
|
||
Feature 示例:
|
||
|
||
```csharp
|
||
public class ShopFeature : IFeature
|
||
{
|
||
private readonly IResourceService _resource;
|
||
private readonly UIManager _uiManager;
|
||
private readonly PlayerData _player;
|
||
private readonly IConfigProvider _config;
|
||
|
||
private IResourceGroup _resources;
|
||
private CompositeDisposable _disposables;
|
||
private ShopViewModel _viewModel;
|
||
|
||
public async Task LoadAsync()
|
||
{
|
||
_resources = _resource.CreateGroup();
|
||
var bgm = await _resource.LoadHandleAsync<AudioClip>("audio/bgm_shop");
|
||
_resources.Add(bgm);
|
||
_viewModel = new ShopViewModel(_player, _config);
|
||
}
|
||
|
||
public async Task EnterAsync()
|
||
{
|
||
_disposables = new CompositeDisposable();
|
||
await _uiManager.OpenAsync<ShopPanel, ShopViewModel>(_viewModel);
|
||
}
|
||
|
||
public async Task ExitAsync()
|
||
{
|
||
_uiManager.Close<ShopPanel>();
|
||
_disposables?.Dispose();
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
_resources?.Dispose();
|
||
_viewModel = null;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 生命周期编排
|
||
|
||
### GameFlow
|
||
|
||
```csharp
|
||
public class GameFlow
|
||
{
|
||
private readonly Container _container;
|
||
private IFeature _currentFeature;
|
||
|
||
public async Task StartupAsync()
|
||
{
|
||
var config = _container.Resolve<IConfigProvider>();
|
||
await config.LoadAllAsync();
|
||
|
||
var save = _container.Resolve<ISaveService>();
|
||
var player = await save.LoadAsync<PlayerData>("player", new PlayerData());
|
||
_container.RegisterInstance(player);
|
||
|
||
await SwitchToAsync<MainMenuFeature>();
|
||
}
|
||
|
||
public async Task SwitchToAsync<TFeature>() where TFeature : IFeature, new()
|
||
{
|
||
if (_currentFeature != null)
|
||
{
|
||
await _currentFeature.ExitAsync();
|
||
_currentFeature.Dispose();
|
||
}
|
||
|
||
var feature = new TFeature();
|
||
await feature.LoadAsync();
|
||
await feature.EnterAsync();
|
||
_currentFeature = feature;
|
||
}
|
||
|
||
public async Task ShutdownAsync()
|
||
{
|
||
if (_currentFeature != null)
|
||
{
|
||
await _currentFeature.ExitAsync();
|
||
_currentFeature.Dispose();
|
||
}
|
||
|
||
var save = _container.Resolve<ISaveService>();
|
||
await save.SaveAsync("player", _container.Resolve<PlayerData>());
|
||
}
|
||
}
|
||
```
|
||
|
||
### 启动入口
|
||
|
||
```csharp
|
||
public class GameBootstrap : MonoBehaviour
|
||
{
|
||
private void Awake()
|
||
{
|
||
var container = new Container();
|
||
|
||
// 核心服务
|
||
container.RegisterInstance<IResourceService>(new AddressablesResourceService());
|
||
container.RegisterInstance<ISaveService>(new FileSaveService());
|
||
container.RegisterInstance<IConfigProvider>(new JsonConfigProvider());
|
||
container.RegisterInstance<IAudioService>(new UnityAudioService());
|
||
container.RegisterInstance<UIManager>(new UIManager());
|
||
|
||
// 游戏数据
|
||
container.RegisterInstance(new PlayerData());
|
||
container.RegisterInstance(new SettingsData());
|
||
|
||
var flow = new GameFlow(container);
|
||
flow.StartupAsync().Forget();
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 模块总表
|
||
|
||
| 模块 | 职责 | 类型 |
|
||
|------|------|------|
|
||
| Container | 轻量 DI,scope/resolve/dispose | 自研 |
|
||
| R3 | ReactiveProperty、事件流、调度 | 第三方库 |
|
||
| IResourceService | 资源加载、引用计数、分组释放 | 接口 |
|
||
| ISaveService | 存档/读档持久化 | 接口 |
|
||
| IConfigProvider | 配置表加载/查询 | 接口 |
|
||
| IAudioService | BGM/SFX 池化、音量控制 | 接口 |
|
||
| INetworkService | 网络请求/推送 | 接口 |
|
||
| UIManager | MVVM Panel 生命周期、页面栈、层级 | 类 |
|
||
| Data 类 | 按业务域拆分的运行时状态 | 游戏自定义 |
|
||
| IFeature | 自包含特性模块,独立生命周期 | 接口 |
|
||
| GameFlow | 全局启动/切换/关闭时间线 | 类 |
|
||
|
||
## 技术选型
|
||
|
||
| 选型 | 决定 | 理由 |
|
||
|------|------|------|
|
||
| DI 容器 | 自研 | 休闲游戏只需显式注册/解析/scope,不需要反射注入 |
|
||
| 响应式 | R3 | 成熟的 Unity 响应式库,自带 ObservableProperty/Collection |
|
||
| 异步 | C# Task + async/await | 休闲游戏的 GC 开销可忽略,不需要 UniTask 的零 GC |
|
||
| 资源加载 | Addressables | Unity 标准,通过 IResourceService 接口可替换 |
|
||
| 配置数据 | JSON(默认) | 通过 IConfigProvider 接口可换 Luban |
|
||
| UI | uGUI + MVVM | BindableProperty 驱动,ViewModel 纯 C# |
|
||
|
||
## 不做的事
|
||
|
||
- 不做反射式 DI / 自动注入
|
||
- 不做完整的 ORM
|
||
- 不做 UI 自动绑定
|
||
- 不做热更新框架
|
||
- 不做编辑器工具
|
||
- 不预设具体业务 Data 类
|
||
- 不依赖 UniTask(C# Task 够用)
|