“橡皮筋效应”能解决多人游戏插值卡顿问题吗?

12

我使用UDP编写了多人乒乓球游戏,并在客户端上使用插值和外推来创建平滑的效果。

它可以运行。然而,球体会有些不停顿的感觉。每当接收到新数据包时,它会向前跳动一点点。看起来有点延迟,但还是可以玩的。

肯定有办法让游戏看起来更流畅。我已经了解了“橡皮筋带”(Rubber Banding)的相关知识。从这里开始,最好的方式是什么?

我希望能有能够很好回答我的问题的人。

更新

根据Ivan的要求,这里有一张ping时间的图表。然而,我认为问题存在于客户端平滑代码中。

进入图片描述


你是否有一个中央服务器(可能与客户端共同放置)来协调游戏进程,还是所有客户端都处于平等地位? - tucuxi
我正在使用点对点连接。其中一个客户端充当服务器,并以固定时间步长(20Hz)将游戏状态发送到另一个对等方。 - Z0q
为什么要保持固定的时间步长?为什么不只发送更新? - tucuxi
感谢提供详细信息。@Z0q,您能否分享一些有关客户端代码的详细信息?您缓冲多少更新以进行插值?客户端是自己预测位置还是仅显示服务器更新?如果客户端发现预测位置与刚从服务器接收到的位置不同,您会立即更新用户观察到的效果还是通过平滑逐渐更新? - Ivan
2
当您检测到差异时,应注意时间(m_flPredictionErrorTime)。然后,您需要选择一些时间来进行平滑处理cl_smoothtime。在接近显示代码的某个地方,您需要计算要显示的错误量errorAmount = (currentTimeMillis() - m_flPredictionErrorTime) / cl_smoothtime。将您的差异向量乘以该值vOffset = m_vecPredictionError * errorAmount,并添加到参数向量(x、y、速度等)中。一旦errorAmount大于1,您就可以停止考虑它(完整的增量已经被显示)。 - Ivan
显示剩余7条评论
2个回答

6
从您的之前的问题中了解到,我知道您正在发送每个客户端的球拍和球的位置。然而,只要客户端在每个时间点上都达成关于球拍位置的共识,球的运动就是完全确定的(除去舍入误差),因此您应该避免球的卡顿现象。 每个客户端都应保留其自己的内部状态,包括球拍和球的位置和速度。伪代码类似于以下内容:
// input thread
if input changed,
   alter paddle speed and/or direction
   send timestamped message to inform my opponent of paddle change

// incoming network thread
if paddle packet received
   alter opponent's paddle speed and/or direction at time it was sent
   fix any errors in previously extrapolated paddle position <--- Easy
if ball-packet received
   fix any errors in ball position and speed <--- Tricky

// update entities thread
for each entity (my paddle, opponent paddle, the ball)
   compute updated entity position, adjusted by time-since-last-update
   if ball reached my end, send ball-packet to other side
   draw updated entity

假设两种数据包正在交换:

  • paddle数据包是挡板的时间戳位置+速度,当客户端改变其自己挡板的速度时发送。
  • ball数据包是球的时间戳位置+速度,当球到达客户端(本地)一侧时发送,无论它是否从挡板上弹开。

伪代码对于update-entities线程中所有未知量执行外推(“假设事物按照惯常方式继续移动”)。唯一出现问题的点用<---箭头标记。

您可以通过将挡板位置扭曲到新位置来轻松纠正挡板位置,并可能在短时间内插值移动以使其更加平滑。

如果两个客户端都大致同意,则很容易纠正球位置(然后您可以再次使用插值技巧,以进一步平滑它)。但是,一个客户端可能会看到近似错过,而另一个客户端可能会看到近似命中。在这种情况下,由于您正在使用点对点模型,我们让本地客户端进行调用,并向对手解释发生了什么(在另一种设计中,您将有一个中央服务器做出这些决策;这是避免作弊的好方法)。如果两个客户端不同意,您无法避免出现一个丑陋的跳跃-但是希望这种情况相对较少且短暂,除非它与ping峰值重合。


你好,感谢您的回答。那不是我之前的问题。如果两个客户端都不同意,我该如何避免这些跳跃?这才是我的问题。 - Z0q
我的回答是,你太频繁地交换了错误类型的信息,导致了经常发生且可以避免的争论。你应该只从知道某些东西的客户端向还不知道这些东西的客户端交换信息,然后信任传入的信息。由于网络延迟,客户端B无法知道客户端A最近的挥拍动作。因此,客户端B不应告诉客户端A有关客户端A动作的任何信息。 - tucuxi

2
“消除这种效应的其中一种方法是在对客户端进行错误预测修正时使用平滑处理。”
“它是如何工作的:在代码的某个点,您会发现球的位置和客户端位置不同。”

Discrepancy

将其立即应用于客户端代码作为更正措施(这也是您可以看到这些跳跃的原因之一)的做法改为在一段时间内执行,例如500毫秒的cl_smoothtime

首先,您的程序应该存储错误检测事件发生时的时间m_flPredictionErrorTime

public void onErrorDetected() {
    this.m_flPredictionErrorTime = System.currentTimeMillis();
}

在显示代码附近,你会计算要显示多少错误。以下是一些伪代码供参考。
public void draw() {
    Point preditctionError = this.clientPredictedBallCoordinates - this.serverCoordinates;
    Point deltaToDisplay = calculateErrorVector(preditctionError);
    Point positionToDisplay = clientPredictedBallCoordinates + deltaToDisplay;
    // actually draw the ball here
}

public Point calculateErrorVector(Point coordinatesDelta) {
     double errorAmount = ( System.currentTimeMillis() - this.m_flPredictionErrorTime ) / this.cl_smoothtime.
     if (errorAmount > 1.0) {
         // whole difference applied in full, so returning zero delta
         return new Point(0,0);
     }
     if (errorAmount < 0) {
         // no errors detected yet so return zero delta
         return new Point(0,0);
     }
     Point delta = new Point(coordinates.x*errorAmount, coordinates.y*errorAmount);
     return delta;
}

我从Source Multiplayer Networking wiki中获取了这个想法。 Cpp的实际代码示例可以在其SDK中找到,该示例围绕GetPredictionErrorSmoothingVector function展开。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接