系统全景:总体架构与分布式拓扑 private

系统全景:总体架构与分布式拓扑

本篇只回答系统由哪些部分组成、每部分拥有哪类状态、请求怎样跨节点流动。Unity 局部实现见客户端分区;Actor 和节点代码见服务端分区;单次战斗包的端到端过程见联机同步分区。

返回技术文档目录

1. 一句话认识 QMGame

QMGame 不是“Unity 客户端连接一台游戏服务器”,而是由三类运行时和一套内容生产流水线组成:

  1. Unity 客户端负责输入、画面、音频、UI、本地预测和远端实体表现。
  2. Login、PlayerMgr、Lobby、Battle 等 .NET 节点负责认证、玩家业务、房间、权威战斗状态和结算。
  3. Redis、MySQL、ZooKeeper 分别承担共享缓存、长期持久化和服务注册发现。
  4. Excel、地图编辑场景、Protobuf、Jenny 等工具在运行前生成两端需要的代码和数据。
flowchart TB
    subgraph Offline[离线内容生产]
        Excel[Excel 配置] --> ConfigGen[配置生成]
        Scene[Unity 地图编辑场景] --> MapBake[地图 Gather / Bake / Export]
        Proto[Protobuf 定义] --> ProtoGen[协议代码生成]
        Entitas[Entitas 模板] --> Jenny[Jenny 代码生成]
    end

    subgraph Client[Unity 客户端进程]
        Bootstrap[ClientProxy / Zenject]
        ECS[Entitas Context / Systems]
        View[GameObject / UI / Animator / VFX]
        Net[NetworkManager]
        Bootstrap --> ECS --> View
        Net <--> ECS
    end

    subgraph Server[服务端集群]
        Login --> PlayerMgr --> Lobby
        Lobby --> TeamMgr --> Matching --> BattleMgr --> Battle
        Lobby --> Channel
        Lobby --> Ranking
        Battle --> Ranking
        Agent[Agent / 运营任务] --> Lobby
    end

    subgraph Data[基础设施]
        ZK[ZooKeeper]
        Redis[(Redis)]
        MySQL[(MySQL 分片)]
    end

    ConfigGen --> Client
    ConfigGen --> Server
    MapBake --> Client
    MapBake --> Battle
    ProtoGen --> Net
    ProtoGen --> Server
    Net <--> Login
    Net <--> Lobby
    Net <--> Battle
    Server <--> ZK
    Server <--> Redis
    PlayerMgr <--> MySQL

2. 五条必须先记住的边界

2.1 战区不是一个固定服务器地址

客户端最初只知道 Login 入口。Login/PlayerMgr 分配 Lobby,Lobby/房间流程再分配 Battle。战区是业务分组,不等价于单台进程。因此“7000 端口能连上”只说明入口可达,不能证明 Lobby 或 Battle 已注册、已被选中且客户端可访问。

2.2 Battle 是战斗状态的服务端边界

联网模式下,客户端先执行输入和本地表现,再向 Battle 发送请求。Battle 做实体归属、关卡进度、生命/动作状态和字段校验;成功时更新服务端记录并广播,失败时返回错误和服务端位置。当前 Battle 并不运行 Unity NavMeshAgent,也没有按 Unity NavMesh 逐帧模拟所有移动。

2.3 地图数据是前后端共同契约

Unity 编辑场景只是制作源。地图导出器生成的二进制数据会被客户端 MapProcessor 和 Battle 地图逻辑读取。导航 Prefab、地图 bytes、资源 GUID 索引和服务端地图副本必须同批发布,不能只更新 Unity 场景。

2.4 配置不是运行时直接读 Excel

Excel 是配置源,生成工具产出代码和二进制/文本数据,客户端与服务端分别加载生成物。修改字段时,要把“表结构、生成代码、两端加载、默认值、旧数据兼容”看成一次完整改动。

2.5 Jenny 不是游戏后端

Jenny-Server.bat 启动的是 Entitas 代码生成服务,用于生成组件、Matcher、Context 等客户端代码。它不提供 Login/Lobby/Battle,也不能让 Unity 登录本地战区。

3. 前端、后端和共享契约

边界客户端拥有服务端拥有共同契约
登录账号输入、入口选择、会话连接身份校验、战区校验、节点分配Login/Lobby 协议、SessionId
大厅UI、请求发起、本地缓存展示玩家 Actor、背包/任务/队伍等业务状态Lobby 协议、配置 ID
战斗输入、预测、动画、特效、远端表现Battle Actor、实体归属、权威字段、广播和结算Battle 协议、实体 ID、地图/配置版本
地图加载场景资源、NavMesh、光照和表现对象加载地图逻辑数据、刷怪与规则地图 bytes、模板 ID、坐标和区域语义
数据临时 UI/表现缓存Redis 共享状态、MySQL 长期状态Protobuf、配置生成物、版本号

判断一个 Bug 属于哪一端,不要看“现象出现在哪块屏幕”,而要问:错误值第一次在哪个边界产生?例如远端角色抖动可能来自客户端 PacketCache,也可能来自服务端广播源位置,或者来自两端地图版本不一致。

4. 一次玩家旅程怎样穿过系统

