|
楼主 |
发表于 2022-10-7 14:09:02
|
显示全部楼层
快节奏有冲突的同步策略解决快节奏有冲突的同步策略,核心就是 3 个关键词:预测、和解、插值 。理解了这 3 个概念,任何情况的同步对你来说应该都是游刃有余。不过在此之前,让我们先看看逻辑与表现分离的架构。
逻辑与表现分离多人实时游戏,通常会划分为表现层和逻辑层。表现层指游戏画面的显示和用户输入的获取;逻辑层指渲染无关的、只关注状态变化和计算的玩法逻辑。
逻辑层和表现层最终是面向数据的,例如玩家的位置、生命值等,我们把这些数据统称为 状态。我们把所有能影响状态变化的因素称为 输入,例如玩家操作(如移动)、系统事件(如天上打雷了)、时间流逝等等。
逻辑层就是定义所有状态和输入,然后实现状态变更的算法:
新状态 = 老状态 + 输入1 + ... + 输入N
重要
逻辑层本质上就是一个状态机。
在实现逻辑层的过程中,有几个重要的点需要关注:
- 无输入,不变化:状态变更仅发生在输入时刻,没有输入时状态不会改变
- 无外部依赖:状态计算应该没有任何外部依赖,例如 Date.now()、Math.random() 等,所有这些都应该显式成为输入的一部分
- 结果的一致性:在相同的状态和输入下,得到的新状态应该是一致的
tip
像随机数这类场景,可以通过伪随机数生成器实现,在相同的种子输入下,随机结果应该是一致的。
在逻辑层的状态计算基础之上,预测、和解、插值就更容易理解了。
预测预测就是将玩家的输入立即应用到本地状态,而无需等待服务端返回。
如果玩家的每一次操作如果都要等到服务端确认后才能生效,那么延迟将是不可避免的。解决方案就是:玩家做出任何操作后,立刻将输入应用到本地状态,并刷新表现层显示。例如按下了 “右”,那么就立即向右移动,而无需等待服务端返回,效果如图。
现在,操作的延迟消失了。你按下 “左” 或者 “右” 都可以得到立刻的反馈。
但问题似乎并没有完全解决,在移动过程中,你总是能感到来回的 “拉扯” 或者位置抖动。这是因为你在执行本地预测的时候,也在接收来自服务端的同步,而服务端发来的状态总是滞后的。
例如:
- 你的坐标是 (0,0)
- 你发出了 2 个 右移 指令(每次向右移动 1 个单位),服务器尚未返回,执行本地预测后,坐标变为 (2,0)
- 你又发出了 2 个 右移 指令,服务器尚未返回,执行本地预测后,坐标变为 (4,0)
- 服务端发回了你的前 2 个右移指令:从 (0,0) 执行 2 次右移,坐标变为 (2,0),被拉回之前的位置
由于延迟的存在,服务端的同步总是滞后的,所以你总是被拉回之前的位置。如此往复,就是你在图中看到的抖动和拉扯。
归根到底,是服务端同步过来的状态与本地预测的状态不一致,所以我们需要 “和解” 它们。
和解和解就是一个公式:预测状态 = 权威状态 + 预测输入
重要
和解的概念最难理解,但也是实现无延迟感体验最重要的一步。你可以先简单记住上面的公式,应用到项目中试试看。
权威和预测一般我们认为服务器总是权威的,从服务端接收到的输入称为 权威输入,经权威输入计算出来的状态称为 权威状态。同样的,当我们发出一个输入,但尚未得到服务端的返回确认时,这个输入称为非权威输入,也叫 预测输入。
在网络畅通的情况下,预测输入迟早会按发送顺序变成权威输入。我们需要知道发出去的输入,哪些已经变成了权威输入,哪些还是预测输入。在可靠的传输协议下(例如 WebSocket)你无需关注丢包和包序问题,所以只需简单地对比消息序号即可做到。
和解过程在前述预测的基础上,和解就是我们处理服务端同步的状态的方式。如果使用的是状态同步,那么这个过程是:
- 收到服务端同步来的 权威状态
- 将本地状态立即设为此权威状态
- 在权威状态的基础上,应用当前所有 预测输入
如果使用的是帧同步,那么这个过程是:
- 收到服务端同步来的权威输入
- 将本地状态立即 回滚 至 上一次的权威状态
- 将权威输入应用到当前状态,得到此次的 权威状态
- 在权威状态的基础上,应用当前所有 预测输入
由此可见,状态同步和帧同步只是网络传输的内容不同,但它们是完全可以相互替代的 —— 最终目的都是为了同步权威状态。
例子这有用吗?我们回看一下上面预测的例子,有了和解之后,会变成怎样:
- 你的坐标是 (0,0)
- 你发出了 2 个 右移 指令(每次向右移动 1 个单位),服务器尚未返回
- 权威状态:(0,0)
- 预测输入:右移#1 右移#2
- 预测状态:(2,0) (权威状态 + 预测输入)
- 你又发出了 2 个 右移 指令,服务器尚未返回
- 权威状态:(0,0) (未收到服务端同步,不变)
- 预测输入:右移#1 右移#2 右移#3 右移#4
- 预测状态:(4,0) (权威状态 + 预测输入)
- 服务端发回了你的前 2 个右移指令 (帧同步)
- 上一次的权威状态:(0,0)
- 权威输入:右移#1 右移#2
- 权威状态:(2,0) (上一次的权威状态 + 权威输入)
- 预测输入:右移#3 右移#4 (#1、#2 变成了权威输入)
- 预测状态:(4,0) (权威状态 + 预测输入,之前的拉扯不见了)
看!虽然服务端同步来的权威状态是 “过去” 的,但有了和解之后,拉扯问题解决了,效果如图:
预测 + 和解处理本地输入是非常通用的方式。你会发现,在没有冲突时,网络延迟可以完全不影响操作延迟,就跟单机游戏一样!例如上面移动的例子,如果不发生冲突(例如与它人碰撞),即便网络延迟有 10 秒,你也可以毫无延迟并且平滑的移动。这就是在有延迟的情况下,还能实现无延迟体验的魔术。
冲突那么冲突的情况会怎样呢?比如上面的例子,你发送了 4 次移动指令,但在服务端,第 2 次移动指令之后,服务端插入了一个新输入 —— “你被人一板砖拍晕了”。这意味着,你的后两次右移指令将不会生效(因为你晕了)。那么该过程会变成这样:
- 你的坐标是 (0,0)
- 你发出了 2 个 右移 指令(每次向右移动 1 个单位),服务器尚未返回
- 权威状态:(0,0)
- 预测输入:右移#1 右移#2
- 预测状态:(2,0)
- 你又发出了 2 个 右移 指令,服务器尚未返回
- 权威状态:(0,0)
- 预测输入:右移#1 右移#2 右移#3 右移#4
- 预测状态:(4,0)
- 服务端发回了你的前 2 个右移指令
- 权威状态:(2,0)
- 预测输入:右移#3 右移#4 (#1、#2 变成了权威输入)
- 预测状态:(4,0)
- 服务端发回了与预期冲突的新输入
- 上一次的权威状态:(2,0)
- 权威输入:你被拍晕了 右移#3 右移#4
- 权威状态:(2,0) (因为先被拍晕了,所以后两个右移指令无效)
- 预测输入:无 (所有预测输入都已变为权威输入)
- 预测状态:(2,0)
此时,之前的预测状态 (4,0) 与最新的预测状态 (2,0) 发生了冲突,客户端当然是以最新状态为主,所以你的位置被拉回了 (2,0) 并表现为晕眩。这就是网络延迟的代价 —— 冲突概率。
插值插值指在表现层更新 “其它人” 的状态变化时使用插值动画去平滑过渡。
到目前为止,我们已经获得了自己在本地无延迟的丝滑体验。但在其它玩家的眼中,我们依旧是卡顿的。这是由于同步帧率和显示帧率不一致导致的,所以我们在更新其它人的状态时,并非一步到位的更新,而是通过插值动画来平滑过渡。
重要
预测+和解是解决 自己 的问题,发生在 逻辑层;插值是解决 其它人 的问题,发生在 表现层 。
例如上面的例子,显示帧率是 30fps,服务端的同步帧率是 3 fps。收到服务端同步的其它玩家的状态后,不是立即设置 node.position,而是通过 Tween 使其在一个短暂的时间内从当前位置平滑移动到新位置。如此,在其它玩家眼中,我们看起来也是平滑的了:
解决快节奏有冲突的同步,就是 预测、和解、插值 这 3 个核心思想,掌握了它们你应该就能举一反三,轻松应对各种场景。
|
|