741 lines
39 KiB
Markdown
741 lines
39 KiB
Markdown
# Design: Port ROK Hero & Bag System
|
||
|
||
## Context
|
||
|
||
工程 `Survivors` 基于 OctoberStudio Unity 模板(单机肉鸽),现在要引入 ROK 那种重养成 SLG 玩法(英雄+背包+装备)。已有的技术栈:
|
||
|
||
- **客户端**:ET 框架(Entity/Component/System)+ YIUI UI 框架 + HybridCLR 热更 + YooAsset 资源 + ExcelExporter 配置
|
||
- **服务端**:ET Server(dotnet)+ 已有 Login Demo 全链路 + MongoDB Driver(未启用业务)
|
||
- **协议**:protobuf3 + MemoryPack 序列化
|
||
- **已就绪**:cn.etetet.login 登录链路、cn.etetet.proto 代码生成、cn.etetet.yiuiloopscrollrectasync 循环列表
|
||
|
||
**约束**:
|
||
1. ROK 源码用了 PureMVC + sproto + ILRuntime/IFix,**完全不能直接复制**
|
||
2. 协议层必须用 ET protobuf 重新定义
|
||
3. UI 必须用 YIUI 重写
|
||
4. 服务端必须用 ET Entity/Component/System 重写
|
||
5. ROK Server 是 C++/Lua(9 个微服务),与 ET 协议不兼容,**放弃对接**
|
||
6. OctoberStudio 现有 `SaveManager` 本地存档系统与 ET 服务端并存
|
||
|
||
**驱动者**:单机肉鸽 + SLG 卡牌养成的混合体验需要"局外永久成长"沉淀玩家,背包/英雄是核心载体。
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
|
||
1. 借鉴 ROK 业务设计(数据模型/Config 字段/UI 流程/服务端 RPC 列表),用 ET + YIUI 重新实现一套完整的英雄+背包+装备系统
|
||
2. 客户端-服务端真权威架构(防作弊),所有养成操作服务端校验,DB 持久化
|
||
3. 与现有 OctoberStudio 流程无缝衔接(启动顺序、场景切换、UI 风格)
|
||
4. 分阶段可交付(先英雄基础养成 → 装备 → 天赋),每个阶段独立可用
|
||
5. 协议、配置、UI 三套自动化工具链完整跑通
|
||
|
||
**Non-Goals:**
|
||
|
||
1. **不**对接 ROK Server(C++/Lua),ET Server 是新写的真权威
|
||
2. **不**搬运 ROK 的美术资源(图集/Spine/Sound),只搬数据结构和流程
|
||
3. **不**做 ROK 的"雕像兑换"/"天赋页改名"/"建造锻造"等次要功能(一期不做)
|
||
4. **不**修改 OctoberStudio 局内战斗代码(先把养成搭好,后续接入"出战英雄"逻辑)
|
||
5. **不**实现服务端集群(玩家数量小时单进程足够,未来再分布式)
|
||
|
||
## Decisions
|
||
|
||
### D1: 服务端 vs 客户端权威架构
|
||
|
||
**选择**:**服务端真权威 + 客户端缓存 + 推送同步**
|
||
|
||
**为什么**:
|
||
- 防作弊是 SLG 卡牌养成的基本盘
|
||
- 客户端只展示+操作,所有数值/规则在服务端校验
|
||
- ROK 设计就是这套,移植成本低
|
||
|
||
**备选**:
|
||
- 纯客户端单机(无服务器)→ 后续做联网/跨设备同步会非常痛苦
|
||
- 纯无状态服务器(每请求都查 DB)→ DB 压力大、响应慢
|
||
|
||
**取舍**:客户端有少量延迟(操作 → 服务端 → 推送回执 → UI 刷新),但接受。
|
||
|
||
### D2: 业务包组织方式
|
||
|
||
**选择**:**新增 3 个 cn.etetet.* 业务包**:
|
||
- `cn.etetet.hero` - 英雄系统(含装备穿戴关联)
|
||
- `cn.etetet.bag` - 背包+装备(含锻造合成)
|
||
- `cn.etetet.gacha` - 抽卡系统(依赖 hero + bag)
|
||
|
||
每个包内部按 ET 标准目录结构:
|
||
```
|
||
cn.etetet.hero/
|
||
├── package.json
|
||
├── Scripts/
|
||
│ ├── Model/Share/ # HeroInfo / SkillInfo 等 DTO
|
||
│ ├── Model/Client/ # 客户端缓存 Component
|
||
│ ├── Model/Server/ # 服务端权威 Component
|
||
│ ├── Hotfix/Client/ # 客户端 Handler + System
|
||
│ └── Hotfix/Server/ # 服务端 Handler + System
|
||
├── Proto/HeroOuter_C_3000.proto
|
||
└── ModelView/Client/YIUIComponent + YIUIGen # YIUI UI 代码(由工具生成)
|
||
```
|
||
|
||
**为什么不放在 Assets/**:
|
||
- ET 包结构方便复用/导出/版本控制
|
||
- YooAsset 收集规则已默认 `Packages/cn.etetet.*` 路径
|
||
- Asmdef 隔离编译速度更快
|
||
|
||
**备选**:
|
||
- 全部塞进现有 `cn.etetet.statesync` 包 → 包变巨大,不清晰
|
||
- 放在 `Assets/Scripts/` → 编译慢、与 ET 框架代码分离
|
||
|
||
### D3: Hero 实体在 ET 中的建模
|
||
|
||
**选择**:每个英雄一个 `Hero` Entity,挂在 `Player.GetComponent<HeroComponent>()` 下作为子 Entity。
|
||
|
||
```
|
||
Player (Entity, _id = playerId)
|
||
├── HeroComponent
|
||
│ ├── Hero (Entity, _id = heroEntityId, ChildOf = HeroComponent)
|
||
│ │ ├── HeroSkillComponent # 技能等级数组
|
||
│ │ ├── HeroTalentComponent # 3 套天赋页
|
||
│ │ └── HeroEquipComponent # 8 个装备槽
|
||
│ └── Hero (Entity, _id = ...)
|
||
└── BagComponent
|
||
└── ItemUnit (Entity, _id = itemIndex)
|
||
```
|
||
|
||
**为什么用 Entity 而不是 POCO 类**:
|
||
- 自动支持 BSON 序列化(写 DB 容易)
|
||
- 子 Entity 自动有 EntityId(用作 itemIndex)
|
||
- ET System 自动绑定(Awake/Destroy/Update 等生命周期)
|
||
- 跟 ET 框架风格统一
|
||
|
||
**备选**:
|
||
- POCO `Dictionary<long, HeroData>` → 简单,但失去 ET ECS 优势、序列化麻烦
|
||
- 一个 Hero 拆多个 Entity(按子系统) → 过度设计,性能没必要
|
||
|
||
### D4: 协议 ID 段与 proto 文件划分
|
||
|
||
**选择**:占用 3000-3999 段,分三个文件:
|
||
- `HeroOuter_C_3000.proto` - 英雄业务(含装备穿脱)+ DTO
|
||
- `BagOuter_C_3100.proto` - 背包业务(含锻造合成)
|
||
- `GachaOuter_C_3200.proto` - 抽卡业务
|
||
|
||
| ID 范围 | 用途 |
|
||
|---|---|
|
||
| 3000-3099 | Hero RPC + 推送 + DTO |
|
||
| 3100-3199 | Bag/Equipment RPC + 推送 + DTO |
|
||
| 3200-3299 | Gacha RPC + 推送 + DTO |
|
||
|
||
**为什么不放一起**:
|
||
- 文件太大不好维护
|
||
- 业务模块独立,便于将来拆解
|
||
|
||
**ET 框架要求**:每个 proto 文件起始 ID 用 `OpcodeRangeStart` 控制,工具自动按顺序分配。
|
||
|
||
### D4.1: 通用 Item 设计原则
|
||
|
||
**选择**:**所有游戏内可获得的资源统一为 Item**——金币、钻石、英雄碎片、装备、材料、经验书、升星石、觉醒符文、礼包、邮件附件,全部用同一个 `ItemConfig` + 一个 `itemId` 管理。
|
||
|
||
**核心约定**:
|
||
|
||
```protobuf
|
||
// 全游戏通用,凡是"奖励"列表都用它
|
||
message RewardEntry {
|
||
int32 itemId = 1;
|
||
int64 count = 2;
|
||
}
|
||
|
||
// 与 RewardEntry 结构相同,只是语义不同(消耗)
|
||
message CostEntry {
|
||
int32 itemId = 1;
|
||
int64 count = 2;
|
||
}
|
||
```
|
||
|
||
```csharp
|
||
// 所有发奖/扣资源 MUST 走这两个统一接口
|
||
public static class BagComponentSystem
|
||
{
|
||
public static void AddItems(this BagComponent self, List<RewardEntry> rewards);
|
||
public static bool RemoveItems(this BagComponent self, List<CostEntry> costs); // 不足则全失败
|
||
public static long GetItemCount(this BagComponent self, int itemId);
|
||
}
|
||
```
|
||
|
||
**为什么**:
|
||
- **数据建模简化**:玩家身上不存"金币字段、钻石字段、英雄碎片字段……",只存一个 `BagComponent`,金币也是 BagComponent 里的一条 ItemInfo(itemId=1)
|
||
- **业务接口收敛**:抽卡发奖、升级扣钱、商店买卖、邮件附件、关卡奖励——全部用 `AddItems`/`RemoveItems`,业务代码大量减少
|
||
- **新增资源零成本**:新增"幸运碎片"只需要在 ItemConfig.xlsx 加一行,**不需要写任何代码**
|
||
- **UI 复用最大化**:`BagItemUICommon` 通过 `(itemId, count)` 渲染任何道具,所有面板都用它
|
||
|
||
**itemId 段位规划**:
|
||
| itemId 段 | 用途 |
|
||
|---|---|
|
||
| 1-99 | 货币(1=金币、2=钻石、3=PvP 币、…) |
|
||
| 100-999 | 通用道具(经验书、升星石、礼包) |
|
||
| 1000-9999 | 英雄碎片(itemId = heroBaseId + 1000) |
|
||
| 10000-19999 | 装备物品(itemId = equipBaseId) |
|
||
| 20000-29999 | 装备材料/图纸 |
|
||
| 30000+ | 时装、皮肤、其他 |
|
||
|
||
**反例(要避免)**:
|
||
- ❌ Player Entity 上加 `public long Gold; public long Diamond;` —— 违反通用原则
|
||
- ❌ 抽卡系统自己直接操作 BagComponent 的内部字典 —— 应该调 AddItems
|
||
- ❌ 不同业务自己实现"扣钱"逻辑 —— 必须走 RemoveItems
|
||
|
||
**取舍**:金币/钻石这种高频访问的字段查询会比独立字段稍慢(多一次字典查找),但可接受。如真有性能问题,可在 BagComponent 上加 `Dictionary<int, long> _quickCache` 缓存常用 itemId 的数量。
|
||
|
||
### D5: 配置表方案 - Luban(替代 ExcelExporter)
|
||
|
||
**选择**:**引入 Luban**(新业务表全部用 Luban;OctoberStudio 老配置保持不变)
|
||
|
||
**为什么改变方向**:
|
||
- 本次新增的 14+ 张表大量使用 `List<RewardEntry>`、`Map<int, List<WeightItem>>`、bean 继承、外键引用,**ExcelExporter 表达力不足**
|
||
- 抽卡权重表、技能效果表、天赋树前置依赖在 Luban 里是一等公民,在 ExcelExporter 里要拆多列/塞 JSON 字符串
|
||
- Luban 编译期校验外键,**消灭运行时 NullReference**
|
||
- 后续商店/邮件/活动/多语言配置膨胀时,Luban 优势越来越大
|
||
|
||
**不写自定义模板,用原生 API + 一层 Component 包装**:
|
||
|
||
```csharp
|
||
// Luban 原生生成(零自定义模板)
|
||
public partial class Tables
|
||
{
|
||
public TbHero TbHero { get; }
|
||
public TbItem TbItem { get; }
|
||
public TbGachaPool TbGachaPool { get; }
|
||
public TbGachaWeight TbGachaWeight { get; }
|
||
// ... 自动生成
|
||
}
|
||
|
||
// ET 集成(挂在 Scene 上)
|
||
[ComponentOf(typeof(Scene))]
|
||
public class ConfigComponent : Entity, IAwake
|
||
{
|
||
public cfg.Tables Tables;
|
||
}
|
||
|
||
public static class ConfigComponentSystem
|
||
{
|
||
[EntitySystem]
|
||
private static void Awake(this ConfigComponent self)
|
||
{
|
||
self.Tables = new cfg.Tables(name =>
|
||
JsonConfigLoader.Load($"Config/{name}.json"));
|
||
}
|
||
}
|
||
|
||
// 业务访问
|
||
public static class SceneExtensions
|
||
{
|
||
public static cfg.Tables Cfg(this Scene self)
|
||
=> self.GetComponent<ConfigComponent>().Tables;
|
||
}
|
||
|
||
// 使用
|
||
var hero = scene.Cfg().TbHero.Get(10001);
|
||
var pool = scene.Cfg().TbGachaPool.Get(poolId);
|
||
```
|
||
|
||
**为什么不写自定义模板**:
|
||
- 模板维护成本高(升级 Luban 要同步改)
|
||
- 原生 API 已足够好用,多一个 `.Cfg().` 调用可接受
|
||
- 符合 ET ECS 哲学(配置作为 Scene 资源 Component)
|
||
|
||
**Luban 集成方式**:
|
||
- 用 dotnet luban 作为命令行工具,通过 `tools/luban/gen.ps1` 一键导出
|
||
- 输入:`Config/Datas/*.xlsx` + `Config/Defines/*.xml`
|
||
- 输出:客户端 `Bundles/Config/*.json` + `cn.etetet.config/Scripts/Model/Share/cfg/*.cs`,服务端共享同一份代码
|
||
- 客户端运行时通过 YooAsset 加载 json,服务端从 `Bin/Config/` 读取
|
||
|
||
**老配置不动**:OctoberStudio 现有零散表 + ET ExcelExporter 现有几张测试表保留,不强制迁移。**新业务(Hero/Bag/Equipment/Gacha)全部走 Luban**。
|
||
|
||
**ROK 配置数据迁移(不需要手输)**:
|
||
|
||
- **数据源真相**:ROK 工程**没有 Excel 源文件**。配置只有两份:
|
||
- 服务端:`E:/Game/gmd/ROK/Server/common/config/gen/Configs.data`(45 万行 Lua 表,`tabtoy` 生成)
|
||
- 客户端:`E:/Game/gmd/ROK/Client/Assets/Resources/.../*.db`(SqlCipher4Unity3D 加密 SQLite)+ 205 个 `*Define.cs` DTO
|
||
- **现成工具**:姐妹工程 `E:/Game/gmd/Tools/export_hero_json_from_configs_data.py` 已能解析 `Configs.data` 输出 JSON(已验证 Hero/HeroLevel/HeroStar 等表)
|
||
- **迁移路径**:
|
||
|
||
```
|
||
Configs.data (Lua) ROK Client *Define.cs
|
||
│ │
|
||
│ 扩展 export_*.py │ 扫描字段名 + 类型
|
||
│ ┌─────────┐ │ ┌─────────┐
|
||
▼ │ Python │ ▼ │ Roslyn │
|
||
raw_*.json ◄────────────────────── *.bean.xml (Luban Schema)
|
||
│
|
||
│ Python (raw_*.json → Luban *.xlsx 数据填充)
|
||
▼
|
||
Config/Datas/*.xlsx + Config/Defines/*.xml
|
||
│ dotnet luban
|
||
▼
|
||
Bundles/Config/*.json + cfg/*.cs
|
||
```
|
||
|
||
- **优势**:**配置内容不丢失** + **Schema 派生自 ROK Client DTO** + **零手工录入**
|
||
|
||
**风险与缓解**:
|
||
- [Luban 学习曲线] → P0 阶段做 1 张示例表(ItemConfig)跑通完整链路,文档化
|
||
- [Luban 与 HybridCLR 兼容] → Luban 生成的代码都是 partial class + 基础类型,无 AOT 元数据问题
|
||
- [json 文件 YooAsset 收集] → 在 `AssetBundleCollectorSetting` 加 `Config/` 路径规则
|
||
|
||
**备选**:
|
||
- 继续用 ExcelExporter → 复杂表难写,长期成本高
|
||
- 代码硬编码 → 数值改一次重编一次,不可接受
|
||
|
||
### D6: 服务端持久化策略 - **基于现成 MongoHelper 的业务封装**
|
||
|
||
**前置事实**:
|
||
- `cn.etetet.core/Scripts/Core/Share/Serialize/MongoHelper.cs` 已提供 BSON 序列化(ToJson/FromJson/Clone/CloneBytes/Register)
|
||
- `com.etetet.init/Plugins/MongoDB/*` 已自带 MongoDB.Driver 原生 DLL
|
||
- **但是没有**:`IDBComponent.Save<T>/Query<T>/Delete<T>` 业务封装、连接池、慢查询、重试
|
||
|
||
**结论**:**新建业务包 `cn.etetet.db` 包装这层**,不重写 BSON 也不引入新 ORM。
|
||
|
||
**选择**:**MongoDB + 脏标记 + 30 秒定时刷盘 + 离线立即刷盘**
|
||
|
||
**为什么 MongoDB**:
|
||
- ET 框架原生集成(已有 MongoDB.Driver DLL + MongoHelper)
|
||
- 文档型契合 Entity 结构(Hero、Bag 嵌套天然存为子文档)
|
||
- 索引、查询、复制集都有现成方案
|
||
|
||
**为什么脏标记 + 定时**:
|
||
- 每操作都直写 DB → 性能差、网络抖动会阻塞业务
|
||
- 全在内存 → 服务器崩了数据全丢
|
||
- 折中:操作改内存 + 标记 + 定时批量写
|
||
|
||
**风险与缓解**:
|
||
- [服务器突然崩溃 → 30 秒未刷盘数据丢失] → 关键操作(充值、付费)走"实时刷盘"通道
|
||
- [DB 写入失败 → 标记保留待重试] → 监控告警 + 自动重试 3 次
|
||
- [玩家强退 → 离线刷盘耗时] → 异步等待,最大 5 秒超时
|
||
|
||
**开发期支持**:内存模式 `MemoryDBComponent`,StartConfig.DBConnection = "memory",无需起 mongod
|
||
|
||
### D7: UI 架构 - **深度复用 YIUI 全套子包**(UI 几乎零造轮)
|
||
|
||
**前置事实**:本工程已集成 9 个 YIUI 子包(framework/yiui/yiuiinvoke/yiuitips/yiuireddot/yiuiloopscrollrectasync/yiuiyooassets/yiuieffect/yiui3ddisplay),覆盖面板管理 / 数据绑定 / 事件绑定 / 弹窗 / 红点 / 长列表 / 资源加载 / UI 特效 / 3D 模型展示。详见 `Doc/ET-Packages-Audit.md` "YIUI 框架" 一节。
|
||
|
||
**选择**:使用 YIUI 框架原生 MVC 模式 + 全套子包能力,**禁止自建 PanelHelper / 长列表 / Tooltip 容器 / 红点树 / 3D 展示等已有能力**
|
||
|
||
```
|
||
HeroListPanel (Panel)
|
||
├── HeroListPanelComponent.cs # ET Entity,状态层(YIUI 工具生成)
|
||
├── HeroListPanelComponentSystem.cs # ET System,业务层
|
||
└── HeroListPanel.prefab # YIUI UIBindCDETable 配置
|
||
├── CommonHeader (已有 UICommon,复用)
|
||
├── YIUILoopScrollVertical (yiuiloopscrollrectasync)
|
||
│ └── HeroListItem (UICommon,u_Data* + UIDataBind*)
|
||
└── BottomBar
|
||
```
|
||
|
||
**面板生命周期统一入口**(**不要自建 PanelHelper**):
|
||
|
||
```csharp
|
||
// 打开
|
||
await scene.YIUIRoot().OpenPanelAsync<HeroListPanelComponent>();
|
||
// 带参打开(最多 5 个参数)
|
||
await scene.YIUIRoot().OpenPanelParamAsync<HeroDetailPanelComponent>(heroId);
|
||
// 模态等待结果
|
||
var result = await scene.YIUIRoot().OpenPanelWaitAsync<TipsMessageViewComponent>("确认抽卡?");
|
||
```
|
||
|
||
**YIUI 子包对业务的具体映射**:
|
||
|
||
| ROK 业务点 | YIUI 子包/API | 备注 |
|
||
|---|---|---|
|
||
| 长列表(背包 1000+ / 英雄列表) | `cn.etetet.yiuiloopscrollrectasync` → `YIUILoopScrollChild.SetDataRefresh` | 数据变长度不变用 `ReRenderer()` 增量刷新 |
|
||
| 道具图标异步加载 | `cn.etetet.yiuiyooassets` → `UIDataBindImage` + `u_DataIcon.SetValue("ItemIcon_1001")` | YooAsset 自动加载,**不写 LoadHelper** |
|
||
| 抽卡按钮灰显 / 不可用 | `cn.etetet.yiuieffect` → `UIDataBindGray` + `u_DataCanDraw.SetValue(false)` | 不写 Material 置灰 |
|
||
| 抽卡结果闪光/粒子 | `cn.etetet.yiuieffect` → `UIEffect` + `UIParticle` | 不引第三方 VFX |
|
||
| 英雄详情 3D 模型 | `cn.etetet.yiui3ddisplay` → `YIUI3DDisplayChild.ShowAsync("HeroModel_10001")` | 框架管 RT/Camera/Pool |
|
||
| 抽卡 CD / 活动倒计时 | `cn.etetet.yiuiframework` → `CountDownMgr` | 不自建 Timer 管理器 |
|
||
| 抽卡 RPC 等待挡点击 | `cn.etetet.yiuiframework` → `scene.YIUIMgr().BanLayerOptionForever()` | 异步期间防双击 |
|
||
| 确认/取消弹窗 | `cn.etetet.yiuitips` → `TipsHelper.OpenWait<TipsMessageViewComponent>(scene, msg)` | 自带 `HashWait` 模态返回 |
|
||
| 操作成功飘字 | `cn.etetet.yiuitips` → `TipsHelper.OpenSync<TipsTextViewComponent>(scene, "招募成功")` | 自带淡入淡出动画 |
|
||
| 客户端错误提示(错误码) | `cn.etetet.yiuitips` + `ErrorMessageHelper.GetMessage(code)` | 走 `TipsTextView`,不写 Toast 系统 |
|
||
| 红点(英雄/背包/抽卡入口) | `cn.etetet.yiuireddot` → `RedDotMgr.Inst.SetCount(key, count)` + Prefab `RedDotBind` | **只写叶子节点**,父节点自动汇总 |
|
||
| 顶栏 + 关闭 | `CommonHeader` + `YIUICloseCommon`(已有) | 直接拖入新 Panel |
|
||
|
||
**UI 与服务端业务的解耦**:用 `cn.etetet.yiuiinvoke` 的 `[YIUIInvokeSystem("Key")]` 机制 + invoke 契约包(D2 已定义 `cn.etetet.heroinvoke` 等),让 GM 或其他业务不直接 reference 业务包。
|
||
|
||
### D8: 客户端缓存与事件机制 - **UIData 局部驱动 + ET EventSystem 跨面板广播**
|
||
|
||
**两层职责拆分**(明确边界,避免误用):
|
||
|
||
| 层 | 用什么 | 时机 |
|
||
|---|---|---|
|
||
| **Panel/View 内部数据 → 自身 UI 刷新** | YIUI `UIDataBind*` + `u_DataXxx.SetValue(...)` | Panel 内 List Item 的图标、名字、数量等局部字段,**不广播** |
|
||
| **跨面板的业务变化广播** | ET `EventSystem.Instance.Publish(new XxxEvent)` | 英雄列表 + 英雄详情同时打开时,一处变多处响应 |
|
||
|
||
**示例**:
|
||
|
||
```csharp
|
||
// 收到服务端推送
|
||
public class Hero_HeroInfoHandler : MessageHandler<Hero_HeroInfo>
|
||
{
|
||
protected override async ETTask Run(Session session, Hero_HeroInfo msg)
|
||
{
|
||
var heroComp = session.GetComponent<PlayerComponent>().Player.GetComponent<HeroComponent>();
|
||
heroComp.UpdateHero(msg.Hero);
|
||
// 跨面板广播(多个 Panel 订阅)
|
||
EventSystem.Instance.Publish(new HeroUpdatedEvent { HeroId = msg.Hero.HeroId });
|
||
}
|
||
}
|
||
|
||
// Panel 监听
|
||
[Event] public class HeroUpdatedEvent_RefreshList : AEvent<Scene, HeroUpdatedEvent>
|
||
{
|
||
protected override async ETTask Run(Scene scene, HeroUpdatedEvent args)
|
||
{
|
||
var panel = scene.YIUIMgr().GetPanel<HeroListPanelComponent>();
|
||
if (panel == null) return;
|
||
// Panel 内部通过 u_Data* 局部刷新,不再二次广播
|
||
await panel.RefreshHero(args.HeroId);
|
||
}
|
||
}
|
||
|
||
// Panel 内部刷新(YIUI 数据驱动,不发事件)
|
||
public static async ETTask RefreshHero(this HeroListPanelComponent self, long heroId)
|
||
{
|
||
var hero = self.ClientScene().GetHero(heroId);
|
||
var item = self.GetItemByHeroId(heroId);
|
||
item.u_DataLevel.SetValue(hero.Level); // 局部刷新,YIUI 自动绑定到 TMP_Text
|
||
item.u_DataPower.SetValue(hero.Power);
|
||
item.u_DataIcon.SetValue($"HeroIcon_{hero.HeroId}"); // 自动走 YooAsset 加载
|
||
}
|
||
```
|
||
|
||
**为什么不全用 EventSystem**:
|
||
- Panel 内部 List Item 数百个字段,全发事件 → 事件爆炸 + 性能问题
|
||
- YIUI `UIDataBind` 是为局部数据刷新设计,零事件开销
|
||
|
||
**为什么不全用 UIDataBind**:
|
||
- 跨面板(列表 + 详情)数据同步需要广播机制
|
||
- UIData 只在单个 Panel/Component 内有效
|
||
|
||
### D9: 与 OctoberStudio 集成
|
||
|
||
**选择**:**Init.unity 作为主入口**,登录后切到 OctoberStudio Main Menu
|
||
|
||
```
|
||
启动流程:
|
||
1. Init.unity (ET 启动) → Realm → Gate 登录
|
||
2. 登录成功,拉取 HeroInfoList + ItemInfoList
|
||
3. 切换到 OctoberStudio Main Menu 场景(保留原玩法入口)
|
||
4. 在 Main Menu 添加"英雄"/"背包"按钮 → 打开 YIUI Panel
|
||
```
|
||
|
||
**为什么不替换 Main Menu**:
|
||
- OctoberStudio 已有完整局内战斗、宝箱、设置等流程
|
||
- 强行替换风险大
|
||
- "ET 是基础设施 + OctoberStudio 是单机玩法"双层架构
|
||
|
||
**OctoberStudio 存档**:
|
||
- `CurrencySave`、`CharactersSave` 保留为局内/设置用途
|
||
- 英雄、装备、背包用 ET 服务端存档(DB)
|
||
- 二者通过适配层访问(如金币:先查 ET,再降级到本地)
|
||
|
||
### D9.1: 抽卡系统设计
|
||
|
||
**选择**:抽卡作为独立业务包 `cn.etetet.gacha`,但**强依赖** `bag-system`(消耗+奖励)+ `hero-system`(抽到英雄/碎片)。**抽卡是端到端验证最佳工具**,进入 P1 阶段第一个落地。
|
||
|
||
**核心算法(服务端权威)**:
|
||
|
||
```csharp
|
||
public static class GachaComponentSystem
|
||
{
|
||
public static List<RewardEntry> Draw(this GachaComponent self, int poolId, GachaDrawType type)
|
||
{
|
||
var poolCfg = GachaPoolConfigCategory.Instance.Get(poolId);
|
||
var weights = GachaWeightConfigCategory.Instance.GetByPool(poolId);
|
||
var times = type == GachaDrawType.Single ? 1 : 10;
|
||
|
||
// 1. 校验消耗
|
||
var costItemId = type == GachaDrawType.Single ? poolCfg.SingleCostItem : poolCfg.MultiCostItem;
|
||
var costCount = type == GachaDrawType.Single ? poolCfg.SingleCostCount : poolCfg.MultiCostCount;
|
||
if (!bag.HasItem(costItemId, costCount)) return null; // 由调用方判错返回
|
||
|
||
// 2. 扣消耗
|
||
bag.RemoveItem(costItemId, costCount);
|
||
|
||
// 3. 随机抽取
|
||
var rewards = new List<RewardEntry>();
|
||
var totalWeight = weights.Sum(w => w.Weight);
|
||
for (int i = 0; i < times; i++)
|
||
{
|
||
int r = RandomGenerator.RandomNumber(0, totalWeight);
|
||
var pick = SelectByWeight(weights, r);
|
||
rewards.Add(new RewardEntry { ItemId = pick.ItemId, Count = pick.Count });
|
||
|
||
// 4. 更新保底
|
||
self.UpdateGuarantee(poolId, pick.Rare);
|
||
}
|
||
|
||
// 5. 十连保底兜底(必出指定品质)
|
||
if (type == GachaDrawType.Multi10 && !rewards.Any(r => GetItemRare(r.ItemId) >= poolCfg.MultiGuaranteeRare))
|
||
ReplaceLowestWithGuaranteed(rewards, weights, poolCfg.MultiGuaranteeRare);
|
||
|
||
// 6. 累计保底兜底(90 抽必出)
|
||
if (self.GetGuaranteeRemain(poolId) == 0)
|
||
ReplaceWithGuaranteed(rewards, weights, poolCfg.GuaranteeRare);
|
||
|
||
// 7. 发放奖励
|
||
bag.AddItems(rewards);
|
||
|
||
// 8. 日志
|
||
GachaAuditLog.Log(self.Player.Id, poolId, rewards, self.GetPoolStats(poolId));
|
||
|
||
return rewards;
|
||
}
|
||
}
|
||
```
|
||
|
||
**保底设计**:
|
||
- **每卡池独立保底**:玩家在不同卡池的保底计数互不影响
|
||
- **双重保底**:十连必出(如必有 1 个 4 星)+ 累计必出(90 抽必有 5 星)
|
||
- **重置规则**:自然出 ≥ 保底品质 → 计数清零;保底强制出 → 也清零
|
||
|
||
**防作弊**:
|
||
- 客户端**完全不参与**随机数生成
|
||
- 客户端 payload 中的任何"种子/运气/概率加成"字段都被忽略
|
||
- 服务端日志记录每次抽卡的完整信息,便于事后审计/玩家投诉处理
|
||
|
||
**为什么把抽卡放 P1 第一个**:
|
||
- 它能**端到端验证**:消耗道具 → 服务端随机 → 发奖 → 入背包 → 用于英雄培养
|
||
- 完成抽卡,等于把 hero-system + bag-system + protocol + persistence 都跑通了
|
||
- 后续养成系统的"测试数据"也可以直接靠抽卡获得,省去 GM 命令
|
||
|
||
**风险**:
|
||
- [伪随机种子可预测] → 用 `RandomNumberGenerator.GetInt32`(CSPRNG)或 Random + 高熵 seed
|
||
- [抽卡接口被刷] → 服务端加冷却限速 + 异常 IP/UID 自动报警
|
||
|
||
### D10: 分阶段交付计划 - **UI 后置,先服务端 + 日志 + GM 验证**
|
||
|
||
**选择**:**业务骨架先行、UI 集中后期补**。P1-P3 不做任何 YIUI 面板,只产出"服务端业务 + 协议 + GM 命令 + 日志"。所有 UI 集中在 P4 一次性补齐。
|
||
|
||
| 阶段 | 内容 | 验证方式 | 周期 |
|
||
|---|---|---|---|
|
||
| **P0** 基础设施 | DBComponent + 协议工具链 + Luban + **日志框架** + **GM 框架** + 跑通登录 | 单元测试 + Demo 命令 | 3-5 天 |
|
||
| **P1** 抽卡 + 通用 Item + 英雄基础 | **服务端**:通用 Item、抽卡算法、英雄实体、招募/升级/技能升级;**零 UI** | GM 命令 + 业务日志 + 单元测试 | 1-1.5 周 |
|
||
| **P2** 背包 + 装备穿戴 | **服务端**:背包增减、6 大类、装备穿脱;**零 UI** | GM 命令 + 业务日志 | 1 周 |
|
||
| **P3** 高级养成 | **服务端**:升星、觉醒、天赋、锻造;**零 UI** | GM 命令 + 业务日志 | 1-2 周 |
|
||
| **P4** UI 集中实现 | YIUI 把 P1-P3 的所有面板一次性补齐 | 手动 + 自动化 UI 测试 | 2-3 周 |
|
||
|
||
**为什么改成 UI 后置**:
|
||
|
||
1. **业务逻辑稳定性优先**:先把服务端真权威 + 协议 + DB 持久化跑稳,再做 UI 不会反复返工
|
||
2. **UI 不阻塞后端开发**:UI 是消费者,业务接口确定了 UI 才好做,先做 UI 反而互相阻塞
|
||
3. **日志 + GM 完全可替代 UI 验证**:所有业务正确性都能用 GM 命令触发 + 日志检查,效率比手动点 UI 快 5-10 倍
|
||
4. **集中做 UI 质量更好**:分散在每个阶段做 UI 容易抄近路、视觉风格不统一;集中做能整体规划
|
||
5. **避免 UI 改动反复影响后端**:业务字段变了 UI 要改、UI 设计变了协议要改,等业务定型再做 UI 把这类返工降到 0
|
||
|
||
**风险与缓解**:
|
||
|
||
- **[业务定义错了到 P4 才发现]** → 用 GM 命令 + 日志强校验,每个 RPC 都有正反例测试;具体在 `gm-tools` 与 `observability-logging` capability 中定义
|
||
- **[P4 UI 工作量大]** → P0 就把 YIUI 模板 + `CommonHeader`/`BagItemUICommon` 准备好;P1-P3 也产出 UI 字段需求文档,让 P4 不需要返查
|
||
- **[策划/产品要看效果]** → 给策划提供 GM Web 控制台(基础版即可,复用 ET 现有 Watcher),可视化展示玩家数据
|
||
|
||
**P1 第一个落地的还是抽卡**:抽卡仍然是端到端验证最佳工具,但**验证方式从 UI 改为 GM 命令 + jsonl 日志**。
|
||
|
||
**关键约束**:P1-P3 阶段**绝对不写 YIUI Panel 代码**。任何"我需要个简单界面看看效果"的需求都通过 GM 命令 + 日志解决。
|
||
|
||
### D11: 日志策略 - **基于 ET 自带 Log 扩展**(不重写)
|
||
|
||
**前置事实**:`cn.etetet.core` 已有 `Log` 静态类(Debug/Info/Warning/Error/Trace/Console),`cn.etetet.loader` 已集成 NLog 后端,可通过 `Fiber.Instance.Log` 接口替换实现。**我们要做的是扩展,不是新框架**。
|
||
|
||
**选择**:**三层日志体系(扩展 ET Log)**
|
||
|
||
| 层级 | 用途 | 输出 | 保留 |
|
||
|---|---|---|---|
|
||
| **L1 业务日志** | 每个 RPC 入口/出口、参数、玩家ID、结果 | `Logs/Business/{yyyy-MM-dd}.log` | 30 天 |
|
||
| **L2 审计日志** | 抽卡、付费、GM 操作、关键资源变动 | `Logs/Audit/{module}/{yyyy-MM-dd}.jsonl` | 永久 |
|
||
| **L3 错误日志** | 异常、错误码返回、慢查询 | `Logs/Error/{yyyy-MM-dd}.log` + 实时告警 | 90 天 |
|
||
|
||
**结构化 JSON 行格式**(jsonl,便于 grep/jq/ELK):
|
||
|
||
```json
|
||
{"ts":"2026-05-27T11:30:01.234","level":"INFO","module":"Gacha","playerId":1001,"action":"Draw","poolId":2,"drawType":"Multi10","costItem":2,"costCount":9,"rewards":[{"itemId":10001,"count":1,"rare":4},...],"guaranteeRemain":85,"reqId":"abc123"}
|
||
```
|
||
|
||
**关键约定**:
|
||
|
||
1. **每个 RPC Handler 入口必埋点**:playerId + actionName + params 摘要
|
||
2. **关键操作(消耗/发奖/抽卡/升星)必写 L2 审计**:单独 jsonl 不混在普通日志里,便于运营/客服/数据分析读取
|
||
3. **每个 ErrorCode 返回必写 L3 错误日志**:含 playerId / errorCode / 调用栈/ 上下文
|
||
4. **客户端日志桥接**:客户端的 `Log.Error` 关键错误可选上报到服务端(默认开启,可配置关闭)
|
||
5. **reqId 串联**:客户端发送 RPC 带 reqId,服务端日志全程透传,便于跨服务追查
|
||
6. **日志分模块**:`[Hero] [Bag] [Gacha] [Equip] [DB] [Login]` 前缀,便于 `grep -F '[Gacha]'`
|
||
7. **慢查询告警**:DB 查询 > 100ms、Handler 执行 > 500ms 自动写 L3 告警
|
||
|
||
**ET 框架集成(明确边界)**:
|
||
- **保留**:`Log.Debug/Info/Warning/Error` 全部入口不变,业务代码继续用
|
||
- **扩展**:新建 `cn.etetet.logging` 包,加扩展方法 `Log.Audit(module, action, payload)` / `Log.Slow(module, action, durationMs)` / `Log.Module(tag, level, msg)`
|
||
- **底层**:复用现有 NLog(`Packages/cn.etetet.loader/Scripts/Loader/Server/NLog.config`),加 3 个 target 分别输出到 `Logs/Business/` `Logs/Audit/{module}/` `Logs/Error/`
|
||
- **Handler 自动埋点**:抽 `RpcMessageHandler<TReq,TResp>` 基类,在 OnRun 前后自动写 L1(入口/出口 + 耗时)
|
||
- **慢操作**:包装 `DBComponent` / `MessageDispatcher` 的入口方法,超阈值自动 Log.Slow
|
||
- **reqId 透传**:在所有 C2G/C2R 协议基类加 `reqId` 字段,Handler 基类把 reqId 放到 `AsyncLocal<string>`,供下游 Log 自动取
|
||
|
||
**风险与缓解**:
|
||
- [日志写盘 IO 性能] → NLog 异步队列 + 批量刷盘,关键审计日志走同步落盘保证不丢
|
||
- [日志爆盘] → L1 按天滚动 + 30 天清理,L3 90 天清理,L2 转 OSS 永久归档
|
||
- [敏感字段泄露] → 玩家个人信息(手机号/邮箱)必须脱敏,工具自动扫描
|
||
|
||
### D12: GM 验证策略 - **直接复用 yiuigm + console 现成包**(不重写框架)
|
||
|
||
**前置事实**:
|
||
- `cn.etetet.yiuigm` 已有 `[GM]` 属性 + `IGMCommand` 接口 + 完整 GMPanel UI(含参数 Enum/String/Bool/Float/Int/Long 6 类)
|
||
- `cn.etetet.console` 已有 `[ConsoleHandler]` 属性 + `IConsoleHandler` 接口 + 服务端 stdin REPL(启动时自动监听控制台输入)
|
||
- 两套都已纳入热更编译
|
||
|
||
**结论**:**完全不需要新建 GM 框架**。我们要做的只是用这两个属性写业务 GM 类。
|
||
|
||
**选择**:**GM 命令是 P1-P3 阶段唯一的验证入口**(在 UI 出来之前),必须覆盖所有写操作 + 提供读取/快照/模拟工具
|
||
|
||
**GM 双轨(基于现成包)**:
|
||
|
||
1. **服务端 GM Console**(`cn.etetet.console` 现成)
|
||
- 注册方式:`[ConsoleHandler("AddItem")] public class C_AddItem : IConsoleHandler { public async ETTask Run(Fiber fiber, ModeContex contex, string content) { ... } }`
|
||
- 直接登录服务端 shell 执行
|
||
- 适合数据校验、批量操作、压测
|
||
2. **客户端 GM Panel**(`cn.etetet.yiuigm` 现成)
|
||
- 注册方式:`[GM(EGMType.Test, 1, "发放道具", "给当前玩家发放指定道具")] public class GM_AddItem : IGMCommand { public List<GMParamInfo> GetParams() { ... } public async ETTask<string> Run(Scene scene, GMParamVo vo) { ... } }`
|
||
- 仅 Debug 包启用(`#if ENABLE_GM`)
|
||
- 适合开发期手动测试
|
||
|
||
**GM 命令分类**:
|
||
|
||
| 类别 | 命令示例 | 用途 |
|
||
|---|---|---|
|
||
| **资源发放** | `gm AddItem <itemId> <count>` / `AddCurrency <type> <count>` | 给自己/指定玩家发资源 |
|
||
| **英雄操作** | `AddHero <heroId>` / `SetHeroLevel <heroId> <lv>` / `SetHeroStar <heroId> <star>` | 创建/调整英雄 |
|
||
| **背包操作** | `ClearBag` / `ShowBag` / `SetItemCount <itemId> <count>` | 背包状态调整与查看 |
|
||
| **抽卡操作** | `Gacha <poolId> <times>` / `ResetGachaGuarantee <poolId>` / `SetGachaCount <poolId> <count>` | 抽卡 + 保底操作 |
|
||
| **数据查询** | `ShowHeroes` / `ShowBag` / `ShowGachaStats` / `DumpPlayer <playerId>` | 输出当前状态 jsonl |
|
||
| **数据快照** | `Snapshot <name>` / `Restore <name>` | 保存/恢复整个玩家状态 |
|
||
| **批量模拟** | `SimulateGacha <poolId> <times>` 服务端循环 N 次抽卡输出概率统计 | 概率审计 |
|
||
| **数据校验** | `VerifyConsistency <playerId>` 校验玩家数据完整性 | 发现脏数据 |
|
||
| **服务管理** | `ReloadConfig` / `FlushDirty` / `Kick <playerId>` | 运维操作 |
|
||
|
||
**GM 操作必写审计日志**(D11 的 L2 层):
|
||
```json
|
||
{"ts":"...","level":"WARN","module":"GM","operator":"admin","action":"AddItem","targetPlayer":1001,"args":{"itemId":1,"count":1000000}}
|
||
```
|
||
|
||
**关键测试场景(用 GM 验证)**:
|
||
|
||
| 场景 | GM 命令组合 | 期望日志 |
|
||
|---|---|---|
|
||
| 抽卡正确扣消耗 | `AddCurrency 2 10` → `Gacha 1 1` → `ShowBag` | Bag 钻石 -1,奖励入包 |
|
||
| 抽卡保底触发 | `SetGachaCount 1 89` → `Gacha 1 1` 10 次 | 第一次抽到必出保底品质 |
|
||
| 抽卡概率正确 | `SimulateGacha 1 10000` | 输出概率分布,与配置误差 < 1% |
|
||
| 英雄升级正确 | `AddHero 10001` → `AddItem <经验书ID> 100` → 调升级 | Hero level 提升,经验书减少 |
|
||
| 数据持久化 | `Snapshot before` → 一系列操作 → `FlushDirty` → 重启 → `DumpPlayer` 对比 | 数据完全一致 |
|
||
| 通用 Item 不漏 | `ShowBag` 在做了大量操作后金币/钻石/碎片都显示正确 | 没有"零钻石"等漏算 |
|
||
|
||
**风险与缓解**:
|
||
- [GM 命令被滥用上线包带出] → 编译条件 `#if !RELEASE` 包裹 GM 注册,Release 包不含 GM 命令
|
||
- [GM 操作绕过业务校验导致数据不一致] → GM 直接调 `BagComponentSystem.AddItems` 等业务接口,不允许绕过
|
||
|
||
### D13: 强制复用原则 - **避免重复造轮子**
|
||
|
||
**背景**:本次工程已有 38 个 ET 包,覆盖 UI / 协议 / 资源热更 / GM / 服务端 console / 网络 / 数学 / 序列化等大部分基础设施。详见 `Doc/ET-Packages-Audit.md`。
|
||
|
||
**强制原则**(每次新功能落地前必须自检):
|
||
|
||
1. **第一步查表**:开 `Doc/ET-Packages-Audit.md` 速查表,确认是否有现成 ET 包能用
|
||
2. **能扩展不重写**:若现有包能力不全,**优先在现有包加扩展方法或新 Component**,禁止重写
|
||
3. **业务功能建新包**:完全新的业务(hero/bag/gacha)建独立 `cn.etetet.xxx` 包,**禁止塞进** `core` / `login` 等基础包
|
||
4. **设计文档必须标注复用**:每个 D 决策必须明确"复用了哪些包 + 扩展点 + 新增点"
|
||
|
||
**本次明确的复用清单**:
|
||
|
||
| 能力 | 状态 | 复用方式 |
|
||
|---|---|---|
|
||
| 日志输出 | 复用 `cn.etetet.core/Log.cs` | 新建 `cn.etetet.logging` 加 `Log.Audit/Log.Slow/Log.Module` 扩展方法 |
|
||
| 客户端 GM 面板 | 完全复用 `cn.etetet.yiuigm` | 新增 `[GM]` 业务类即可 |
|
||
| 服务端 GM Console | 完全复用 `cn.etetet.console` | 新增 `[ConsoleHandler]` 业务类即可 |
|
||
| BSON 序列化 | 完全复用 `cn.etetet.core/MongoHelper` | 不重写 |
|
||
| MongoDB Driver | 完全复用 `com.etetet.init/Plugins/MongoDB` | 不重写 |
|
||
| 协议生成 | 完全复用 `cn.etetet.proto` + `ET/Proto/Proto2CS` 菜单 | 新增 `Proto/*.proto` 文件即可 |
|
||
| 资源热更 | 完全复用 `cn.etetet.yooassets` + `cn.etetet.hybridclr` | 不动 |
|
||
| YIUI 框架 | 完全复用 `cn.etetet.yiuiframework` 等 9 个子包 | 业务 Panel 用 YIUI 自动化工具生成 |
|
||
| 登录链路 | 扩展 `cn.etetet.login` | 给 Player 挂业务 Component,不改 Account 字段 |
|
||
| ExcelExporter | **不用**(保留供 StartConfig 等基础设施用) | 新业务表全部走 Luban |
|
||
|
||
**本次明确的新增清单**:
|
||
|
||
| 新增包 | 职责 |
|
||
|---|---|
|
||
| `cn.etetet.db` | DBComponent 业务封装(基于 MongoHelper) |
|
||
| `cn.etetet.logging` | Audit/Slow/Module 扩展(基于 ET Log) |
|
||
| `cn.etetet.config` | Luban Tables + ConfigComponent |
|
||
| `cn.etetet.hero` | 英雄系统 |
|
||
| `cn.etetet.bag` | 背包/装备系统 |
|
||
| `cn.etetet.gacha` | 抽卡系统 |
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- **[移植工作量大]** → 分 4 阶段交付,每阶段独立验收
|
||
- **[ROK 业务规则细节多]** → 在 specs.md 里列清楚 scenario,写测试覆盖
|
||
- **[服务端单进程性能瓶颈]** → 玩家少(< 1000 同时在线)时不会到瓶颈,需要时再拆 Scene
|
||
- **[ET 框架学习曲线]** → 团队需要熟悉 Entity/Component/System 模式,跟 PureMVC 思路差距不小;建议先吃透 Login Demo 再开干
|
||
- **[DB 字段变更迁移]** → 在每个 Entity 加 dataVersion 字段 + DBMigrationComponent 自动迁移
|
||
- **[ROK 协议是 sproto,转 protobuf 字段细节可能丢失]** → 不直接转换,按业务需求重新设计 proto;只参考字段语义不参考字段顺序
|
||
- **[OctoberStudio 与 ET 双重启动复杂]** → 在 Loader 添加"开发模式快速跳过登录"开关
|
||
- **[HybridCLR AOT 元数据问题]** → 业务代码全部放热更层 Hotfix,泛型扫描通过 `ET/HybridCLR/Generate/All` 一键解决
|
||
- **[Luban 学习曲线]** → P0 阶段做 ItemConfig 一张示例表跑通全链路 + 写文档
|
||
- **[UI 后置导致策划/老板看不到效果]** → P0 起步就有 GM Web Console(基于 ET Watcher),日志 + 数据可视化即时反馈
|
||
- **[GM 漏覆盖某个业务点]** → 每个新增 RPC Handler MUST 同时新增对应 GM 命令(在 spec 中强制约束)
|
||
- **[Login Demo 没有密码校验]** → P0 阶段补一个简单的账号密码校验(hash 入库),生产再升级
|
||
|
||
## Migration Plan
|
||
|
||
### P0 之前必须先完成的"基础设施"
|
||
1. 执行 `ET/Loader/Compile (F6)` 生成客户端热更 DLL
|
||
2. `dotnet build ET.sln -c Debug` 生成 `Bin/ET.App.dll`
|
||
3. 跑通 Login Demo(Init.unity → 输入账号 → 登录成功)
|
||
4. **新建 `cn.etetet.db` 包**,封装 `IDBComponent.Save/Query/Delete`(基于 MongoHelper + MongoDB.Driver);支持 Memory/MongoDB 双模式
|
||
5. **新建 `cn.etetet.logging` 包**,加 `Log.Audit/Log.Slow/Log.Module` 扩展方法 + NLog 三 target 配置
|
||
6. **接入 ROK 配置迁移工具链**(参考 `gmd/Tools/export_hero_json_from_configs_data.py`)
|
||
7. 跑通"登录 → 拉空 HeroInfoList → 服务端日志验证(暂无 UI)"链路
|
||
|
||
### 部署顺序
|
||
1. 服务端先升级(DB schema 兼容)
|
||
2. 客户端再发版(向后兼容旧服务端)
|
||
|
||
### Rollback 策略
|
||
- 客户端:YooAsset 资源回滚到上一版本 manifest
|
||
- 服务端:dotnet build 之前打 tag,回滚到上版本 commit + 重启
|
||
- 数据:DB schema 通过 dataVersion 向前兼容,理论上无需回滚 DB
|
||
|
||
## Open Questions
|
||
|
||
1. **Q: 玩家创建账号流程在哪做?** Login Demo 没账号注册,是允许任意账号自动注册,还是要单独 RegisterPanel?
|
||
_倾向:P0 允许任意账号自动注册(首次登录创建 Player),生产再加严格注册_
|
||
|
||
2. **Q: OctoberStudio 现有金币 (CurrencySave) 和 ET 服务端金币如何统一?**
|
||
_倾向:保留两份。OctoberStudio 金币用于局内(关卡内不需要联网),服务端金币用于英雄养成消耗。两者通过"局外结算时同步"_
|
||
|
||
3. **Q: 离线作战是否支持?**
|
||
_倾向:P0-P3 阶段都要求联网(联网才能拉数据/校验操作)。后续可加"离线模式"用本地缓存作为兜底_
|
||
|
||
4. **Q: 多语言怎么做?**
|
||
_倾向:P0-P3 阶段只支持中文。预留 L10N 接口,需要时再补_
|
||
|
||
5. **Q: HeroInfo 是否需要包含战斗属性快照?**
|
||
_倾向:HeroInfo 不存战斗属性(攻防血),客户端根据 level + star + skills + talents + equips 实时算。这样字段少、推送数据小_
|
||
|
||
6. **Q: 是否需要"友好名"分页(如把所有 6 大类合并成 4 个 Tab)?**
|
||
_倾向:UI 分页用枚举 BagItemType 直接 6 个 Tab,简单清晰_
|
||
|
||
7. **Q: 抽卡保底配置是否做"软保底"(概率随抽数提升)?**
|
||
_倾向:P1 阶段只做"硬保底"(90 抽必出),软保底(如 70 抽后概率从 0.6% 渐升到 1.5%)放到 P3 优化_
|
||
|
||
8. **Q: 抽卡历史是否需要持久化到 DB(供玩家查询)?**
|
||
_倾向:保留**最近 100 次**抽卡记录到 GachaComponent,超过的自动清理。完整历史只在服务端日志_
|
||
|
||
9. **Q: 一次性发奖列表(如十连)传 10 个 Item_ItemInfo 还是合并?**
|
||
_倾向:服务端在十连 R2C_GachaDraw 响应里把奖励聚合后一次发 `repeated RewardEntry rewards`,**不再单独推 Item_ItemInfo**;客户端从 rewards 列表自己更新本地 BagComponent_
|
||
|
||
10. **Q: 金币/钻石变化是否要走 BagComponent?还是单独的 ResourceComponent?**
|
||
_倾向:**严格按 D4.1 通用 Item 原则**,金币/钻石作为 itemId=1/2 存在 BagComponent 里。否则就破坏了"全游戏 Item 统一"的核心约定_
|