第 30 章 · Owner 伪权威与确认回包
这一章解决:本地预演发出请求后,如何知道 Owner 是否处理了它,并把「临时反馈」切换成「已确认结果」。
先看一下
第 29 章把按钮反馈拆成三层:本地表现、待确认请求、共识结果。现在缺中间那条回路。玩家 A 按下技能按钮,本地已经播了音效;GameState Owner 过了一小段时间才处理请求。A 需要知道这次请求是成功、失败,还是还在路上。
这一章会拿到什么
requestId+lastProcessedRequestId作为确认回包的最小模板NetSkillReceipt参数化事件回执片段- 「同步字段确认」和「事件回执确认」的取舍
- 一条硬边界:Owner 伪权威只做可玩性裁决,不做安全承诺
依赖前面
- 第 12 章的请求式架构模板
- 第 20 章的请求幂等工具
- 第 21 章的参数化 Network Event
- 第 29 章的本地预演 pending 状态
伪权威是什么意思
Section titled “伪权威是什么意思”GameState Owner 在本卷里承担实例内裁决:检查阶段、冷却、分数、目标是否存在,然后写同步字段。它接近传统 netcode 里的服务器角色,但仍然运行在某位玩家的客户端上,不能作为可信安全边界。
所以本章使用「伪权威」这个词:它能让一局游戏有单一裁决点,让状态更一致,让本地预演有确认回包;反作弊、客户端输入可信、高频物理的严格权威模拟,都超出它的能力边界。
这个边界决定了确认回包的目标:让交互可观察、可恢复、可调试。 它不承诺把 VRChat 世界变成服务器权威游戏。
两种确认方式
Section titled “两种确认方式”确认可以走同步字段,也可以走参数化事件。两者不是互斥关系。
| 方式 | 适合 | 不适合 |
|---|---|---|
| 同步字段确认 | 迟入要看到、需要 Debug 面板回查、请求少但重要 | 高频、只给发起者看的短反馈 |
| 参数化事件回执 | 只通知发起者「这次请求成了 / 没成」、需要低延迟反馈 | 迟入恢复、长期状态、战报历史 |
第 12 / 20 章的 lastProcessedRequestId[] 是同步字段确认:它让 Owner 记住每位玩家处理到哪一条。第 30 章再加一条事件回执:Owner 处理完后,单独告诉发起者本次请求是否接受。
字段确认:处理到哪一条
Section titled “字段确认:处理到哪一条”GameState 上已经有一组字段:
[UdonSynced] private int[] lastProcessedRequestId = new int[100];这里用 playerId 直接当数组下标,是为了让片段短。完整项目要把它包成一个 GetPlayerSlot(playerId) 或 IsValidPlayerSlot(playerId) helper:先检查范围,再读写数组。玩家离开时也要清理或冻结对应槽位,避免旧玩家的请求序号影响后续玩家。
处理请求时,Owner 先看玩家自己的 requestId,再更新这一槽。
private void ProcessSkillRequest(PlayerLobbyState pls, VRCPlayerApi who){ int playerId = who.playerId; int slot = GetPlayerSlot(playerId); if (slot < 0) return;
int reqId = pls.RequestId; if (reqId <= lastProcessedRequestId[slot]) return;
bool accepted = TryApplySkill(pls.SkillId, pls.TargetId, who); lastProcessedRequestId[slot] = reqId;
RequestSerialization(); SendSkillReceipt(playerId, reqId, accepted);}lastProcessedRequestId[slot] = reqId 的语义是:Owner 已经看过这条请求,并决定接受或拒绝。它不是「一定成功」。成功失败要用额外字段或回执表达。
事件回执:这一条成没成
Section titled “事件回执:这一条成没成”回执只给发起者本人看,适合走参数化事件。由于官方没有 NetworkEventTarget.Player,常见做法是向 All 广播回执,所有客户端本地过滤目标 playerId。
这不是私聊通道。所有当前客户端都会收到这条事件,只是本地过滤后不处理。它适合低频、无隐私的确认回包;不适合高频输入流,也不适合携带只应给某个玩家看的隐藏信息。
private void SendSkillReceipt(int playerId, int requestId, bool accepted){ SendCustomNetworkEvent( NetworkEventTarget.All, nameof(NetSkillReceipt), playerId, requestId, accepted);}
[NetworkCallable]public void NetSkillReceipt(int playerId, int requestId, bool accepted){ if (Networking.LocalPlayer == null) return; if (Networking.LocalPlayer.playerId != playerId) return;
playerLobbyState.OnSkillConfirmed(requestId, accepted);}这段需要 using VRC.SDK3.UdonNetworkCalling; 和 using VRC.Udon.Common.Interfaces;。它的作用是把第 29 章的 pending 本地预演收尾;长期状态仍然交给同步字段。
如果回执丢了怎么办?同步字段仍然会更新。发起者可以在 OnDeserialization 或本地轮询里用自己的槽位检查 lastProcessedRequestId[slot] >= pendingRequestId,用字段确认兜底清理 pending。事件回执负责快,字段确认负责稳。
Owner 处理结果要能解释
Section titled “Owner 处理结果要能解释”accepted 一个布尔值通常不够。UI 如果要显示原因,可以把拒绝原因压成 byte reason。
| reason | 含义 | UI 表现 |
|---|---|---|
| 0 | 成功 | 进入真实冷却 |
| 1 | 阶段不允许 | 显示「当前阶段不能使用」 |
| 2 | 冷却中 | 恢复按钮,播放轻提示 |
| 3 | 目标无效 | 取消准星锁定 |
| 4 | 资源不足 | 显示资源不足提示 |
回执参数可以变成 playerId, requestId, accepted, reason。这仍然是小参数事件,不需要 JSON。
注意:拒绝原因是 UI 反馈,不是安全日志。真正需要回查的异常(例如 1 秒内 50 个请求、分数突变、速度异常)应写进第 33 章的异常日志字段或调试面板。
和传统 server confirmation 的差异
Section titled “和传统 server confirmation 的差异”Gambetta 原模型里,服务器返回的是权威状态 + 已处理输入序号。客户端收到后,以服务器状态为基准,重放尚未确认输入。
VRChat 版本只保留其中一部分:
| 原模型 | VRChat 近似 |
|---|---|
| 权威服务器处理输入 | GameState Owner 处理请求 |
| 服务器返回权威状态 | 同步字段复制当前共识状态 |
last processed request | lastProcessedRequestId[slot] |
| 客户端重放未确认输入 | 只清理 / 撤销本地预演,不做完整重放 |
| 服务器防作弊 | 只能做合理性检查,不能成为可信安全边界 |
这张表是第六部的核心翻译动作。能借的是序号、确认、pending 清理;不能借的是服务器权威和完整重放。
挑一条试。
- 把回执事件删掉,只保留同步字段确认。按钮 pending 会慢多少?哪类 UI 会受影响最大?
- 把
accepted拆成reason。设计 3 个拒绝原因,并决定哪些原因要播放音效,哪些只显示短文字。
| 场景 | 预期 | 实际 |
|---|---|---|
| 1. 合法请求 | Owner 推进 lastProcessedRequestId,回执 accepted=true,本地进入真实冷却 | … |
| 2. 冷却中重复请求 | Owner 推进处理序号但回执拒绝,本地撤销预演 | … |
| 3. 人为丢失回执事件 | 同步字段确认仍能兜底清 pending | … |
| 4. Owner 转移后发请求 | 新 Owner 继续从同步字段里的 lastProcessedRequestId 判断,不重复处理旧请求 | … |
| 5. 迟入玩家加入 | 不补收历史回执,只看到当前共识字段 | … |
- 能解释
GameState Owner为什么是伪权威,不是可信服务器 - 能区分「处理过」和「接受」
- 能说明拒绝请求为什么也要推进
lastProcessedRequestId - 能写出事件回执 + 同步字段兜底的双层确认
- 能指出 VRChat 近似版确认和传统 server confirmation 的差异
- Gabriel Gambetta · Client-Side Prediction and Server Reconciliation(访问日期:2026-06-17)—
last processed request与 reconciliation 原模型。 - VRChat Creator Docs · Network Events —
[NetworkCallable]、参数化事件与速率限制。 - 第 12 章 · 请求式架构 —
PlayerObject发请求、GameState裁决的母本。 - 第 20 章 · 版本号、序号与幂等 —
requestId与lastProcessedRequestId的边界。 - 第 29 章 · 即时反馈与本地预演 — pending 本地预演的入口。