系统全景:总体架构与分布式拓扑 private
系统全景:总体架构与分布式拓扑
本篇只回答系统由哪些部分组成、每部分拥有哪类状态、请求怎样跨节点流动。Unity 局部实现见客户端分区;Actor 和节点代码见服务端分区;单次战斗包的端到端过程见联机同步分区。
1. 一句话认识 QMGame
QMGame 不是“Unity 客户端连接一台游戏服务器”,而是由三类运行时和一套内容生产流水线组成:
- Unity 客户端负责输入、画面、音频、UI、本地预测和远端实体表现。
- Login、PlayerMgr、Lobby、Battle 等 .NET 节点负责认证、玩家业务、房间、权威战斗状态和结算。
- Redis、MySQL、ZooKeeper 分别承担共享缓存、长期持久化和服务注册发现。
- 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 已可用 |
| 玩家分配到哪个 Lobby | PlayerMgr 分配结果、SessionId | Unity ServerCfg 中的 Login 地址 |
| 某场 Battle 在哪里 | BattleMgr 的节点选择和 BattleId 路由 | 任意 Battle 端口 |
| 玩家背包/任务当前值 | 在线时先看 Lobby Player Actor,再看保存链路 | 客户端 UI 文本 |
| 长期角色数据 | MySQL 分片及保存日志 | Redis 中的临时索引 |
| 远端角色画面位置 | Battle 广播源值 + 客户端 PacketCache/纠偏 | 只看 Transform |
| 目标点是否可走 | 客户端同版本 NavMesh 路径结果;若要强权威需服务端导航能力 | Battle 已运行 Unity NavMesh |
8. 部署和本地隔离的最小闭环
至少要形成以下闭环,才能说“本地服务器可联机”:
- ZooKeeper、Redis、MySQL 可达,基础战区数据已初始化。
- Login、PlayerMgr、Lobby、BattleMgr、Battle 按依赖顺序启动并注册。
- Worker 向 Manager 上报能力,
GetFreeNode不为空。 - 服务端返回的
PublicAddr:Port对 Unity 所在机器可访问。 - Unity Editor 的
ServerCfg.asset只指向 Login 入口;Lobby/Battle 地址由服务端返回。 - 客户端与服务端协议、配置和地图数据来自同一版本。
排查时按 Login -> PlayerMgr -> Lobby -> BattleMgr -> Battle 顺序追踪同一个 playerId/SessionId/BattleId,不要只盯最后一条客户端报错。
9. 进一步阅读
- 客户端内部:02 Unity 客户端运行架构、11 客户端启动模块。
- 服务端内部:03 服务端节点实现与分布式协调。
- 登录与切服:04 网络、登录与协议、12 网络会话模块。
- 战斗端到端:22 联机同步:移动包、预测、广播与纠偏。
- 地图共享契约:05 地图编辑、导出与双端加载、13 地图导出模块。