为什么我的玩家的移动速度会发生变化? (在 Unity 中使用 Fishnet)

Why is my player's movement speed changing? (using Fishnet with Unity)

我是 Unity 的新手,我已经开始学习如何使用 Fishnet 网络。我创建了一个基本的玩家移动脚本,它可以比网络变换更快地同步玩家位置。但是我 运行 遇到了一个我不知道如何解决的奇怪问题。

在我的场景中,我有一个网络管理器,它在连接后生成我的播放器预制件——一个带有播放器脚本和网络对象的简单精灵。我没有添加网络转换,因为我将手动同步每个玩家的位置以减少客户端之间的延迟。这是播放器脚本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using FishNet.Object;

public class Player : NetworkBehaviour
{
    private void Update()
    {
        if (IsOwner) //only the client that owns this object will run this code
        {
            //get input, send it to server
            float horizontalInput = Input.GetAxisRaw("Horizontal");
            float verticalInput = Input.GetAxisRaw("Vertical");
            RpcMoveCharacter(horizontalInput, verticalInput);
        }

        //since this is an observers rpc, only the server will call it
        RpcSendCharacterPosition(transform.position.x, transform.position.y);
    }

    [ServerRpc]
    public void RpcMoveCharacter(float x, float y)
    {
        //change the position of the server's instance of the player
        transform.Translate(x * 10f * Time.deltaTime * Vector3.right);
        transform.Translate(y * 10f * Time.deltaTime * Vector3.up);
    }

    [ObserversRpc]
    public void RpcSendCharacterPosition(float x, float y)
    {
        if (IsClientOnly)
        {
            //ensure clients' instance of the player match the server's' position
            transform.position = new Vector2(x, y);
        }
    }
}

该脚本运行完美...除了一个问题:玩家的移动速度对于两个玩家来说并不一致。只有当我构建和 运行 我的游戏,然后连接两个版本的游戏时才会出现问题。

当任何一个玩家是主机(服务器+客户端)时,他们的玩家对象在两个屏幕上都以中等速度移动。这是预期的速度。

当来自我的统一编辑器 window 的游戏 运行ning 的 版本只是一个客户端时,玩家在两个屏幕上都以快速移动--比预期快很多倍。

当我使用 'build and run' 创建的游戏的 版本只是一个客户端时,玩家在两个屏幕上的移动速度都很慢——比预期慢很多倍.

我已经测试了我能想到的一切。我做的一项测试是防止网络管理员生成播放器预制件,提前将播放器对象放置在场景中,并将其转换为:

    private void Update()
    {
        if (IsOwner)
        {
            float horizontalInput = Input.GetAxisRaw("Horizontal");
            float verticalInput = Input.GetAxisRaw("Vertical");
            RpcMoveCharacter(horizontalInput, verticalInput);
        }

        RpcSendCharacterPosition(transform.position.x, transform.position.y);
    }

    [ServerRpc]

对此:

    private void Update()
    {
        //now anyone can control the player object
        float horizontalInput = Input.GetAxisRaw("Horizontal");
        float verticalInput = Input.GetAxisRaw("Vertical");
        RpcMoveCharacter(horizontalInput, verticalInput);

        RpcSendCharacterPosition(transform.position.x, transform.position.y);
    }
    //same effect as note above
    [ServerRpc (RequireOwnership = false)]

为了看看玩家重生功能是否有问题。我的更改没有任何影响——根本没有任何改变。 如果我的 editor 只是一个客户端,它仍然会移动播放器太快,如果我的 build 只是一个客户端,它仍然移动播放器太慢。

我尝试的另一件事是制作一个全新的项目,以防我在上一个项目中奇怪地切换了一个设置或其他东西。一旦我创建了一个新项目,我所做的就是导入 fishnet,将 fishnet 的默认 NetworkManager 对象添加到我的场景中,创建一个名为 player 的简单预制件,将网络对象和原始播放器脚本添加到播放器预制件,将网络管理器设置为生成播放器预制件,然后重试。运气不好——一切都完全一样。

