交互同步:一次 RPC 不能同时承担命令、状态与事件

自动关联目录:交互同步:一次 RPC 不能同时承担命令、状态与事件

核心判断:Command 是请求,Receipt 是请求结果,State 是可恢复事实,Event 是只消费一次的瞬时信号。它们对可靠性、合并、晚加入和重放的语义不同,不能因为 UE 都能 RPC 或 Replicate 就走同一条通道。

一、业务冲突:本地反馈要快,迟到玩家看到的世界又必须正确

玩家按下交互后希望立刻看到按钮、动画或音效;DS 可能因距离、占用或资源变化拒绝。其他玩家只关心权威结果,晚加入者需要当前门、机关或占用状态,却不应补播几秒前的音效和点击动画。

若把结果放进一个 replicated struct,中间多次变化可能在一次网络更新内合并;若把事件放进持续复制属性,晚加入者又会把历史当新事件。可靠 RPC 保证到达和顺序,也不提供业务幂等与重连恢复。

二、实际实现:已经区分受众,但仍混用了事件与属性

现有交互路径使用可靠 Client-to-Server RPC 发送命令,并用时间戳过滤过旧请求。执行结果保存为一个结构体:非 Owner 通过 COND_SkipOwner 属性复制观察,Owner 通过 Client RPC 立即收到;角色正在交互的状态则独立复制。这个设计已经意识到发起者与观察者需要不同路径。

另一个通用事件组件用递增 EventIndex、Owner/Broadcast 两个环形数组、Push Model 和客户端排序去重来减少丢事件;Client-to-Server 还区分 Reliable/Unreliable。它解决了“属性只保留末值”的一部分问题,但每次仍复制整个动态数组,覆盖时依据服务器帧龄而非接收确认,字符串键值参数也只有数量限制而缺少类型与字节预算。

flowchart LR
    C[本地 Command + ClientSeq] --> D[DS 验证/执行]
    C -.立即.-> P[预测表现]
    D --> R[Owner Receipt]
    D --> S[可恢复 State Revision]
    D --> E[瞬时 Presentation Event]
    R --> P
    S --> J[当前玩家/晚加入/重连]
    E --> O[当前相关观察者]

三、值得保留:受众条件、序号与 Push Model 方向正确

Owner 走直达回执、观察者走复制,减少了发起者等待下一次属性更新的延迟;权威持续状态独立保存,也为恢复留下基础。事件递增序号、客户端排序去重、OwnerOnly 条件和单机复用 OnRep 路径都值得保留。

可靠与不可靠不是按“重要/不重要”拍脑袋选择,而应看是否可被后续状态覆盖。高频瞄准或拖拽采样可丢,最终提交和一次性资源变更不可丢,这个判断应继续保留。

四、结构性缺陷:没有业务序号,就无法幂等、纠错和解释

命令缺少统一 CommandId、TargetGeneration、ExpectedRevision 与 LeaseId,客户端重试时 DS 无法证明是重复还是新动作,Receipt 也无法精确对应本地预测。仅用发送时间过滤旧包,不能处理未来时间、重放和目标换代。

单一 Result 属性把事件当状态:连续结果可能被合并,晚加入者可能观察到没有意义的“最后一次结果”。环形数组虽保留多个事件,但服务器帧数不能证明任一客户端已经收到;扩缩容和整体 TArray 复制会带来带宽尖峰,晚加入还可能消费保留窗口内的旧事件。通用字符串 Map 让 Schema、长度、枚举范围和权限只能在脚本里补,容易 fail-open。

五、目标设计:四条语义通道,共用同一个关联键

Command 使用类型化 payload,调用者身份来自 Connection,携带 ClientSeq、目标代际和预期 Revision;DS 保存短期幂等窗口。Receipt 只发 Owner,明确 Accepted/Rejected、原因和权威 Revision。State 使用 RepNotify 或 Fast Array 表达可恢复事实。Event 只承载表现触发,带 EventSeq、Audience、TTL 和关联 Revision,晚加入默认不回放。

离散点击不做插值;拖拽、引导和持续交互则拆成三层:Owner 用 CommandId 本地预测,DS 按速率、范围和 Lease 校验连续采样,观察者根据 ServerTime 在小缓冲区内插值。短时丢包允许有界外推,超限后冻结到最后权威状态;松手与最终结果走可靠提交。连接 Relevancy 决定谁收到状态,重连只恢复最新 Revision。

void OnReceipt(Receipt r)
{
    PendingPrediction* p = predictions.Find(r.commandId);
    if (!p) return;                    // 已取消或已处理
    if (r.accepted) p->Confirm(r.stateRevision);
    else p->Rollback(r.reason, r.authoritativeState);
    predictions.Remove(r.commandId);
}

void OnState(StateSnapshot s)
{
    if (s.revision <= appliedRevision) return;
    ApplyAuthoritativeState(s);
    ReconcilePredictionsAfter(s.revision);
}

玩法关键事件必须落为 State 变更;Gameplay Cue、声音和镜头等才走可丢或限时 Event。重连只恢复 State 与有效 Lease,不恢复旧 Prediction,也不补播过期 Event。

六、决策规则:先问语义,再选 UE 网络机制

数据丢失后果晚加入UE 承载建议
Command可能未执行不重放Owned Actor Server RPC + 幂等键
Receipt本地预测悬挂不恢复旧回执Owner Client RPC,超时由 State 收敛
State世界事实错误必须得到最新值Replicated Property/Fast Array + Revision
Event少一次表现默认不补播Unreliable/可靠短流或 Gameplay Cue
连续采样后续样本可覆盖不需要历史Unreliable,序列号与最终可靠提交
持久结果丢失不可接受必须恢复DS 状态提交后复制 Snapshot

这次复盘最重要的不是选择 Reliable 还是 Unreliable,而是先承认四种数据有不同时间语义。通道拆开后,预测、纠偏、晚加入和断线重连才各有明确落点。