第二部理解章 · 权威拓扑先于代码
这一节不要求动手。把第二部七章揉成一个能贯穿 Vol.2 后续部的心智模型,对仗第一部理解章 A「同步不是广播,是状态复制」。
读 VRChat 多人项目源码时,可以扫一遍代码里 Networking.SetOwner 出现的次数。
如果一份脚本里出现五次以上 SetOwner,几乎可以断定:这个项目的权威拓扑没设计清楚。每次 SetOwner 都是在用代码补救「这份状态此刻属于谁」这个本应在设计阶段就回答的问题。
第二部的全部章节都在做一件事:在写第一行代码之前,把权威拓扑画清楚。
把第二部读成一句话
Section titled “把第二部读成一句话”第二部七章 + 闭环 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 把同一套权威拓扑套到「两人回合制猜数字」上,证明它能换玩法。
写代码前的四个问题
Section titled “写代码前的四个问题”每次写到「这个值要同步」,可以回头问四个问题。在第一部理解章 A 的四问基础上扩展:
1. 这份状态是 per-player 的,还是房间共享的? per-player → PlayerObject 房间共享 → GameState
2. 谁拥有写权? PlayerObject 字段 → 玩家本人写 GameState 字段 → GameState Owner 写 要写 PlayerObject 字段但写者不是 Owner → 走批准回执
3. 这份状态的 Owner 离开后谁接管? PlayerObject → 自动销毁,要清理它在 GameState 留下的痕迹 GameState → 候选队列下一位(或固定 / 推举)
4. 写操作是「立刻发」还是「写请求字段」? 高频、可丢、过了就过了 → SendCustomNetworkEvent 要可恢复、要审计、要等 Owner 处理 → 写请求字段四个问题答完,权威拓扑就清晰了。代码里 SetOwner 这一行很少需要写。
把第二部写成一张图
Section titled “把第二部写成一张图” ┌─────────────────────────────────┐ │ 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 字段」的箭头。这是权威拓扑的核心约束。
第二部的三句话
Section titled “第二部的三句话”权威拓扑要在写代码前画清楚。每份共享状态都有一位托管者:玩家的状态由玩家本人托管,房间的状态由 GameState Owner 托管,玩家通过请求字段向房间状态提交修改。代码里反复出现 SetOwner 通常意味着这一步还没做完。
第三部「游戏循环」处理大厅、准备、队伍、开局、结算、观战这些贯穿一局的流程。第二部给的工具足够支撑:per-player 状态用 PlayerObject、房间状态用 GameState、状态修改走请求式架构、Owner 离开走候选队列。第三部的章节会把这些工具组合成「玩家实际接触到的游戏骨架」。
第四部「数据通道」处理 requestId 之外的工程化问题:stateVersion、命令模式、VRCJson、字节打包。闭环 3 在那里做。
- 第 8 章 · 玩家进入房间后系统要给他什么:六阶段生命周期。
- 第 9 章 · VRCPlayerObject 边界卡:「适合 / 不适合」清单。
- 第 11 章 · GameState 三种 Owner 策略:三种策略与三个反向案例。
- 第 12 章 · 请求式架构:请求字段与
lastProcessedRequestId。 - 第 14 章 · Owner 失效恢复检查表:写完每个 GameState 类对象都要过的清单。
- 闭环 2 · PlayerObject + GameState 中级一局:第二部所有工具的合订。
- 附录 · 术语表:第二部所有术语。