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

Unity Photon Fusion 2D多人游戏冲刺机制卡顿问题咨询

2D多人游戏Photon Fusion冲刺机制抖动问题解决方法

问题描述

我基于Unity Photon Fusion开发2D多人游戏的冲刺机制,需求如下:

  • 玩家按下LShift时进入低速移动并显示蓄力滑块UI
  • 按住LMb进行蓄力,释放后执行冲刺,冲刺结束后恢复默认移动速度
  • 按下RMb可在蓄力过程中或蓄力前取消蓄力,不执行冲刺并恢复默认速度

目前所有功能均正常,仅冲刺部分存在客户端抖动的移动效果。尝试过调整NetworkRigidbody2D的插值数据源(Auto、snapshots、predicted、无插值),最初本地处理DashMove时,状态authority会覆盖客户端玩家位置;改用RPC后因基于时间而非tick导致抖动;让状态authority本地处理冲刺时,玩家会直接跳转到冲刺最终位置。

当前实现代码:

using UnityEngine;
using Fusion;

public class Player : NetworkBehaviour
{
    public Rigidbody2D PlayerRigidbody;

    private float _dashSpeed = DataUtils.DashSpeed; // Speed of the dash
    private float _lastDashTime = -Mathf.Infinity;
    private Vector2 _movement;

    public float minDuration; // Minimum ability duration
    public float maxDuration = DataUtils.MaxDashDuration; // Maximum ability duration

    public CharacterCanvasBase Canvas;

    public override void FixedUpdateNetwork()
    {
        // player direction input to dash
        _movement = DirectionInput;

        // below is psuedo code
        if (LShiftPressed)
        {
            StartCoroutine(Dash());
        }
    }

    private IEnumerator Dash()
    {
        _lastDashTime = Time.time;

        Vector2 startDashPosition = PlayerRigidbody.position;
        Vector2 dashDirection = _movement.normalized;

        // Check if the player is moving. If not, dash downward
        if (dashDirection == Vector2.zero)
        {
            dashDirection = Vector2.down; // Or any default direction you prefer
        }

        // Get charge level to determine dash speed and possibly duration
        float chargeLevel = Canvas.GetComponent<HexCanvas>().ChargeController.GetChargeProgress();
        float adjustedDuration = minDuration + (maxDuration - minDuration) * Mathf.Pow(chargeLevel, 2);
        float dashEndTime = Time.time + adjustedDuration;

        while (Time.time < dashEndTime)
        {
            float dashProgress = (Time.time - _lastDashTime) / adjustedDuration;
            RPC_DashMove(startDashPosition, dashDirection, dashProgress, _dashSpeed * adjustedDuration);
            yield return null;
        }
    }

    [Rpc(RpcSources.All, RpcTargets.All)]
    public void RPC_DashMove(Vector2 startDashPosition, Vector2 dashDirection, float dashProgress, float dashSpeed)
    {
        PlayerRigidbody.MovePosition(Vector2.Lerp(startDashPosition, startDashPosition + dashDirection * dashSpeed, dashProgress));
    }
}

正确实现方式

1. 用Networked变量同步冲刺状态

Fusion依赖tick而非实时时间同步,必须把冲刺的关键状态做成Networked变量,让状态权威端(State Authority)统一控制,其他端通过同步变量实现平滑移动:

// 添加Networked变量存储冲刺状态
[Networked] private bool IsDashing { get; set; }
[Networked] private Vector2 DashStartPos { get; set; }
[Networked] private Vector2 DashDirection { get; set; }
[Networked] private float DashTotalDuration { get; set; }
[Networked] private float DashStartTime { get; set; } // 使用NetworkRunner.Time替代本地Time.time

2. 状态权威端处理冲刺逻辑

抛弃本地协程,在FixedUpdateNetwork中由状态权威端触发并计算冲刺进度,所有端基于同步的状态变量执行移动:

public override void FixedUpdateNetwork()
{
    _movement = DirectionInput;

    // 仅状态权威端处理冲刺触发逻辑
    if (HasStateAuthority && LShiftPressed && !IsDashing)
    {
        StartDash();
    }

    // 所有端同步处理冲刺移动
    if (IsDashing)
    {
        UpdateDashMovement();
    }
}

private void StartDash()
{
    IsDashing = true;
    DashStartPos = PlayerRigidbody.position;
    DashDirection = _movement.normalized;
    if (DashDirection == Vector2.zero) DashDirection = Vector2.down;

    float chargeLevel = Canvas.GetComponent<HexCanvas>().ChargeController.GetChargeProgress();
    DashTotalDuration = minDuration + (maxDuration - minDuration) * Mathf.Pow(chargeLevel, 2);
    DashStartTime = Runner.Time;
}

private void UpdateDashMovement()
{
    float elapsedTime = Runner.Time - DashStartTime;
    // 结束冲刺
    if (elapsedTime >= DashTotalDuration)
    {
        IsDashing = false;
        return;
    }

    float dashProgress = elapsedTime / DashTotalDuration;
    Vector2 targetPos = DashStartPos + DashDirection * _dashSpeed * DashTotalDuration;
    Vector2 newPos = Vector2.Lerp(DashStartPos, targetPos, dashProgress);

    // 状态权威端直接更新刚体位置,其他端通过NetworkRigidbody2D同步插值
    if (HasStateAuthority)
    {
        PlayerRigidbody.MovePosition(newPos);
    }
}

3. 配置NetworkRigidbody2D插值

  • 将NetworkRigidbody2D的Interpolation Source设置为Predicted,本地玩家使用预测插值降低延迟
  • 确保Prediction Mode设置为Interpolated,让客户端基于同步的tick数据平滑过渡位置

4. 蓄力与取消逻辑优化

  • 蓄力阶段仅在本地处理UI显示,释放LMb时再通知状态权威端启动冲刺
  • 按下RMb取消时,本地重置UI状态,若冲刺未启动则通知权威端无需触发冲刺

问题根源解析

  • 协程依赖本地Time.time:Fusion同步基于服务器tick,本地时间与服务器tick不同步导致位置偏差
  • 频繁RPC调用:每秒数十次RPC会引发网络延迟和位置跳变,Networked变量同步更高效稳定
  • 进度计算不一致:之前用本地时间计算冲刺进度,客户端与权威端进度不同步,导致同步时直接跳转到最终位置

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 18:42:34