第 43 章 · 加入体感优化
这一章解决:玩家按下攻击后,如何立即得到反馈,同时让伤害、分数、敌人死亡仍以 Owner 确认为准。
先看一下
第 42 章已经能打敌人,但流程偏硬:交互发请求,等待 GameState 和 EnemyObject 裁决,再看到 HP、死亡和分数变化。网络延迟只有 100 ms 时,按钮也会显得慢。第 43 章加的是体感层,不改共识层。
这一章会拿到什么
- 一张「本地先播 / 等确认 / 可降级」判断表
- 命中请求的 pending 记录:
pendingHitRequestId、目标、预估伤害 - 确认回包:接受、拒绝、修正三种结果
- 远端命中特效与本地预演的分层
- 网络拥塞时的反馈降级规则
依赖前面
- 第 29 章的本地预演边界
- 第 30 章的 Owner 确认回包
- 第 32 章的修正、插值与降级
- 第 42 章的 EnemyObject 命中裁决
- 闭环 4 的
matchId
体感优化只碰表现层
Section titled “体感优化只碰表现层”第 43 章不改第 42 章的结果归属。伤害仍由 EnemyObject Owner 写,分数仍由 GameState Owner 写,结算仍由 GameState 写。
| 内容 | 何时发生 | 写权 |
|---|---|---|
| 按钮按下态、音效、准星反馈 | 本地立即发生 | 本地客户端 |
| 命中请求 | 本地发出,带 requestId | 玩家自己的 PlayerObject |
| 敌人 HP 变化 | Owner 接受请求后发生 | EnemyObject Owner |
| 分数和剩余敌人数 | 敌人死亡后发生 | GameState Owner |
| 击中特效广播 | Owner 确认后发生 | Network Event |
这张表的边界和第 29 章一致:本地预演可以提前改变表现,不能提前改变共识状态。合作防守世界里,玩家会感受到「按下去立刻有声音和动画」,但迟入玩家只看同步字段里的结果。
Pending 命中记录
Section titled “Pending 命中记录”玩家每次攻击时,本地先记录一个 pending。这个 pending 不同步,只服务本地 UI。
private int pendingHitRequestId = -1;private int pendingEnemyId = -1;private int pendingDamage;private double pendingStartTime;
public void PressAttack(int enemyId){ if (pendingHitRequestId >= 0) return;
int requestId = NextLocalRequestId(); pendingHitRequestId = requestId; pendingEnemyId = enemyId; pendingDamage = localDamagePreview; pendingStartTime = Networking.GetServerTimeInSeconds();
ApplyLocalAttackPreview(enemyId); RequestHitEnemy(enemyId, localDamagePreview);}ApplyLocalAttackPreview 可以播放音效、按钮冷却、准星扩散、近端枪口火光。它不改 EnemyObject.hp,也不改 GameState.totalScore。
private void ApplyLocalAttackPreview(int enemyId){ attackButton.interactable = false; localAudio.PlayOneShot(attackPreviewClip); muzzleFlash.Play(); ShowPendingMarker(enemyId);}pending 的存在还能防连点。未确认前再次攻击同一个目标,可以直接挡掉,也可以排队一条。第一版采用挡掉,避免多个 pending 同时修正。
确认回包:接受、拒绝、修正
Section titled “确认回包:接受、拒绝、修正”第 42 章 GameState 裁决命中后,给发起玩家一个确认回包。回包里至少带请求号和结果。
| 结果 | 含义 | 本地处理 |
|---|---|---|
| accepted | 命中成立,伤害已交给 EnemyObject | 清 pending,进入真实冷却 |
| rejected | 阶段不对、目标无效、敌人已死、请求过旧 | 撤销按钮 pending,显示短提示 |
| corrected | 命中成立,但伤害或目标被修正 | 清 pending,显示修正后的伤害 |
片段如下:
[NetworkCallable]public void OnHitConfirmed(int requestId, byte result, int finalDamage){ if (!Networking.IsOwner(gameObject)) return; if (requestId != pendingHitRequestId) return;
pendingHitRequestId = -1; pendingEnemyId = -1;
if (result == HIT_ACCEPTED) { StartConfirmedCooldown(); return; }
if (result == HIT_CORRECTED) { ShowCorrectedDamage(finalDamage); StartConfirmedCooldown(); return; }
CancelLocalAttackPreview();}确认回包不是长期状态。迟入玩家不需要知道某个按钮是否曾经 pending,只需要知道敌人当前 HP、死亡状态、分数和阶段。
第 30 章讲过,单发给某个玩家通常是 All + playerId 本地过滤。第 43 章只把它用于低频攻击确认。不要把每一帧准星、连发输入或隐藏信息塞进确认回包;高频内容要么留本地,要么回到同步字段和降级策略里。
命中特效分两层
Section titled “命中特效分两层”攻击反馈分本地层和远端层。
| 层 | 谁看到 | 何时播放 | 可否丢失 |
|---|---|---|---|
| 本地预演 | 发起玩家 | 按下按钮立刻播放 | 可以丢,不进恢复 |
| 确认特效 | 所有当前玩家 | Owner 接受后广播 | 可以丢,结果已进同步字段 |
| 结果状态 | 所有人含迟入 | 同步字段到达后应用 | 不能丢 |
本地预演解决手感。确认特效解决「其他玩家也看到这次命中」。结果状态解决迟入和结算。
GameState 或 EnemyObject 确认命中后,可以广播一条参数化事件:
[NetworkCallable]public void NetPlayHitFx(int enemyId, int attackerPlayerId, int damage){ PlayHitFx(enemyId, attackerPlayerId, damage);}这条事件不负责加分,不负责扣血。它只是表现层。网络拥塞时可以少发或采样发送。
网络拥塞时先降级表现
Section titled “网络拥塞时先降级表现”Networking.IsClogged 或 OnPostSerialization 失败增多时,按语义重要性降级。
| 优先级 | 保留 | 降级 |
|---|---|---|
| 1 | GameState 阶段、分数、波次 | 不降级,只减少发送频率 |
| 2 | EnemyObject HP、死亡状态 | 合并更新,减少非关键字段 |
| 3 | PlayerObject HP、个人分 | 保留,必要时降低 UI 刷新频率 |
| 4 | 确认特效广播 | 采样发送,只播关键击杀 |
| 5 | 本地预演 | 本地仍可播,不占网络 |
网络差时,先停远端飘字、远端音效和非关键命中特效。GameState 的阶段、分数、波次不能停。否则迟入和结算会坏。
private bool ShouldBroadcastHitFx(bool isKill){ if (isKill) return true; if (Networking.IsClogged) return false; return true;}这段的意思很简单:击杀特效保留,普通命中在拥塞时只本地播。
失败提示要短
Section titled “失败提示要短”Owner 拒绝请求时,反馈要短,不要弹出一长段解释。常见原因只有几类。
| 原因 | UI 文案方向 |
|---|---|
| 目标已经死亡 | 「目标已清除」 |
| 不在 INGAME | 「比赛未开始」 |
| 请求过旧 | 不提示,直接恢复按钮 |
| 距离或冷却不合法 | 「距离不够」或「冷却中」 |
| 网络拥塞 | 「同步中」 |
这些提示是本地 UI,不进入同步字段。它们帮助发起者理解按钮为什么恢复了,不能成为规则判断依据。
挑一条试。
- 把普通命中特效改成「只本地播」,击杀特效仍广播给所有人。多人测试时,远端玩家是否还能读懂战斗发生了什么?
- 把 pending 策略从「未确认前挡掉下一次攻击」改成「允许排队 2 次」。需要多保存哪些字段?拒绝第二条请求时 UI 怎么恢复?
| 场景 | 预期 | 实际 |
|---|---|---|
| 1. 单客户端按攻击 | 按钮立即反馈,本地音效和枪口火光播放,共识字段未提前变化 | … |
| 2. Owner 接受命中 | pending 清空,EnemyObject HP 更新,必要时播放确认特效 | … |
| 3. Owner 拒绝命中 | pending 清空,按钮恢复,HP / 分数 / 阶段无跳变 | … |
| 4. 击杀敌人 | 本地预演先播,确认后所有当前玩家看到击杀表现,迟入只看死亡状态 | … |
| 5. 网络拥塞 | 普通远端命中特效减少,GameState 和 EnemyObject 同步仍保留 | … |
| 6. 连续快速点击 | pending 未清空前不会制造多条重复伤害 | … |
| 7. 迟入玩家加入 | 不补播旧预演和旧特效,只看到当前敌人状态和分数 | … |
- 能把攻击拆成本地预演、待确认请求、共识结果三层
- 能解释确认回包为什么不能代替同步字段
- 能说明网络拥塞时先降级哪些表现
- 能指出哪些 UI 提示只属于本地层
- 能把本章和第 42 章的命中裁决边界区分开
- 第 29 章 · 即时反馈与本地预演:本章的本地预演来源。
- 第 30 章 · Owner 伪权威与确认回包:确认回包的请求序号边界。
- 第 32 章 · 修正、插值与优雅降级:失败修正和降级顺序。
- 第 42 章 · 加入 NPC 与对象池:命中裁决和 EnemyObject 状态来源。
- VRChat Creator Docs · Networking and Synchronization:同步变量、Network Event 和迟入恢复的官方说明。