sequenceDiagram
    participant C as Unity Client
    participant L as Login
    participant PM as PlayerMgr
    participant LB as Lobby
    participant BM as BattleMgr
    participant B as Battle
    participant R as Redis/MySQL

    C->>L: 连接入口、请求战区、登录
    L->>R: 校验账号、战区和令牌
    L->>PM: 创建/恢复玩家会话
    PM->>R: 加载玩家数据
    PM->>LB: 选择低负载 Lobby
    PM-->>L: Lobby PublicAddr + SessionId
    L-->>C: 登录成功和 Lobby 会话信息
    C->>LB: 建立 Lobby 连接
    LB-->>C: 玩家数据、大厅状态
    C->>LB: 组队/匹配/创建战斗
    LB->>BM: 请求可用 Battle 节点
    BM->>B: 创建 Battle Actor
    B-->>LB: BattleId + 客户端地址
    LB-->>C: Battle 地址、BattleId、PlayerId
    C->>B: 建立 Battle 连接并进入场景
    C->>B: 战斗操作请求
    B-->>C: 战斗状态、实体生命周期、结算

这里存在三条不同的客户端连接:Login、Lobby、Battle。切换会话时使用的地址不是客户端猜出来的,而是上一个服务阶段返回的可访问地址。容器部署或本地隔离配置中最常见的问题,就是服务端注册了客户端无法访问的内网地址。

5. 服务端为什么拆成 Manager 和 Worker

QMGame 的 BattleMgr/Battle、TeamMgr/Team、ChannelMgr/Channel 采用相似结构:

  • Manager 保存可用 Worker 节点和负载分区,负责选择目标节点。
  • Worker 进程承载实际 Actor,并持续上报能力值。
  • 每个战斗、队伍或频道拥有自己的业务 ID,后续消息必须路由回持有对应 Actor 的节点。
  • 增加 Worker 可以横向扩容新对象容量,但不能随意把一个正在运行的 Actor 请求发到另一节点。
flowchart LR
    Caller[Lobby / Matching] --> Manager[BattleMgr]
    Manager --> Zone[NodeMgr / ZoneMgr]
    Zone -->|Low| B1[Battle Worker A]
    Zone -->|Middle| B2[Battle Worker B]
    Zone -->|High| B3[Battle Worker C]
    B1 --> BA1[Battle Actor 1]
    B1 --> BA2[Battle Actor 2]
    B2 --> BA3[Battle Actor 3]

GetFreeNode() 优先低负载区;部分创建链路使用 GetFreeNodeEx(),必要时扩大选择范围。Battle Worker 通常用当前战斗数作为 Ability 上报。详细类和创建时序见 03 服务端节点实现与分布式协调。

6. ZooKeeper、Redis、MySQL 各自解决什么

6.1 ZooKeeper:谁活着、地址在哪里

服务节点通过临时节点注册自己。进程或会话消失后,临时节点随之删除;Watcher 和周期刷新帮助 Manager 感知节点变化。它解决的是注册发现和配置变化,不保存玩家战斗过程。

6.2 Redis:多节点共享的短中期状态

Redis 用于登录令牌、战区列表、在线状态、排行、房间索引、活动状态等需要跨进程访问的数据。Redis 可用不代表某个 Lobby/Battle Actor 仍然存在,缓存索引与内存 Actor 必须结合判断。

6.3 MySQL:需要长期保存的业务数据

账号、角色、充值和日志等长期数据进入 MySQL,并按业务 key 选择分片。Lobby 中的 Player Actor 是在线工作副本,不是持久化数据库本身。新增字段必须检查 EF Entity/Context、分片路由、Protobuf、Redis 缓存和保存时机。

7. 状态应该去哪里找

想确认的状态第一检查点不应误判为
玩家是否通过登录Login 日志、令牌/战区 Redis 数据Lobby 已可用
玩家分配到哪个 LobbyPlayerMgr 分配结果、SessionIdUnity ServerCfg 中的 Login 地址
某场 Battle 在哪里BattleMgr 的节点选择和 BattleId 路由任意 Battle 端口
玩家背包/任务当前值在线时先看 Lobby Player Actor,再看保存链路客户端 UI 文本
长期角色数据MySQL 分片及保存日志Redis 中的临时索引
远端角色画面位置Battle 广播源值 + 客户端 PacketCache/纠偏只看 Transform
目标点是否可走客户端同版本 NavMesh 路径结果;若要强权威需服务端导航能力Battle 已运行 Unity NavMesh

8. 部署和本地隔离的最小闭环

至少要形成以下闭环,才能说“本地服务器可联机”:

  1. ZooKeeper、Redis、MySQL 可达,基础战区数据已初始化。
  2. Login、PlayerMgr、Lobby、BattleMgr、Battle 按依赖顺序启动并注册。
  3. Worker 向 Manager 上报能力,GetFreeNode 不为空。
  4. 服务端返回的 PublicAddr:Port 对 Unity 所在机器可访问。
  5. Unity Editor 的 ServerCfg.asset 只指向 Login 入口;Lobby/Battle 地址由服务端返回。
  6. 客户端与服务端协议、配置和地图数据来自同一版本。

排查时按 Login -> PlayerMgr -> Lobby -> BattleMgr -> Battle 顺序追踪同一个 playerId/SessionId/BattleId,不要只盯最后一条客户端报错。

9. 进一步阅读


下一篇:Unity 客户端运行架构