第 36 章 · 对象池与生成系统
这一章解决:子弹、敌人、卡牌、掉落物如何生成、复用、归还,并让迟入玩家看到正确激活状态。
先看一下
轻实时玩法会不断生成东西:子弹飞出、敌人刷出、掉落物出现、卡牌被翻开。普通 Unity 项目可以频繁 Instantiate 和 Destroy,但 VRChat 世界更适合预放置和复用。生成系统一旦失控,性能、Owner、迟入恢复和网络预算会一起出问题。
这一章会拿到什么
- 对象池的生命周期:预放置、取出、初始化、归还
VRCObjectPool的 Owner 边界- 生成请求走
GameState裁决的最小模式 - 池耗尽时的降级策略
- 一份对象池测试矩阵
依赖前面
- 第 5 章的池化兜底
- 第 12 章的请求式架构
- 第 20 章的幂等处理
- 第 26 章的对象池接口边界
这是 difficulty 4 章节。只想先完成最小主项目时,可以先跳到第八部的闭环,再回来看本章的高风险系统。
对象池解决的是生命周期
Section titled “对象池解决的是生命周期”对象池(Object Pool)把「创建 / 销毁」改成「取出 / 归还」。对象在场景里预先存在,平时 inactive,需要时激活,用完再 inactive。
| 阶段 | 做什么 | 谁负责 |
|---|---|---|
| 预放置 | 在场景里放好固定数量对象 | 制作者 |
| 登记 | 把对象放进 VRCObjectPool 数组 | 制作者 |
| 取出 | TryToSpawn() 激活一个空闲对象 | pool Owner |
| 初始化 | 设置位置、朝向、HP、生命周期 | 对象 Owner 或生成器 |
| 使用 | 执行移动、碰撞、表现 | 对象自身 |
| 归还 | Return(obj) 回到池里 | pool Owner |
第 5 章已经把 VRCObjectPool 放进迟入恢复的档③:迟入玩家会看到池里哪些对象当前 active。注意它只同步激活状态。对象自己的 HP、目标、倒计时仍然要靠对象上的字段或本地初始化规则恢复。
Pool Owner 决定能不能取出
Section titled “Pool Owner 决定能不能取出”VRCObjectPool 是网络组件。调用 TryToSpawn() 和 Return() 的一方必须处理 Owner 边界。更稳的做法是:生成请求交给 GameState Owner 或指定生成器 Owner,由它统一取对象。
public VRCObjectPool enemyPool;
public GameObject TrySpawnEnemy(Vector3 position, Quaternion rotation){ if (!Networking.IsOwner(enemyPool.gameObject)) return null;
GameObject obj = enemyPool.TryToSpawn(); if (obj == null) return null;
obj.transform.SetPositionAndRotation(position, rotation); return obj;}如果每个玩家按按钮时都抢 pool Owner,再立刻取对象,所有权切换会变成额外网络成本。合作防守里的敌人刷出应由 GameState Owner 按波次执行;玩家技能生成物可以先走 PlayerObject 请求,再由 GameState 裁决是否生成。
生成请求要幂等
Section titled “生成请求要幂等”对象池最容易出重复生成:玩家连点,回执丢失后重发,Owner 转移前后同一请求被处理两次。第 20 章的 requestId 继续使用。
private bool AcceptSpawnRequest(int playerId, int requestId, byte spawnType){ int slot = GetPlayerSlot(playerId); if (slot < 0) return false; if (requestId <= lastProcessedRequestId[slot]) return false; if (phase != PHASE_INGAME) return false; if (!HasSpawnBudget(spawnType)) return false;
lastProcessedRequestId[slot] = requestId; return true;}这里继续沿用第 12 章的防御写法:先把 playerId 转成受控槽位,再访问数组。教程片段里的数组长度只是示意,项目里要按房间容量和离开清理策略确定。
处理通过后再从池里取对象。取出失败不应该让请求无限重试。池满时要给明确回执:冷却中、对象不足、当前阶段不允许生成。
对象每次启用都要重置
Section titled “对象每次启用都要重置”池化对象不会重新执行构造流程。每次从池里取出时,要重置运行状态:HP 回到默认值,状态回到 IDLE,目标清空,生命周期计时重新安排,必要时再 RequestSerialization()。
初始化入口最好是显式方法,例如 OnSpawned,而不是依赖 Start()。Start() 只适合场景加载时做一次缓存。对象反复取出时,HP、状态、计时器、目标、视觉残留都要在 spawn 入口重置。归还入口同理:先把对象状态写成不可交互,再交回 pool。
迟入恢复要拆两层
Section titled “迟入恢复要拆两层”对象池只能告诉迟入玩家「这个对象此刻 active」。它不会自动告诉迟入玩家「这只怪还剩多少 HP、正在追谁、属于第几波」。所以池化生成物要拆两层恢复。
| 层级 | 恢复来源 | 例子 |
|---|---|---|
| 是否存在 | VRCObjectPool active 状态 | 第 3 只敌人此刻激活 |
| 当前状态 | 对象自己的同步字段 | HP、状态、目标、所属波次 |
| 全局进度 | GameState | 当前波次、剩余敌人数、阶段 |
如果某个对象只是一段持续特效,例如 3 秒火焰圈,迟入玩家看不见过去两秒也可以接受,它可以不进入同步对象池。影响规则的对象才需要池化兜底。
池耗尽是游戏状态
Section titled “池耗尽是游戏状态”池耗尽时,TryToSpawn() 会失败。这个失败要可见、可控、可测试。
常见处理有三种。第一种是拒绝生成:玩家技能进入失败回执,UI 显示资源不足。第二种是复用最旧对象:适合纯视觉特效,不适合敌人和掉落物。第三种是降级生成:不刷实体,只播本地特效或增加聚合计数。
合作防守敌人池建议拒绝或延迟,不要偷偷复用未死亡敌人。子弹和短特效可以复用最旧对象,但要保证不影响分数和 HP。
什么时候别做复杂对象池
Section titled “什么时候别做复杂对象池”| 情况 | 更稳的做法 |
|---|---|
| 物体只出现一次 | 场景里直接放置,切 active 即可 |
| 特效很短且不影响规则 | 本地播放,不进网络池 |
| 池大小难以估计 | 先做最大同时存在数量测试 |
| 对象状态多到难以重置 | 拆对象类型,减少复用范围 |
| 每次生成都要抢 Owner | 改成集中生成器或 GameState 裁决 |
对象池不是越通用越好。一个「万能生成器」同时生成敌人、卡牌、子弹、掉落物,会把初始化和归还规则混在一起。更稳的结构是按对象类型拆池:EnemyPool、ProjectilePool、DropPool。
挑一条试。
- 把敌人池大小从 20 改成 5。第 6 个敌人应该被拒绝、延迟,还是替换最旧敌人?
- 给池化子弹加一个「命中后归还」入口。哪些状态要在下一次
OnSpawned里清掉?
| 场景 | 预期 | 实际 |
|---|---|---|
| 1. 正常生成敌人 | pool Owner 取出对象,对象执行初始化并同步状态 | … |
| 2. 玩家连点生成请求 | requestId 阻止重复生成 | … |
| 3. 池耗尽 | 返回明确失败,不无限重试,不创建新对象 | … |
| 4. 对象死亡归还后再次取出 | HP、状态、目标、速度和特效残留被重置 | … |
| 5. 迟入玩家进入 | active 对象存在,HP / 状态 / 波次可恢复 | … |
- 能解释
VRCObjectPool只同步 active 状态 - 能说出 pool Owner 为什么重要
- 能用
requestId防止重复生成 - 能列出池化对象每次取出必须重置的字段
- 能给池耗尽设计一个降级策略
- VRChat Creator Docs · Network Components —
VRC Object Pool的官方组件定位。 - UhiyamaLab · Optimization with VRCObjectPool(访问日期:2026-06-17)— VRChat 子弹与特效对象池的社区教程。
- 第 5 章 · 迟入玩家怎样恢复世界 — 池化兜底的分类。
- 第 20 章 · 版本号、序号与幂等 — 重复生成请求的处理。