第 40 章 · 从玩法反推架构决策记录
这一章解决:完整项目开始前,先把「谁写状态、状态走哪条通道、迟入怎么恢复、出问题怎么降级」写成一页可执行的 ADR。
先看一下
合作防守世界很容易从「先刷一只怪」开始写。这样进度看起来快,但后面会连续遇到同一类问题:玩家 HP 放哪里、敌人死亡谁裁决、波次时间谁推进、迟入玩家看到第几波、Owner 离开后哪一份状态还算数。第八部先写 ADR,再写代码。
这一章会拿到什么
- 一份合作防守世界的核心循环说明
- 一张状态归属表:PlayerObject、GameState、EnemyObject、Local 各负责什么
- 一张同步通道表:同步字段、Network Event、ObjectPool、ObjectSync、本地表现各放什么
- 一组最小 ADR 条目,可直接带到第 41 章闭环 4
- 一份项目级测试矩阵
依赖前面
- 第 12 章的请求式架构:玩家对象请求,
GameState裁决 - 第 20 章的
requestId与stateVersion - 第 26 章的轻实时范式:事件做反馈,状态做共识
- 第 36 章的对象池生命周期
- 理解章 G 的降级优先级
先把玩法写成一条循环
Section titled “先把玩法写成一条循环”第八部主项目是一局 2–4 人合作防守。玩家在大厅准备,开局后敌人按波次出现,玩家守住目标并获得分数,时间到或目标被破坏后进入结算,再回到大厅重开。
这条循环先写成状态机,不写成脚本。
| 阶段 | 玩家看到什么 | 系统必须保存什么 |
|---|---|---|
LOBBY | 玩家加入、准备、选队伍或角色 | 每位玩家的 ready / team / role,当前房间是否能开局 |
COUNTDOWN | 倒计时、队伍锁定、出生点确认 | 倒计时开始时间、目标阶段、锁定后的队伍快照 |
INGAME | 波次推进、敌人出现、玩家受击和得分 | 当前波次、波次开始时间、剩余敌人、团队分数、每位玩家 HP |
RESULT | 胜负、分数、MVP、重开按钮 | 结算原因、最终分数、每位玩家贡献快照 |
RESTARTING | 清场、归还对象池、准备回大厅 | 清理进度、下一局 matchId |
第 41 章最小可玩版本只做这一条循环里最窄的一条路:LOBBY 进入 INGAME,刷一波目标,分数增加,进入 RESULT,再重开。第 42 章之后再把敌人和对象池补完整。
ADR 只记录会影响返工的决定
Section titled “ADR 只记录会影响返工的决定”ADR 是 Architecture Decision Record(架构决策记录)的简称。这里不用复杂模板,只保留四个问题:决定了什么,为什么这么做,不这样会怎样,后面怎么验证。
本卷的项目 ADR 采用这个格式:
| 字段 | 写什么 |
|---|---|
| 决策 | 一句话写清楚状态或对象归属 |
| 依据 | 来自哪章的规则,或来自官方约束 |
| 代价 | 放弃了什么能力,或引入什么测试成本 |
| 验证 | 用哪条测试矩阵证明它成立 |
它的范围很窄:只记录以后改错会返工的东西,主要是 Owner、同步通道、迟入恢复、降级入口。敌人数量、出生点坐标、UI 文案进入配置表或关卡说明,不进入 ADR。
合作防守世界的状态先分四块。
| 状态 | 归属 | 通道 | 迟入恢复 |
|---|---|---|---|
ready / teamId / roleId | 每位玩家的 PlayerObject | [UdonSynced] 字段 | 读每个玩家对象当前值 |
currentHP / isDowned | 每位玩家的 PlayerObject | [UdonSynced] 字段 | 读当前 HP 与倒地状态 |
requestId / requestType / requestPayload | 每位玩家的 PlayerObject | [UdonSynced] 字段 | GameState 用 lastProcessedRequestId 去重 |
phase / matchId / stateVersion | GameState | Manual sync | 读当前阶段与版本 |
waveIndex / waveStartServerTime | GameState | Manual sync | 从波次和时间恢复 UI |
teamScore / enemyAliveCount | GameState | Manual sync | 读当前聚合值 |
resultReason / finalScore | GameState | Manual sync | 结算屏直接恢复 |
| 单个敌人的 HP / 状态 / 目标 | EnemyObject | 对象自己的同步字段 | 读 active 对象字段 |
| 敌人是否存在 | VRCObjectPool | active / inactive 同步 | 迟入自动拿到 active 状态 |
| 子弹轨迹、枪口火光、按钮按下态 | Local | 本地变量 / 本地播放 | 不恢复 |
| 命中音效、击杀飘字 | Network Event | 参数化事件 | 不重放,必要结果写回状态 |
这张表有两个重点。
第一,GameState 只存全局聚合:阶段、波次、分数、剩余敌人数。它不保存每位玩家的 HP,也不保存每只敌人的全部状态。玩家状态归 PlayerObject,敌人状态归 EnemyObject。
第二,本地表现明确不进入迟入恢复。迟入玩家不需要补看 3 秒前的枪口火光,但必须能看到当前第几波、剩几个敌人、自己是否倒地。
同一局游戏里会同时用五条通道。第八部不再重新解释每条通道,只把它们落到项目对象上。
| 通道 | 项目里放什么 | 不放什么 |
|---|---|---|
| Local | UI 预演、音效、准星、按钮按下态、临时特效 | 分数、阶段、胜负、HP 最终值 |
| Network Event | 命中特效、击杀飘字、确认回包、短提示 | 迟入必须看到的状态 |
| PlayerObject 同步字段 | ready、team、role、HP、玩家请求、个人贡献 | 团队分、波次、敌人总数 |
| GameState 同步字段 | phase、matchId、stateVersion、waveIndex、teamScore、enemyAliveCount、result | 高频位置、单个敌人所有细节 |
| ObjectPool / EnemyObject | 敌人存在状态、单体 HP、目标、死亡状态 | 玩家 UI 预演、团队全局流程 |
| VRCObjectSync | 简单物理物体或可拾取防守道具的空间 / 基础 Rigidbody 状态 | 规则公平、伤害裁决、复杂 NPC AI |
Manual sync 的数字按当前官方文档写法处理:Continuous sync 单次约 200 bytes,Manual sync 单次约 280 KB,并且 Manual 对象会按 payload 大小受 rate limit 影响。第八部的工程策略是拆开高频变化:GameState 少字段、PlayerObject 分散 per-player 状态、敌人对象只同步必要状态,特效留本地。
确认回包属于低频辅助通道,不是私聊通道。官方没有 NetworkEventTarget.Player,单发给某个玩家通常要 All + playerId 本地过滤;这会让所有当前客户端收到事件,只是多数客户端忽略。高频确认、私密信息和迟入关键状态不要走这条路。
最小 ADR 条目
Section titled “最小 ADR 条目”下面是第八部主项目的第一版 ADR。第 41 章按这份记录搭闭环 4,第 42 到 45 章只做增量修改。
ADR-001:全局流程归 GameState
Section titled “ADR-001:全局流程归 GameState”| 项 | 内容 |
|---|---|
| 决策 | phase、matchId、waveIndex、teamScore、enemyAliveCount、resultReason 归 GameState |
| 依据 | 第 11 章:GameState Owner 是当前实例内的状态提交者;第 26 章:轻实时的波次和聚合状态归全局 |
| 代价 | GameState Owner 离开时必须能重选 Owner,并继续使用同步字段恢复 |
| 验证 | Owner 离开测试:phase、waveIndex、teamScore 不丢,重选后能继续推进 |
ADR-002:每位玩家的状态归 PlayerObject
Section titled “ADR-002:每位玩家的状态归 PlayerObject”| 项 | 内容 |
|---|---|
| 决策 | ready、team、role、HP、倒地状态、当前请求、个人贡献归玩家自己的 PlayerObject |
| 依据 | 第 9 章:PlayerObject 适合 per-player 状态;第 12 章:玩家对象请求,GameState 裁决 |
| 代价 | GameState 不能直接写玩家字段,需要批准回执或由玩家对象自己落地;个人字段不能替代最终结算快照 |
| 验证 | 迟入测试:新玩家能看到每位玩家当前 ready、team、HP;结算测试:最终 MVP 和奖励只读 GameState 快照;玩家离开后相关暂态被清理 |
ADR-003:敌人对象只同步单体必要状态
Section titled “ADR-003:敌人对象只同步单体必要状态”| 项 | 内容 |
|---|---|
| 决策 | 敌人 active 状态由 VRCObjectPool 管,单体 HP / 状态 / 目标由 EnemyObject 自己同步,GameState 只保存 enemyAliveCount |
| 依据 | 第 34 章:影响规则的 NPC 需要 Owner 驱动;第 36 章:对象池只同步 active 状态 |
| 代价 | 每个敌人对象都要有重置入口,池耗尽要有失败回执 |
| 验证 | 迟入战斗中加入:能看到 active 敌人、当前 HP、当前波次和剩余敌人数 |
ADR-004:体感先本地,结果等确认
Section titled “ADR-004:体感先本地,结果等确认”| 项 | 内容 |
|---|---|
| 决策 | 攻击按钮、音效、枪口火光和准星反馈本地先播;伤害、分数、敌人死亡等结果等 GameState 或 EnemyObject 确认 |
| 依据 | 第 29 章:本地预演只改表现,不改共识状态;第 30 章:确认回包用请求序号追踪 |
| 代价 | 本地预演可能被拒绝,需要 UI 撤销或短提示 |
| 验证 | 模拟 Owner 拒绝:本地按钮恢复,分数和 HP 没有提前跳变 |
ADR-005:对象池耗尽是可见状态
Section titled “ADR-005:对象池耗尽是可见状态”| 项 | 内容 |
|---|---|
| 决策 | 敌人池、投射物池、掉落物池分别建池;池耗尽时拒绝或延迟,不运行时创建新对象 |
| 依据 | 第 36 章:对象池把最大同时存在数量变成预算;理解章 G:复杂系统先证明能降级 |
| 代价 | 极端情况下会少刷敌人或少播特效,玩法要接受这个降级 |
| 验证 | 把敌人池容量调低:第 N 个敌人生成失败时不崩、不无限重试,UI 或日志给出原因 |
把不做的事也写进去
Section titled “把不做的事也写进去”ADR 还要记录项目第一版主动放弃的能力。这样第 41 章不会在最小可玩版本里偷偷膨胀。
| 第一版不做 | 原因 | 后续位置 |
|---|---|---|
| 精确 FPS 命中公平 | VRChat 没有自定义权威服务器,公平射击不是本项目承诺 | 第 31、33 章作为边界回查 |
| 每个敌人高频同步位置 | 网络预算和 Owner 负载不适合大量高频对象 | 第 42 章只同步关键敌人状态 |
| 复杂职业和技能树 | 会扩大 PlayerObject 字段和测试矩阵 | 第 45 章上线后迭代再开 |
| 长期经验、排行榜、跨实例活动 | 属于 Vol.5 持久化和运营系统 | 第 45 章只留接口 |
| 第三方对象同步包作为主线依赖 | 第 38 章已经定位为选项,不是默认前提 | 需要时另写评估 ADR |
这些放弃项定义第一版边界。一个最小可玩版本要先证明房间流程、状态恢复和重开成立,再加内容。
给第 41 章的落地清单
Section titled “给第 41 章的落地清单”第 41 章闭环 4 按下面的顺序落地。
- 复用闭环 3 的
PlayerLobbyState+GameState请求式骨架。 GameState增加phase、matchId、waveIndex、waveStartServerTime、teamScore、enemyAliveCount、resultReason。- PlayerObject 增加
currentHP、personalScore、isReady、teamId、roleId。 - 先用一个「训练目标」代替完整 NPC:目标被命中后加分并减少
enemyAliveCount。 - 结算只判断一件事:目标全部清掉或倒计时结束。
- 重开时清
GameState聚合字段,PlayerObject 保留玩家身份和队伍,重置 HP / ready。
第 41 章仍然是最小版本。它不急着接第 42 章的对象池,也不急着接第 43 章的体感优化。闭环 4 的目标是:一局完整跑完,并且迟入、Owner 离开、重开都能解释。
挑一条试。
- 把合作防守改成「一支队伍守目标」和「两支队伍轮流守目标」两种版本。哪些字段仍然归 PlayerObject,哪些字段必须从单个
teamScore变成数组? - 把敌人从「可击杀目标」改成「只制造压力的装饰群」。哪些 EnemyObject 同步字段可以删掉,
GameState还需要保存什么?
| 场景 | 预期 | 实际 |
|---|---|---|
| 1. 按 ADR 做字段检查 | 每个 [UdonSynced] 字段都能在归属表里找到来源 | … |
| 2. 2 人正常一局 | LOBBY → COUNTDOWN → INGAME → RESULT → LOBBY 可走完 | … |
3. 迟入玩家在 INGAME 加入 | 能看到当前阶段、波次、分数、敌人剩余数和玩家 HP | … |
4. GameState Owner 中途离开 | 新 Owner 接管后全局阶段和分数不丢 | … |
| 5. 敌人池容量不足 | 系统拒绝或延迟生成,不运行时创建新对象,不重复加分 | … |
| 6. 本地预演被拒绝 | UI 能撤销,本地表现不污染分数、HP、胜负 | … |
- 能解释为什么合作防守先写 ADR,再写闭环 4 代码
- 能把一个字段归到 PlayerObject、GameState、EnemyObject 或 Local
- 能说明 PlayerObject 为什么不能直接裁决团队分
- 能说出对象池对迟入只恢复哪一层
- 能指出第 41 章最小可玩版本主动不做的能力
- VRChat Creator Docs · Networking and Synchronization:Owner、同步变量、Network Event、迟入恢复的官方说明。
- VRChat Creator Docs · Network Specs and Tips:200 bytes、约 280 KB、
RequestSerialization与网络预算的官方说明。 - VRChat Creator Docs · Network Components:
VRCObjectPool、VRCObjectSync和网络回调的官方说明。 - 第 12 章 · 请求式架构:玩家对象请求,状态管理器裁决:PlayerObject + GameState 的固定模板。
- 第 26 章 · 轻实时范式:合作防守项目的节奏和字段来源。
- 第 36 章 · 对象池与生成系统:对象池生命周期和池耗尽处理。
- 理解 G · 复杂系统先证明能降级:第八部 ADR 的降级原则。