多人浏览器游戏:线性插值导致抖动和跳跃
Multiplayer browser game: linear interpolation causing jittering and jumping
我正在开发一款浏览器多人游戏,其中每个客户端都会插入(线性)服务器发送的实体帧。它在高帧率(> 30fps)下看起来并不太糟糕,但在较低的帧率(<30fps)下开始抖动并冻结和跳跃以及非常低的帧率(<10fps)。我想降低帧率,我知道这是可能的(请参阅 Brutal.io,它以 10fps 发送更新)。
这是我使用的基本算法:
- 服务器以帧速率(比如 10fps)发送更新
- 客户端以帧速率(比如 60fps)渲染游戏
- 客户端不更新屏幕上的实体以直接匹配服务器的数据:这看起来很紧张
- 相反,它使用线性插值函数来平滑服务器更新之间的帧
- 以 10:60fps 为例,客户端将渲染中间的 6 帧以创建流畅的动画
- 它通过测量服务器更新之间的增量(差异)以及客户端渲染帧来实现这一点
- 然后通过将客户端增量除以服务器增量来获得乘数
- 然后,调用线性插值函数,使用屏幕位置、服务器位置和倍数生成新的屏幕位置
此片段不包含具体代码,但应该足以演示基本概述(有关信息,请参阅代码中的注释):
var serverDelta = 1; // Setting up a variable to store the time between server updates
// Called when the server sends an update (aiming for 10fps)
function onServerUpdate(message) {
serverDelta = Date.now() - lastServerFrame;
}
// Called when the client renders (could be as high as 60fps)
var onClientRender() {
var clientDelta = Date.now() - lastUpdateFrame;
// Describes the multiplier used for the linear interpolation function
var lerpMult = clientDelta / serverDelta;
if (lerpMult > 1) { // Making sure that the screen position doesn't go beyond the server position
lerpMult = 1;
}
lastUpdateFrame = Date.now();
...
// For each entity
// ($x,$y) is position sent by server, (x,y) is current position on screen
entity.x = linearInterpolate(entity.x, entity.$x, lerpMult / 2);
entity.y = linearInterpolate(entity.y, entity.$y, lerpMult / 2);
}
function linearInterpolate(a, b, f) {
return (a * (1 - f)) + (b * f);
};
如上所述,这会在运动中产生抖动和跳跃。我做错了什么吗?我怎样才能使这个动作流畅?
插值必须在两个服务器状态之间。您可以保留在客户端上接收到的最后一个 X 服务器状态的历史记录。每个服务器状态代表一个特定的帧。
例如,假设您的客户端保留了以下服务器状态及其帧:
state[0] = {frame: 0, ... };
state[1] = {frame: 10, ... };
state[2] = {frame: 20, ... };
如果客户端现在正在渲染第 15 帧,则它必须在 state[1]
和 state[2]
之间的中间位置进行插值。公式是
// prev=1, next=2
let interpolatePercent = (clientFrame - serverState[prev].frame) / serverUpdateRate;
entity.x = interpolatePercent * (serverState[next].entity.x - serverState[prev].entity.x) + serverState[prev].entity.x;
在您的代码中,lerpMult
很可能大于 1,在这种情况下,您现在进行的是外推而不是内插,这要困难得多。
您可能还想看看为浏览器多人游戏实现插值(和外插)的开源库:https://github.com/lance-gg/lance
我正在开发一款浏览器多人游戏,其中每个客户端都会插入(线性)服务器发送的实体帧。它在高帧率(> 30fps)下看起来并不太糟糕,但在较低的帧率(<30fps)下开始抖动并冻结和跳跃以及非常低的帧率(<10fps)。我想降低帧率,我知道这是可能的(请参阅 Brutal.io,它以 10fps 发送更新)。
这是我使用的基本算法:
- 服务器以帧速率(比如 10fps)发送更新
- 客户端以帧速率(比如 60fps)渲染游戏
- 客户端不更新屏幕上的实体以直接匹配服务器的数据:这看起来很紧张
- 相反,它使用线性插值函数来平滑服务器更新之间的帧
- 以 10:60fps 为例,客户端将渲染中间的 6 帧以创建流畅的动画
- 它通过测量服务器更新之间的增量(差异)以及客户端渲染帧来实现这一点
- 然后通过将客户端增量除以服务器增量来获得乘数
- 然后,调用线性插值函数,使用屏幕位置、服务器位置和倍数生成新的屏幕位置
此片段不包含具体代码,但应该足以演示基本概述(有关信息,请参阅代码中的注释):
var serverDelta = 1; // Setting up a variable to store the time between server updates
// Called when the server sends an update (aiming for 10fps)
function onServerUpdate(message) {
serverDelta = Date.now() - lastServerFrame;
}
// Called when the client renders (could be as high as 60fps)
var onClientRender() {
var clientDelta = Date.now() - lastUpdateFrame;
// Describes the multiplier used for the linear interpolation function
var lerpMult = clientDelta / serverDelta;
if (lerpMult > 1) { // Making sure that the screen position doesn't go beyond the server position
lerpMult = 1;
}
lastUpdateFrame = Date.now();
...
// For each entity
// ($x,$y) is position sent by server, (x,y) is current position on screen
entity.x = linearInterpolate(entity.x, entity.$x, lerpMult / 2);
entity.y = linearInterpolate(entity.y, entity.$y, lerpMult / 2);
}
function linearInterpolate(a, b, f) {
return (a * (1 - f)) + (b * f);
};
如上所述,这会在运动中产生抖动和跳跃。我做错了什么吗?我怎样才能使这个动作流畅?
插值必须在两个服务器状态之间。您可以保留在客户端上接收到的最后一个 X 服务器状态的历史记录。每个服务器状态代表一个特定的帧。
例如,假设您的客户端保留了以下服务器状态及其帧:
state[0] = {frame: 0, ... };
state[1] = {frame: 10, ... };
state[2] = {frame: 20, ... };
如果客户端现在正在渲染第 15 帧,则它必须在 state[1]
和 state[2]
之间的中间位置进行插值。公式是
// prev=1, next=2
let interpolatePercent = (clientFrame - serverState[prev].frame) / serverUpdateRate;
entity.x = interpolatePercent * (serverState[next].entity.x - serverState[prev].entity.x) + serverState[prev].entity.x;
在您的代码中,lerpMult
很可能大于 1,在这种情况下,您现在进行的是外推而不是内插,这要困难得多。
您可能还想看看为浏览器多人游戏实现插值(和外插)的开源库:https://github.com/lance-gg/lance