有什么想法吗?我被困在这里——我不知道还能尝试什么,因为 code/scene 中的所有内容似乎都运行良好。我不明白为什么我的构建行为会与我的编辑器的播放模式不同,无论哪个是服务器(或主机),哪个只是客户端。

谢谢!

所以我真的不知道这个 Fishnet 是如何工作的。

但正如一般所说,在任何网络中你永远不能依赖

  • 您的所有设备 运行 处于相同的 FPS(每秒帧数)
  • 您的网络消息立即到达 server/other 客户端
  • 您的网络消息到达 server/other 客户端的时间间隔与您结束它们的时间间隔完全相同

所以我宁愿做的是

  • 首先不要每帧发送网络消息而是一些固定的时间间隔(例如经常使用每0.2秒)

  • 宁愿在本地立即处理所有本地移动

    如果您将用户输入发送到服务器并且必须等到通过接收结果位置来应用它,这对用户体验来说会非常糟糕。这会导致 2 倍的网络延迟,这对本地用户来说是极其不可思议的。

  • 而不是增量而是同步结果位置值。

    通过这种方式,您可以确保所有玩家都与实际结果位置同步,并且它也适用于稍后加入会话或可能由于网络延迟而错过一些输入消息的玩家。

所以我会做类似的事情

public class Player : NetworkBehaviour
{
    // Interval in seconds how often to send your position to the server/clients
    [SerializeField] private float sendInterval = 0.2f;
    
    // How fast you can move in units per second
    [SerializeField] private float moveSpeed = 10f;

    // Use this to adjust your input sensitivities 
    [SerializeField] [Min(0)] private float inputSensitivityX = 1f;
    [SerializeField] [Min(0)] private float inputSensitivityY = 1f;

    // Might have to play a bit with this value to make smooth interpolation faster or slower
    // 5 is an arbitrary value but works quite good from experience
    // depends on your sendInterval and movespeed as well
    [SerializeField] privte float interpolation = 5f;

    // keeps track of passed time
    private float sendTimer;

    private Vector2 receivedTargetPosition;

    private void Start()
    {
        if(!IsOwner)
        {
            receivedTargetPosition = transform.position;
        }
    }

    private void Update()
    {
        //only the client that owns this object will run this code
        if (IsOwner) 
        {
            //get input
            var horizontalInput = Input.GetAxisRaw("Horizontal");
            var verticalInput = Input.GetAxisRaw("Vertical");

            var input = new Vector2(horizontalInput * inputSensitivityX, verticalInput * inputSensitivityY);
            // Makes sure that you always have maximum 1 magnitude for the input
            input = Vector2.ClampMagnitude(input, 1f);

            // use the rotation to already rotate this vector from local into world space
            input = trasform.rotation * input;

            // Here you want the deltaTime of THIS DEVICE
            var movement = moveSpeed * Time.deltaTime * input;

            // Move your player LOCALLY
            transform.position += (Vector3)movement;
        }
        // If you are not the owner you rather apply the received position
        else
        {
            // I would e.g. smoothly interpolate somewhat like
            transform.position = Vector3.Lerp(transform.position, receivedTargetPosition, interpolation * Time.deltaTime);
        }

        // Check if next send time interval has passed
        sendTimer += Time.deltaTime;
        if(sendTimer >= sendInterval)
        {
            sendTimer = 0;

            if(IsServer)
            {
                RpcSendPositionToClients(transform.position.x, transform.position.y);
            }
            else
            {
                RpcSendPositionToServer(transform.position.x, transform.position.y);
            }
        } 
    }

    [ServerRpc]
    public void RpcSendPositionToServer(float x, float y)
    {
        // just in case
        // the owner already gets its position in Update so nothing to do
        if(IsOwner) return;

        //change the position of the server's instance of the player
        receivedTargetPosition = new Vector2(x, y);
    }

    [ClientRpc]
    public void RpcSendPositionToClients(float x, float y)
    {
        // Owner and server already know the positions
        if(IsOwner || IsServer) return;
        
        receivedTargetPosition = new Vector2(x, y);
    }
}