第 10 章 · CyanPlayerObjectPool:历史方案与兼容案例
这一章解决:读懂用
CyanPlayerObjectPool写的旧世界源码,并把这一类 prefab 池模式迁移到VRCPlayerObject。
先看一下
VRCPlayerObject 是 SDK 内置功能,但它进入 SDK 的时间不算长。在它之前,社区里 per-player 对象的事实标准是 CyanPlayerObjectPool:场景里预先摆好 N 份预制体(N 是房间最大人数),玩家加入时手动从池里抢一份分配给他,离开时回收。许多线上世界仍然在用这套实现。
直接读 CyanPlayerObjectPool 的源码会卡在「为什么要这么设计」上。这一章把它当作谱系标本:它解决过什么、为什么现在不再推荐、迁移到 VRCPlayerObject 的字段对照。
这一章会拿到什么
- 一份「
CyanPlayerObjectPool解决过的三类问题」清单:join race、Master 验证、per-player 对象池 - 当前归档状态与迁移决策表
- 一份字段对照表:旧 Pool 上的
_GetPlayerPooledObject等接口在VRCPlayerObject里对应什么
依赖前面
- 第 9 章
VRCPlayerObject的工作机制 - 第 3 章 Owner 与 Master 的边界
- 第 5 章迟入恢复
它解决过什么
Section titled “它解决过什么”时间倒回 VRCPlayerObject 进入 SDK 之前。要给每位玩家一份对象,社区只能用对象池:
- 场景里预先放 N 份预制体(比如 N=80),全部 inactive。
- 玩家加入实例时,由 Master 客户端从池里挑一份,调用
Networking.SetOwner把这份对象的 Owner 设为这位玩家。 - 玩家离开时,由 Master 把这份对象重置 + 回收。
听起来直观,工程上要解决三类难题:
难题 1:join race(加入竞争)
Section titled “难题 1:join race(加入竞争)”两位玩家几乎同时加入实例,两份代码同时跑「找一份空的预制体分给他」。如果两台客户端各自挑了同一份预制体,会发生重复分配。CyanPlayerObjectPool 的解决办法是把分配权固定在 Master 客户端,并加上分配请求的请求队列,逐个处理。
难题 2:Master 客户端崩溃后的状态回收
Section titled “难题 2:Master 客户端崩溃后的状态回收”Master 客户端在玩家离开瞬间崩溃。这份玩家原本拥有的对象处于「Owner 已经离开、回收逻辑没跑完」的不一致状态。CyanPlayerObjectPool 的解决办法是新 Master 接管时扫描所有池对象,发现 Owner 已离开就强制回收。
难题 3:per-player 对象池本身的维护
Section titled “难题 3:per-player 对象池本身的维护”预制体数量、命名、生命周期、Owner 检查、字段重置时机、迟入扫描,每一项都需要写代码。社区的工程经验集中在这一份仓库里,许多线上世界因此能直接用。
当前归档状态
Section titled “当前归档状态”CyanPlayerObjectPool 仓库已于 2024 年 10 月 11 日由作者归档,最后一个正式版本是 2023 年 5 月 5 日的 v1.1.2。归档不等于代码不能跑,意思是:作者不再接受新功能 PR 和 bug 修复。VRChat 官方在 SDK 中加入 VRCPlayerObject 后,三类难题被 SDK 内部接管,社区轮子的工程价值下降。
下面这张表把两套机制对齐:
| 维度 | CyanPlayerObjectPool | VRCPlayerObject |
|---|---|---|
| 对象生成 | 场景预先放 N 份预制体,运行时分配 | SDK 在玩家加入时按场景模板克隆 |
| Owner 设置 | 调用 SetOwner 把对象交给玩家 | SDK 在克隆时锁定 Owner,不可变 |
| 数量限制 | 必须预先设定 N(一般 80) | 跟随实例最大人数 |
| join race 处理 | 仓库内自维护请求队列 | SDK 内部处理 |
| Master crash 回收 | 新 Master 扫描所有对象 | SDK 自动销毁离开玩家的对象 |
| 持久化 | 不直接支持 | 与 VRCEnablePersistence 集成 |
| 当前状态 | 归档(2024-10),仍可用于读懂老世界 | 推荐方案 |
新项目应当直接用 VRCPlayerObject。线上仍然在跑的旧世界没必要立刻迁移,迁移成本超过收益。要迁移,可以参照下面的对照表分阶段切。
字段与接口对照
Section titled “字段与接口对照”读老世界源码的查表。Pool 一侧列的是常见公开接口(具体名以仓库版本为准)。
| Pool 一侧 | PlayerObject 一侧 |
|---|---|
_GetPlayerPooledObject(VRCPlayerApi player) | Networking.GetPlayerObjects(player) 返回 GameObject[],再 GetComponentInChildren<T>() |
_OnLocalPlayerAssigned() | 该玩家的 PlayerLobbyState.Start(Owner 是本地玩家时) |
_OnPlayerAssigned(VRCPlayerApi) | 监听 OnPlayerJoined + 等 PlayerObject Start 完成 |
_OnPlayerUnassigned(VRCPlayerApi) | OnPlayerLeft(PlayerObject 此时已经在销毁过程中) |
| 池容量配置 | 跟随实例最大人数,无需配置 |
| 池预制体引用 | 场景里挂 VRCPlayerObject 组件的模板 GameObject |
老代码里如果出现「某位玩家的对象槽位 index」(如 pooledObjects[i]),它的含义是「这位玩家在池数组里的下标」,迁移时整段下标维护可以删除,PlayerObject 不需要这套。
线上世界什么情况下值得迁移:
| 情况 | 决策 |
|---|---|
| 旧世界跑得好,没新功能开发 | 不迁移。运行良好的代码不是技术债 |
| 计划做每位玩家一份对象级持久化 | 迁移。VRCEnablePersistence 是 PlayerObject 持久化路径的一部分;简单键值存档则另走 PlayerData |
| 现有玩家数已经接近 Pool 容量上限 | 迁移。Pool 容量需要手动调整,PlayerObject 跟随实例 |
| 想加新的 per-player 字段,且改动量小 | 不迁移,沿用旧 Pool 加字段就行 |
| 新世界 / 重构 | 直接用 PlayerObject,不再走 Pool |
迁移本身要分两步:先把字段接口从 Pool 切到 PlayerObject,再删除 Pool 自维护的请求队列、Master crash 扫描、Owner 设置等逻辑。第一步不删旧代码、只补新接口,保证两套并行能跑通;第二步再清理。
不同世界不同活法
Section titled “不同世界不同活法”线上世界改造决策不只看技术指标。
- 大型成熟世界(已上线一年以上、玩家社群稳定):除非要做持久化,迁移收益低。把它当作技术谱系遗物保留,新功能用其他方式扩展。
- 小型实验世界(朋友圈跑,不到一个月):直接迁移。改一次代码相当于把旧 Pool 的所有概念过一遍,下一个项目就不再走旧路。
- 计划合作的混合项目(多位作者,部分人熟悉 Pool):在 README 里写清楚现在用的是哪一套,新加的代码统一用 PlayerObject。两套并存的项目最容易在文档缺失时被新人误读。
- 找一个开源的、用
CyanPlayerObjectPool的 VRChat 世界(GitHub 搜CyanPlayerObjectPool关键字),读一份_OnPlayerAssigned的实现,画出它的「分配 → SetOwner → 字段重置 → 通知 UI」时序。再用VRCPlayerObject写一份等价代码,对比两边代码量。 - 把第 9 章的
PlayerLobbyState故意改成「场景里手动放 8 份预制体,由 Master 用 SetOwner 分配」。跑两个客户端,观察 join race 的具体表现(两位玩家几乎同时加入会发生什么)。看完恢复回来。
本章不引入新代码,复用第 9 章的 PlayerLobbyState 测试矩阵即可。如果在迁移老世界,加一行专门项:
| 场景 | 预期 | 实际 |
|---|---|---|
| 旧 Pool 与新 PlayerObject 并存运行 | 两套代码读到的 isReady 在同一帧内一致 | … |
迁移过程中只要这一行短暂出现不一致,立刻回退到全量旧 Pool 模式,再按字段排查。
- 能说出
CyanPlayerObjectPool解决过的三类问题 - 能复述当前 SDK 下三类问题分别由谁接管
- 能说出迁移决策表里至少两条「值得迁移」和两条「不值得迁移」的情况
- 能识别老代码里池下标维护逻辑(
pooledObjects[i]、assigned[i]等)
- GitHub · CyanLaser/CyanPlayerObjectPool:仓库 README、归档说明与历史 release。
- VRChat Creator Docs · VRCPlayerObject:当前官方 PlayerObject 方案。
- VRChat Creator Docs · Object Ownership:
SetOwner与 Owner 转移的官方说明。 - 第 9 章 · VRCPlayerObject 边界卡:适合 / 不适合放 PlayerObject 的清单。
- 附录 · 术语表:
Object Pool、Owner Assignment、Join Race的客观定义。