跳转到内容

第 40 章 · 从玩法反推架构决策记录

约 11 分钟 难度:5 动手章

这一章解决:完整项目开始前,先把「谁写状态、状态走哪条通道、迟入怎么恢复、出问题怎么降级」写成一页可执行的 ADR。

先看一下

合作防守世界很容易从「先刷一只怪」开始写。这样进度看起来快,但后面会连续遇到同一类问题:玩家 HP 放哪里、敌人死亡谁裁决、波次时间谁推进、迟入玩家看到第几波、Owner 离开后哪一份状态还算数。第八部先写 ADR,再写代码。

这一章会拿到什么

  • 一份合作防守世界的核心循环说明
  • 一张状态归属表:PlayerObject、GameState、EnemyObject、Local 各负责什么
  • 一张同步通道表:同步字段、Network Event、ObjectPool、ObjectSync、本地表现各放什么
  • 一组最小 ADR 条目,可直接带到第 41 章闭环 4
  • 一份项目级测试矩阵

依赖前面

  • 第 12 章的请求式架构:玩家对象请求,GameState 裁决
  • 第 20 章的 requestIdstateVersion
  • 第 26 章的轻实时范式:事件做反馈,状态做共识
  • 第 36 章的对象池生命周期
  • 理解章 G 的降级优先级

第八部主项目是一局 2–4 人合作防守。玩家在大厅准备,开局后敌人按波次出现,玩家守住目标并获得分数,时间到或目标被破坏后进入结算,再回到大厅重开。

这条循环先写成状态机,不写成脚本。

阶段玩家看到什么系统必须保存什么
LOBBY玩家加入、准备、选队伍或角色每位玩家的 ready / team / role,当前房间是否能开局
COUNTDOWN倒计时、队伍锁定、出生点确认倒计时开始时间、目标阶段、锁定后的队伍快照
INGAME波次推进、敌人出现、玩家受击和得分当前波次、波次开始时间、剩余敌人、团队分数、每位玩家 HP
RESULT胜负、分数、MVP、重开按钮结算原因、最终分数、每位玩家贡献快照
RESTARTING清场、归还对象池、准备回大厅清理进度、下一局 matchId

第 41 章最小可玩版本只做这一条循环里最窄的一条路:LOBBY 进入 INGAME,刷一波目标,分数增加,进入 RESULT,再重开。第 42 章之后再把敌人和对象池补完整。


ADR 是 Architecture Decision Record(架构决策记录)的简称。这里不用复杂模板,只保留四个问题:决定了什么,为什么这么做,不这样会怎样,后面怎么验证。

本卷的项目 ADR 采用这个格式:

字段写什么
决策一句话写清楚状态或对象归属
依据来自哪章的规则,或来自官方约束
代价放弃了什么能力,或引入什么测试成本
验证用哪条测试矩阵证明它成立

它的范围很窄:只记录以后改错会返工的东西,主要是 Owner、同步通道、迟入恢复、降级入口。敌人数量、出生点坐标、UI 文案进入配置表或关卡说明,不进入 ADR。


合作防守世界的状态先分四块。

状态归属通道迟入恢复
ready / teamId / roleId每位玩家的 PlayerObject[UdonSynced] 字段读每个玩家对象当前值
currentHP / isDowned每位玩家的 PlayerObject[UdonSynced] 字段读当前 HP 与倒地状态
requestId / requestType / requestPayload每位玩家的 PlayerObject[UdonSynced] 字段GameStatelastProcessedRequestId 去重
phase / matchId / stateVersionGameStateManual sync读当前阶段与版本
waveIndex / waveStartServerTimeGameStateManual sync从波次和时间恢复 UI
teamScore / enemyAliveCountGameStateManual sync读当前聚合值
resultReason / finalScoreGameStateManual sync结算屏直接恢复
单个敌人的 HP / 状态 / 目标EnemyObject对象自己的同步字段读 active 对象字段
敌人是否存在VRCObjectPoolactive / inactive 同步迟入自动拿到 active 状态
子弹轨迹、枪口火光、按钮按下态Local本地变量 / 本地播放不恢复
命中音效、击杀飘字Network Event参数化事件不重放,必要结果写回状态

这张表有两个重点。

第一,GameState 只存全局聚合:阶段、波次、分数、剩余敌人数。它不保存每位玩家的 HP,也不保存每只敌人的全部状态。玩家状态归 PlayerObject,敌人状态归 EnemyObject。

第二,本地表现明确不进入迟入恢复。迟入玩家不需要补看 3 秒前的枪口火光,但必须能看到当前第几波、剩几个敌人、自己是否倒地。


同一局游戏里会同时用五条通道。第八部不再重新解释每条通道,只把它们落到项目对象上。

通道项目里放什么不放什么
LocalUI 预演、音效、准星、按钮按下态、临时特效分数、阶段、胜负、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。第 41 章按这份记录搭闭环 4,第 42 到 45 章只做增量修改。

内容
决策phasematchIdwaveIndexteamScoreenemyAliveCountresultReasonGameState
依据第 11 章:GameState Owner 是当前实例内的状态提交者;第 26 章:轻实时的波次和聚合状态归全局
代价GameState Owner 离开时必须能重选 Owner,并继续使用同步字段恢复
验证Owner 离开测试:phasewaveIndexteamScore 不丢,重选后能继续推进

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 没有提前跳变
内容
决策敌人池、投射物池、掉落物池分别建池;池耗尽时拒绝或延迟,不运行时创建新对象
依据第 36 章:对象池把最大同时存在数量变成预算;理解章 G:复杂系统先证明能降级
代价极端情况下会少刷敌人或少播特效,玩法要接受这个降级
验证把敌人池容量调低:第 N 个敌人生成失败时不崩、不无限重试,UI 或日志给出原因

ADR 还要记录项目第一版主动放弃的能力。这样第 41 章不会在最小可玩版本里偷偷膨胀。

第一版不做原因后续位置
精确 FPS 命中公平VRChat 没有自定义权威服务器,公平射击不是本项目承诺第 31、33 章作为边界回查
每个敌人高频同步位置网络预算和 Owner 负载不适合大量高频对象第 42 章只同步关键敌人状态
复杂职业和技能树会扩大 PlayerObject 字段和测试矩阵第 45 章上线后迭代再开
长期经验、排行榜、跨实例活动属于 Vol.5 持久化和运营系统第 45 章只留接口
第三方对象同步包作为主线依赖第 38 章已经定位为选项,不是默认前提需要时另写评估 ADR

这些放弃项定义第一版边界。一个最小可玩版本要先证明房间流程、状态恢复和重开成立,再加内容。


第 41 章闭环 4 按下面的顺序落地。

  1. 复用闭环 3 的 PlayerLobbyState + GameState 请求式骨架。
  2. GameState 增加 phasematchIdwaveIndexwaveStartServerTimeteamScoreenemyAliveCountresultReason
  3. PlayerObject 增加 currentHPpersonalScoreisReadyteamIdroleId
  4. 先用一个「训练目标」代替完整 NPC:目标被命中后加分并减少 enemyAliveCount
  5. 结算只判断一件事:目标全部清掉或倒计时结束。
  6. 重开时清 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 章最小可玩版本主动不做的能力