217 lines
6.0 KiB
Markdown
217 lines
6.0 KiB
Markdown
# Proto 完整工作流
|
||
|
||
## 1. 命名与放置
|
||
|
||
`.proto` 文件必须放在某个 `cn.etetet.<pkg>/Proto/` 目录下,且文件名遵守:
|
||
|
||
```
|
||
<Module>_<Direction>_<StartOpcode>.proto
|
||
```
|
||
|
||
| 字段 | 取值 | 说明 |
|
||
|---|---|---|
|
||
| `Module` | 任意(如 `HeroOuter`、`BagOuter`、`LoginInner`)| 模块标识 |
|
||
| `Direction` | `C` / `S` / `CS` | C=外网消息(客户端可见),S=内网消息(仅服务端),CS=同时 |
|
||
| `StartOpcode` | 整数 | Opcode 从这里递增分配给每条 message |
|
||
|
||
opcode 分配规范(避免冲突):
|
||
|
||
| 模块 | Outer 起始 | Inner 起始 |
|
||
|---|---|---|
|
||
| Login | 1000 | 20001 |
|
||
| Router | 1100 | - |
|
||
| StateSync | 11001 | 21001 |
|
||
| ActorLocation | - | 20100 |
|
||
| Hero | 3000 | - |
|
||
| Bag | 3100 | - |
|
||
| Gacha | 3200 | - |
|
||
| 业务新模块 | 4000+ | 30000+ |
|
||
|
||
---
|
||
|
||
## 2. 消息注解(决定生成的 C# 类继承谁)
|
||
|
||
### RPC 请求 / 响应
|
||
|
||
```proto
|
||
// ResponseType R2C_SummonHero ← 必须在 message 上一行
|
||
message C2G_SummonHero // ISessionRequest
|
||
{
|
||
int32 RpcId = 1;
|
||
string reqId = 2; ← 推荐字段,前 2 个固定 RpcId + reqId
|
||
int32 configId = 3;
|
||
}
|
||
|
||
message R2C_SummonHero // ISessionResponse
|
||
{
|
||
int32 RpcId = 1;
|
||
int32 Error = 2; ← 前 3 个固定 RpcId / Error / Message
|
||
string Message = 3;
|
||
HeroInfo hero = 4;
|
||
}
|
||
```
|
||
|
||
生成结果继承 `MessageObject, ISessionRequest` 和 `MessageObject, ISessionResponse`,并通过 `[ResponseType(nameof(R2C_SummonHero))]` 自动配对。
|
||
|
||
### 单向通知(服务端推客户端)
|
||
|
||
```proto
|
||
message Hero_HeroInfo // IMessage
|
||
{
|
||
HeroInfo hero = 1;
|
||
}
|
||
```
|
||
|
||
### 嵌套 DTO
|
||
|
||
```proto
|
||
message HeroInfo
|
||
{
|
||
int64 heroId = 1;
|
||
int32 level = 2;
|
||
repeated SkillInfo skills = 3; // List<SkillInfo>
|
||
}
|
||
|
||
message SkillInfo
|
||
{
|
||
int32 skillId = 1;
|
||
int32 skillLevel = 2;
|
||
}
|
||
```
|
||
|
||
不加任何 `//` 注解的 message 默认继承 `MessageObject`,可作为 DTO 嵌入其他消息。
|
||
|
||
### 完整可识别的父类标记
|
||
|
||
Proto2CS 解析时,把 `message X // Y` 里的 `Y` 直接拼到生成类的继承列表。常见值:
|
||
|
||
- `IRequest` / `IResponse` — 内网请求/响应
|
||
- `ISessionRequest` / `ISessionResponse` — 客户端 ↔ Gate 的 RPC
|
||
- `IMessage` — 单向通知
|
||
- `IActorMessage` / `IActorRequest` / `IActorResponse` — Actor 模型消息
|
||
|
||
---
|
||
|
||
## 3. 生成
|
||
|
||
**Unity 菜单触发**:
|
||
|
||
```
|
||
ET → Proto → Proto2CS
|
||
```
|
||
|
||
**MCP 触发**:
|
||
|
||
```
|
||
CallMcpTool execute_menu_item {"menuPath": "ET/Proto/Proto2CS"}
|
||
```
|
||
|
||
工具会:
|
||
1. 扫描所有 `cn.etetet.*/Proto/*.proto`
|
||
2. 按文件名解析 Opcode
|
||
3. 输出三份 C# 到:
|
||
- `cn.etetet.proto/CodeMode/Model/Client/<Name>.cs`
|
||
- `cn.etetet.proto/CodeMode/Model/Server/<Name>.cs`
|
||
- `cn.etetet.proto/CodeMode/Model/ClientServer/<Name>.cs`
|
||
|
||
> CodeMode 下只有 `ClientServer/` 子目录有 `AssemblyReference.asmref`(指向 ET.Model),所以在 `CodeMode: 3 = ClientServer` 模式下,**只有 ClientServer 那份会被编译**进 ET.Model。`Client/` 和 `Server/` 是预留给纯客户端/纯服务端模式的。
|
||
|
||
---
|
||
|
||
## 4. 调用
|
||
|
||
### 客户端发 RPC
|
||
|
||
```csharp
|
||
C2G_SummonHero request = C2G_SummonHero.Create();
|
||
request.configId = 1;
|
||
R2C_SummonHero response = await clientScene
|
||
.GetComponent<ClientSenderComponent>()
|
||
.Call(request, false) as R2C_SummonHero;
|
||
if (response.Error != 0)
|
||
{
|
||
Log.Error(response.Message);
|
||
}
|
||
```
|
||
|
||
### 服务端 RPC Handler
|
||
|
||
```csharp
|
||
// Scripts/Hotfix/Server/C2G_SummonHeroHandler.cs
|
||
[MessageSessionHandler(SceneType.Gate)]
|
||
public class C2G_SummonHeroHandler : RpcMessageHandler<C2G_SummonHero, R2C_SummonHero>
|
||
{
|
||
protected override async ETTask OnRun(
|
||
Session session, C2G_SummonHero request, R2C_SummonHero response)
|
||
{
|
||
Player player = session.GetComponent<SessionPlayerComponent>()?.Player;
|
||
if (player == null) {
|
||
response.Error = ErrorCode.ERR_RpcFail;
|
||
return;
|
||
}
|
||
// 业务...
|
||
await ETTask.CompletedTask;
|
||
}
|
||
}
|
||
```
|
||
|
||
注意:
|
||
- **不要**写 `protected override string Module => "X"` 这种属性,ET0004 会报。
|
||
- 文件放 `Scripts/Hotfix/Server/`。
|
||
|
||
### 服务端推单向通知
|
||
|
||
```csharp
|
||
Hero_HeroInfo notice = Hero_HeroInfo.Create();
|
||
notice.hero = HeroMapper.ToProto(hero);
|
||
session.Send(notice);
|
||
```
|
||
|
||
### 客户端通知 Handler
|
||
|
||
```csharp
|
||
// Scripts/Hotfix/Client/Hero_HeroInfoHandler.cs
|
||
[MessageHandler(SceneType.Main)]
|
||
[FriendOf(typeof(HeroComponent))]
|
||
public class Hero_HeroInfoHandler : MessageHandler<Scene, Hero_HeroInfo>
|
||
{
|
||
protected override async ETTask Run(Scene scene, Hero_HeroInfo message)
|
||
{
|
||
var comp = scene.GetComponent<HeroComponent>() ?? scene.AddComponent<HeroComponent>();
|
||
comp.HeroMap[message.hero.heroId] = message.hero;
|
||
EventSystem.Instance.Publish(scene, new HeroUpdatedEvent { HeroId = message.hero.heroId });
|
||
await ETTask.CompletedTask;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 常见 Proto 坑
|
||
|
||
1. **写完 .proto 不跑 Proto2CS → 引用方报"找不到类型"**:必须先生成。
|
||
2. **缺 `// ISessionRequest`**:消息生成出来不继承 `ISessionRequest`,handler 的 `MessageSessionHandler<TReq, TResp>` 类型约束失败 → 编译报错。
|
||
3. **缺 `// ResponseType R2C_X`**:客户端 `await session.Call(req)` 无法推断响应类型。
|
||
4. **文件名 opcode 重复**:和别的 proto 撞 opcode → 运行时崩。规范分配规则见上面表格。
|
||
5. **手写 `new HeroInfo {...}`** → ET0031。改 `HeroInfo.Create()`。
|
||
6. **修改 .proto 字段顺序 / 类型** → MemoryPack 序列化兼容性问题。线上版本要么不改,要么走版本兼容方案。
|
||
|
||
---
|
||
|
||
## 6. 一键自检 checklist
|
||
|
||
写完一个新 .proto 后:
|
||
|
||
```
|
||
- [ ] 文件名格式 <Module>_<Direction>_<Opcode>.proto
|
||
- [ ] Opcode 不与其他 .proto 冲突(grep "_C_<同段号>_")
|
||
- [ ] RPC 消息有 // ResponseType
|
||
- [ ] 请求带 // ISessionRequest 或 // IRequest
|
||
- [ ] 响应带 // ISessionResponse 或 // IResponse
|
||
- [ ] 通知带 // IMessage
|
||
- [ ] 跑了 ET/Proto/Proto2CS
|
||
- [ ] 检查 cn.etetet.proto/CodeMode/Model/ClientServer/<Name>.cs 已生成
|
||
- [ ] Assets/Refresh 让 Unity 索引到新文件
|
||
- [ ] recompile_scripts 验证 0 错误
|
||
```
|