跳转到内容

第二部理解章 · 权威拓扑先于代码

约 4 分钟 难度:2

这一节不要求动手。把第二部七章揉成一个能贯穿 Vol.2 后续部的心智模型,对仗第一部理解章 A「同步不是广播,是状态复制」。


读 VRChat 多人项目源码时,可以扫一遍代码里 Networking.SetOwner 出现的次数。

如果一份脚本里出现五次以上 SetOwner,几乎可以断定:这个项目的权威拓扑没设计清楚。每次 SetOwner 都是在用代码补救「这份状态此刻属于谁」这个本应在设计阶段就回答的问题。

第二部的全部章节都在做一件事:在写第一行代码之前,把权威拓扑画清楚。


第二部七章 + 闭环 2 + 对照小练 B 可以折叠成:

每份共享状态都有一位托管者。玩家自己的状态由玩家本人托管(PlayerObject),房间共享的状态由 GameState Owner 托管(候选队列推举)。玩家想改房间状态时,他写「请求」字段到自己的 PlayerObject,GameState Owner 读到后裁决并写回房间状态。

每一章都是这句话的一个面。

第 8 章 回答有哪些状态。把玩家进入到离开六阶段的诉求拆成六类,前四类需要 per-player 同步存储。

第 9 章 回答玩家自己的状态归谁VRCPlayerObject 给每位玩家一份对象,Owner 锁死为他本人,写权天然干净。边界卡列出适合 / 不适合放 PlayerObject 的字段。

第 10 章CyanPlayerObjectPool 当作历史标本:在没有原生 PlayerObject 的年代,社区怎么解决这套问题。让读者读懂老代码,并知道为什么现在不再走这条路。

第 11 章 回答房间共享的状态归谁GameState 是状态的物理载体,三种 Owner 策略对应不同玩法节奏。首节明示:GameState Owner 是状态提交者,不是可信服务器。

第 12 章 回答玩家怎么向房间状态提交修改。请求字段写在 PlayerObject 上,GameState Owner 扫描处理。requestId / lastProcessedRequestId 让请求成为持久状态,可恢复、可观察、可调试。

第 13 章 回答多人同时修改怎么办。先到先批 + 批准回执 + 冷却。第三类冲突(状态版本错位)留给闭环 3 的 stateVersion

第 14 章 回答托管者离开了怎么办。三类对象走三条不同恢复路径。GameState 用候选队列;PlayerObject 自动销毁,但要清理它在 GameState 留下的痕迹;物理对象第 7 部专门讲。

闭环 2 把上面每一章放进一个能跑的项目,证明这套权威拓扑能修闭环 1 的两个缺陷,并故意暴露三个新缺陷作为闭环 3 的动机。

对照小练 B 把同一套权威拓扑套到「两人回合制猜数字」上,证明它能换玩法。


每次写到「这个值要同步」,可以回头问四个问题。在第一部理解章 A 的四问基础上扩展:

1. 这份状态是 per-player 的,还是房间共享的?
per-player → PlayerObject
房间共享 → GameState
2. 谁拥有写权?
PlayerObject 字段 → 玩家本人写
GameState 字段 → GameState Owner 写
要写 PlayerObject 字段但写者不是 Owner → 走批准回执
3. 这份状态的 Owner 离开后谁接管?
PlayerObject → 自动销毁,要清理它在 GameState 留下的痕迹
GameState → 候选队列下一位(或固定 / 推举)
4. 写操作是「立刻发」还是「写请求字段」?
高频、可丢、过了就过了 → SendCustomNetworkEvent
要可恢复、要审计、要等 Owner 处理 → 写请求字段

四个问题答完,权威拓扑就清晰了。代码里 SetOwner 这一行很少需要写。


┌─────────────────────────────────┐
│ GameState │
│ Owner: 候选队列推举 │
│ 字段: phase / score / lastProc │
│ 读: 所有人;写: GameState Owner │
└────────┬────────────┬────────────┘
▲ │ 同步广播
│ 处理 ▼
┌──────────┴───────┐ ┌────────────┐
│ OnPlayerRequest │ │ 所有客户端 │
│ Updated 扫描 │ │ UI 应用 │
└──────────┬───────┘ └────────────┘
│ 读
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ PlayerObj A │ │ PlayerObj B │ │ PlayerObj C │
│ Owner: A 锁 │ │ Owner: B 锁 │ │ Owner: C 锁 │
│ 字段: │ │ 字段: │ │ 字段: │
│ isReady │ │ isReady │ │ isReady │
│ requestId │ │ requestId │ │ requestId │
│ reqType │ │ reqType │ │ reqType │
└─────────────┘ └─────────────┘ └─────────────┘

每位玩家拥有自己的 PlayerObject,写权天然干净。GameState 是房间共享状态的中心,由候选队列推举的 Owner 写。请求从 PlayerObject 流向 GameState,状态变化从 GameState 流回所有客户端的 UI。

整张图没有任何「玩家直接写 GameState 字段」或「GameState 直接写 PlayerObject 字段」的箭头。这是权威拓扑的核心约束。


权威拓扑要在写代码前画清楚。每份共享状态都有一位托管者:玩家的状态由玩家本人托管,房间的状态由 GameState Owner 托管,玩家通过请求字段向房间状态提交修改。代码里反复出现 SetOwner 通常意味着这一步还没做完。


第三部「游戏循环」处理大厅、准备、队伍、开局、结算、观战这些贯穿一局的流程。第二部给的工具足够支撑:per-player 状态用 PlayerObject、房间状态用 GameState、状态修改走请求式架构、Owner 离开走候选队列。第三部的章节会把这些工具组合成「玩家实际接触到的游戏骨架」。

第四部「数据通道」处理 requestId 之外的工程化问题:stateVersion、命令模式、VRCJson、字节打包。闭环 3 在那里做。