6.0 KiB
Raw Permalink Blame History

Proto 完整工作流

1. 命名与放置

.proto 文件必须放在某个 cn.etetet.<pkg>/Proto/ 目录下,且文件名遵守:

<Module>_<Direction>_<StartOpcode>.proto
字段 取值 说明
Module 任意(如 HeroOuterBagOuterLoginInner 模块标识
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 请求 / 响应

// 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, ISessionRequestMessageObject, ISessionResponse,并通过 [ResponseType(nameof(R2C_SummonHero))] 自动配对。

单向通知(服务端推客户端)

message Hero_HeroInfo // IMessage
{
    HeroInfo hero = 1;
}

嵌套 DTO

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

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

// 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/

服务端推单向通知

Hero_HeroInfo notice = Hero_HeroInfo.Create();
notice.hero = HeroMapper.ToProto(hero);
session.Send(notice);

客户端通知 Handler

// 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:消息生成出来不继承 ISessionRequesthandler 的 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 错误