交互占用:PlayerId 只能说明是谁,不能证明哪一次

自动关联目录:交互占用:PlayerId 只能说明是谁,不能证明哪一次

核心判断:互斥交互需要的是一份有代际、有期限、可撤销的服务器 Lease。单独复制“当前玩家是谁”只能表达占用结果,无法拒绝同一玩家上一轮交互的迟到释放。

一、业务冲突:抢占必须原子,释放却来自很多不可靠生命周期

单人机关、拖拽物、载具座位和附着型交互都要防止多人同时进入;多人进度物又需要保存参与者集合。结束原因不只包括主动松手,还包括角色死亡、断线、切换世界、对象销毁、能力取消、动画中断和超时。

若对象只保存 CurrentPlayerId,同一玩家快速完成 A、重新开始 B 后,A 的迟到 Release 仍会命中相同 PlayerId,把 B 错误释放。若只依赖 Actor 指针,销毁和重建还会让“同一个逻辑玩家/对象”失去连续身份。

二、实际实现:占用事实分散在对象、角色和参与者数组

工程里已经存在三种表达:对象复制一个正在交互的玩家标识;独占组件在进入、进行、退出状态间绑定角色,并监听角色销毁做清理;多人交互组件复制参与角色数组,移除时保留空槽;角色侧又复制交互组件引用、状态、开始时间和附加参数,以驱动附着、动画和 UI。

这些实现解决了当时的直接需求:DS 串行执行进入操作,忙碌对象拒绝后来者;单机模式手动触发 OnRep 等价路径;Push Model 只在变更时复制;角色销毁会释放独占对象。问题在于它们没有共同回答“这是哪一次占用”。

stateDiagram-v2
    [*] --> Free
    Free --> Reserved: DS grant / epoch++ / deadline
    Reserved --> Active: execution starts
    Active --> Releasing: matching lease releases
    Reserved --> Revoked: timeout / owner lost / target invalid
    Active --> Revoked: disconnect / world leave / cancel
    Releasing --> Free: cleanup committed
    Revoked --> Free: compensating cleanup committed

三、值得保留:权威状态与呈现状态已有分离雏形

对象忙闲由 DS 修改,客户端通过复制状态驱动表现,而不是让本地按钮直接改权威字段,这是正确边界。状态携带服务器开始时间,允许晚到客户端按虚拟进度恢复进入/退出动画;角色销毁监听和 EndPlay 解绑也体现了生命周期意识。

多人数组使用弱引用避免长期强持有角色,单机与联网复用同一 OnRep 展示路径,也降低了两套逻辑分叉。这些机制应保留,但应由统一 Lease 记录提供事实源。

四、结构性缺陷:同一事实被多份字段隐式拼接

玩家标识没有 epoch、deadline 和 target generation,因此无法区分重放、迟到释放与新一轮交互。公共接口也没有统一的断线、换 World 和服务器回收契约;某些对象有销毁清理,不代表所有派生实现都完整。

对象状态、角色状态、绑定指针和参与者数组分别复制,接收顺序不保证原子。客户端可能短暂看到角色已进入、对象仍空闲,或对象被占用但绑定角色尚未解析。多人数组保留空洞并整体复制,既增加状态体积,也没有成员 Revision 来表达加入/退出的因果顺序。

五、目标设计:Lease 是唯一权威,占用视图只是投影

DS 为每次成功 Reserve 生成 LeaseId = (TargetId, TargetGeneration, Epoch),记录 Holder、Action、Phase、GrantedAt、Deadline 和 CancelReason。后续 Activate、Heartbeat、Release、Cancel 都必须携带匹配 LeaseId;对象与角色复制的只是同一 Lease Revision 的不同只读投影。

ReleaseResult Release(Connection sender, LeaseId id)
{
    Lease& lease = leases.Find(id.target);
    Player holder = Sessions.ResolvePlayer(sender);
    if (lease.id != id) return Reject(StaleLease);
    if (lease.holder != holder.stableId) return Reject(NotHolder);

    lease.phase = Releasing;
    CancelExecution(lease);
    DetachPresentationBindings(lease);
    PersistFinalStateIfNeeded(lease);
    return leases.CommitFree(id.target, id.epoch + 1);
}

非持续点击可以用极短 Lease,在同一 DS 事务中 Reserve、Execute、Release;长按、拖拽、载具和合作进度才需要 Active/Heartbeat。不要为所有动作强加心跳。

六、决策规则:是否需要 Lease,取决于动作跨不跨帧

动作形态权威表示必须处理的结束条件
瞬时且幂等命令去重键,可不保留长 Lease重复请求、目标换代
瞬时但有唯一资源事务内短 Lease并发争抢、提交失败
持续独占单 Holder Lease + epoch松手、超时、断线、销毁、换世界
多人协作成员集合 + 每成员 lease token加入/退出乱序、部分断线
可抢占Lease + priority + preemption reason补偿、旧 Holder 迟到命令
可恢复持久状态与运行时 Lease 分离重连只恢复状态,不复活旧 Lease

复盘后的关键提升是:不要再问“这个对象当前绑了谁”,而要问“哪一份仍有效的授权允许谁在什么期限内做什么”。前者适合 UI,后者才足以支撑 DS 权威与生命周期恢复。