You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

服务器高发送频率下Entity interpolation平滑性问题排查

多人游戏玩家位置插值抖动问题

我使用Unity和Riptide开发多人游戏,Riptide用于客户端/服务器间的数据包传输。当前逻辑是客户端发送自身位置到服务器,服务器转发给其他已连接客户端,目标是实现其他玩家位置状态的平滑过渡,暂未实现服务器权威机制。

客户端以固定频率向服务器发送位置,服务器以稍低频率向所有客户端同步玩家位置。目前发现:服务器使用低发送频率(如0.5秒)时,插值效果平滑;但测试高频率(如0.1秒)时,其他玩家会出现延迟、抖动、弹跳现象,移动时没有恒定加速度,不符合预期。

我参考了实体插值相关资料后,在客户端实现了如下插值代码:

// 收到服务器发来的位置数据包时调用
void UpdatePlayerPositionBuffer(int id, Vector2 newPosition) {
    Player player = null;
    ClientManager.Instance.OtherPlayers.TryGetValue(id, out player);
    if (player != null) {
        player.PositionBuffer.Add((Time.time, newPosition));
    }
}

// Unity每帧调用,Time.time为总运行时间
void Update() {
    // 按照Valve推荐,插值延迟设为服务器发送频率的两倍
    float renderingTime = Time.time - interpolationDelay;
    
    foreach (Player p in ClientManager.Instance.OtherPlayers.Values) {
        while (p.PositionBuffer.Count >= 2 && p.PositionBuffer[1].time <= renderingTime) {
            p.PositionBuffer.RemoveAt(0);
        }

        if (p.PositionBuffer.Count >= 2) {
            float t0 = p.PositionBuffer[0].time;
            float t1 = p.PositionBuffer[1].time;

            bool inbetween = t0 < renderingTime && t1 > renderingTime;
            if (!inbetween) {
                continue;
            }

            Vector2 p0 = p.PositionBuffer[0].position;
            Vector2 p1 = p.PositionBuffer[1].position;

            p.Position = Vector2.Lerp(p0, p1, (renderingTime - t0) / (t1 - t0));
            spawner.GetPlayerObject(p.Id).transform.position = p.Position;
        }
    }    
}

public class Player {
    public int Id;
    public Vector2 Position;
    public List<(float time, Vector2 position)> PositionBuffer = new();

    public Player(int id, Vector2 position) {
        Id = id;
        Position = position;
    }
}

演示视频中,红色方块为玩家,带白点的是本地玩家,可以看到本地玩家移动平滑,但其他玩家存在明显的延迟抖动。

问题原因及修复方案

1. 时间戳使用错误

当前代码用客户端本地的Time.time作为位置数据包的时间戳,但这是客户端接收数据包的时间,不是服务器发送该位置的时间,也不是原始客户端发送位置的时间。高频率下,网络波动导致的接收时间偏差会被放大,直接破坏插值的时间基准,引发抖动。

修复:服务器转发位置时,带上服务器发送该数据包的时间戳(或原始客户端发送位置时的时间戳,由服务器同步后转发),客户端用这个统一的时间戳存入PositionBuffer,而非本地接收时间。

2. 插值延迟设置不合理

Valve推荐的插值延迟是基于服务器tick rate的,并非简单的“双倍服务器发送频率”。当服务器发送频率提升到0.1秒(10Hz),双倍延迟为0.2秒,若网络延迟波动超过这个值,会导致renderingTime跑到缓冲队列的最新数据之后,无法触发插值,直接出现位置跳变。

修复:

  • 动态调整插值延迟:基于最近几个数据包的网络延迟平均值,再额外添加50ms左右的缓冲
  • 当缓冲队列中没有足够的插值点(比如renderingTime超过最新的t1),不要直接跳过,而是用外推法预测位置:
    else if (p.PositionBuffer.Count == 1) {
        var latest = p.PositionBuffer[0];
        float timeElapsed = renderingTime - latest.time;
        // 若能从服务器同步速度数据,这里用真实速度更准确
        Vector2 velocity = (latest.position - p.Position) / Time.deltaTime;
        p.Position = latest.position + velocity * timeElapsed;
        spawner.GetPlayerObject(p.Id).transform.position = p.Position;
    }
    

3. 位置缓冲未做去重过滤

高频率发送时,服务器可能转发重复位置(比如玩家未移动),或因网络丢包重传导致重复数据。这些数据会让缓冲队列中出现时间相近但位置相同的点,插值时t1-t0趋近于0,导致Lerp权重异常,引发跳变。

修复:

  • 添加过滤逻辑:新位置与缓冲队列最后一个位置的距离小于阈值(如0.01)时,不加入队列
  • 检查时间戳有效性:确保新时间戳大于缓冲队列最后一个时间戳,避免添加旧数据:
    if (player != null) {
        bool shouldAdd = player.PositionBuffer.Count == 0 
            || player.PositionBuffer.Last().time < serverTimestamp 
            || Vector2.Distance(newPosition, player.PositionBuffer.Last().position) > 0.01f;
        if (shouldAdd) {
            player.PositionBuffer.Add((serverTimestamp, newPosition));
        }
    }
    

4. 时间基准受帧率/时间缩放影响

Time.time会受Unity的Time.timeScale影响,且客户端帧率波动时,插值时间计算会出现偏差。改用Time.unscaledTime或Time.realtimeSinceStartup作为时间基准更可靠。

5. 线程安全问题

Riptide的数据包接收回调可能在主线程外执行,直接在回调中操作PositionBuffer会导致Update遍历队列时数据不一致,引发抖动。

修复:将接收的位置数据先存入线程安全队列,在Update中再从队列取出并更新PositionBuffer。


内容的提问来源于stack exchange,提问作者lomstfer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 04:34:49