Unity 使用NetCode for GameObject(NGO)实现联机

目录

一、安装(Netcode for GameObject)NGO

二、基本设置

添加NetworkManager:

设置NetworkManage属性面板

1.选择Unity Transport

2.添加具有网络行为的物体

1.Network Prefabs Lists

2.设置Player Prefab

三、 网络物体的设置

1.Network Object组件

2.NetworkTransform组件

3.举例:设置一个玩家预制体

完善游戏:

四、联机两种实现方式

1.服务器权威

2.客户端权威

​编辑

五、对于联机的理解


一、安装(Netcode for GameObject)NGO

unity项目中想要设计联机游戏,需要安装与联网相关的资源包(NGO),新建的unity项目默认不包含这个资源包,并且NGO的安装需要unity版本在2021.3以上.(我的版本是2022.3)

unity界面 => Window => Pakage Manager => Unity Registry ;

在搜索框中找到Netcode for GameObject;

直接点击install;

Unity 使用NetCode for GameObject(NGO)实现联机_第1张图片

较低版本的unity也可以使用NGO,具体操作可以参考官方链接

也可以参考官方教程"Hello World"

中文文档icon-default.png?t=N7T8https://blog.csdn.net/weixin_44499065/article/details/132225657

安装好资源包后就可以开始进行联机的基本设置了。


二、基本设置

在整个游戏中必须有且只有一个NetworkManager存在于游戏中,它作为整个网络的核心对网络进行全局管理;

添加NetworkManager:

在场景中添加最常用的方法就是在场景中新建一个空物体命名为NetworkManager(命名无所谓,看个人习惯,这样命名比较方便),为物体添加组件NetworkManager.

Unity 使用NetCode for GameObject(NGO)实现联机_第2张图片

设置NetworkManage属性面板

1.选择Unity Transport

在NetworkManager组件的属性面板上,会出现警告。

Unity 使用NetCode for GameObject(NGO)实现联机_第3张图片

在Select transport理选择Unity Transport,警告就会消失(选择后unity会自动加上对应的Transport组件)

Unity 使用NetCode for GameObject(NGO)实现联机_第4张图片

2.添加具有网络行为的物体

1.Network Prefabs Lists

在Network Prefabs Lists上会有一个默认的列表,不包含任何物体;

Unity 使用NetCode for GameObject(NGO)实现联机_第5张图片

它在下方的资产列表里;

Unity 使用NetCode for GameObject(NGO)实现联机_第6张图片

双击

Unity 使用NetCode for GameObject(NGO)实现联机_第7张图片

点击右上角的锁图标可以将当前属性面板固定,方便将预制体拖入

Unity 使用NetCode for GameObject(NGO)实现联机_第8张图片

在属性面板中可以将玩家预制体拖动到列表中即可;

Unity 使用NetCode for GameObject(NGO)实现联机_第9张图片

2.设置Player Prefab

Player Prefab的作用:当客户端加入游戏时,系统会为该客户端生成一个Player Prefab中的物体;

所以,如果想要在联网的一瞬间生成玩家,就要将玩家预制体拖入Player Prefab中(如果不需要,就跳过“设置Player Prefab”这个步骤)

将玩家预制体拖入Player Prefab,记得要将场景中已存在的玩家删除;

如下图,在没有设置Player Prefab时,点击 启动一个Host( 主机端),没有生成玩家;



当设置了以后,启动主机端,会自动生成一个玩家;


三、 网络物体的设置

任何具有网络行为的物体都要加上Network Object组件.

如果想要同步位置会进行动态改变的物体,那么就要添加Network Transform。

1.Network Object组件

Unity 使用NetCode for GameObject(NGO)实现联机_第10张图片每个具有网络行为的物体都必须添加一个Network Object组件,该组件的作用是为当前物体生成一个在网络中唯一的id,方便区别于其他物体。

当一个父组件具有Network Object组件后,子组件可以不添加,如果需要找到子组件可以通过父组件寻找到;

NetworkObject组件详情

2.NetworkTransform组件

Unity 使用NetCode for GameObject(NGO)实现联机_第11张图片

Network Transfoem组件最重要的作用就是将权威端实例的信息同步到非权威端实例(unity默认的权威端是服务器端)

也就是将玩家的信息从服务器端同步到客户端;

官方文档是这样描述的

Some of theNetworkTransformproperties are automatically synchronized by the authoritative instance to all non-authoritative instances. It isimportant to notethat when any synchronized property changes the NetworkTransform is effectively "teleported" (i.e. all values re-synchronized and interpolation is reset) which can cause a single frame delta in position, rotation, and scale (depending upon what is being synchronized).Always keep this in mind when making adjustments to NetworkTransform properties during runtime.

翻译:

一些NetworkTransform权威实例会自动将属性同步到所有非权威实例。它是值得注意的重要事项当任何同步属性更改时,NetworkTransform有效地“远程传输”(即所有值重新同步,插值重置),这可能导致位置、旋转和缩放(取决于正在同步的内容)上的单个帧增量。在运行时调整NetworkTransform属性时,请始终牢记这一点。

在Syncing字段中可以决定要同步的信息,将不需要同步的信息取消勾选可以节省CPU的成本和带宽。实际上这个勾选只影响权威端,非权威端只能接权威端选择需要同步的轴的更新。

非常建议去看一看官方文档里对NetworkTransform的解释,有利于理解联机是如何实现的;

NetworkTransform组件详情

3.举例:设置一个玩家预制体

在Assets中新建一个文件夹,命名为Prefabs,用来存放预制体

Unity 使用NetCode for GameObject(NGO)实现联机_第12张图片

在场景中新建一个胶囊体(Capsule)作为人物角色命名为Character,在人物上新建两个个子物体,一个物体的类型为Camera命名为CharacterCamera,另一个物体的类型为Cube,调整Cube的位置,使它不在摄像机的视野内

相机的作用是用相机自身绕x轴的旋转模拟人的抬头出现的视角变化,如果只用胶囊体的话,抬头就会造成整个胶囊体的旋转,显然不符合逻辑;(想象一下,人进行抬头,整个人跟着一起转动的画面)

Cube的作用是方便观察物体是否发生旋转;

相机的命名尽量相同,不然后面的角色控制器可能会出问题;

Unity 使用NetCode for GameObject(NGO)实现联机_第13张图片

给 Character 物体添加上Network Object、Network Transform组件;

给 CharacterCamera 添加 Network Transform组件;

在NetworkTransform中勾选需要同步的信息;

Unity 使用NetCode for GameObject(NGO)实现联机_第14张图片

Unity 使用NetCode for GameObject(NGO)实现联机_第15张图片

在Assets中新建一个文件夹,命名为Scripts,用来存放脚本;

在Scripts中新建一个文件夹,命名为Character,用来保存与角色相关的脚本;

Unity 使用NetCode for GameObject(NGO)实现联机_第16张图片

在Character文件夹中新建一个脚本,命名为Character_serverauthoritative_move,作为角色控制器;

Character_serverauthoritative_move.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;

public class Character_serverauthoritative_move : NetworkBehaviour
{
    #region move_variable
    [SerializeField] 
    private float moveSpeed;
    private float move_x;
    private float move_z;
    private Vector3 velocity;
    private Vector3 new_position = Vector3.zero;
    #endregion

    #region rotate_variable
    [SerializeField]
    private float rotaeSpeed;
    [SerializeField]
    private float maxRotation;
    private float rotae_x;
    private float rotate_y;

    private NetworkVariable CharacterNextRotation = new NetworkVariable();
    private NetworkVariable CamerarotationTotal = new NetworkVariable();
    Transform CharacterCameraTransform;
    #endregion

    private void Awake()
    {
        CharacterCameraTransform = transform.Find("CharacterCamera");//获取为角色物体子对象的相机(命名一定要与角色相机的名称相同,否者找不到,返回相机的transform组件)

        if (CharacterCameraTransform == null)
        {
            Debug.Log("Can't find Character'Camera,please check!!!");
        }
    }

    private void Start()
    {
        #region move_variable_initialization
        moveSpeed = 5f;
        move_x = 0;
        move_z = 0;
        #endregion

        #region rotate_variable_initialization
        rotaeSpeed = 8f;
        maxRotation = 80f;
        rotae_x = 0;
        rotate_y = 0;
        #endregion

        Cursor.lockState = CursorLockMode.Locked;
    }

    #region rotate_function
    private void Update()
    {
        if(IsClient)
        {
            if(IsLocalPlayer)
            {
                GetRotateInput();
                Vector4 temp = new Vector4(rotae_x, rotate_y, rotaeSpeed, maxRotation);
                RefeshAnglesServerRpc(temp);
            }
        }
        if (IsServer)
        {
            foreach (ulong uid in NetworkManager.Singleton.ConnectedClientsIds)
            {
                NetworkManager.Singleton.SpawnManager.GetPlayerNetworkObject(uid).GetComponent().Rotate();
            }
        }
    }

    private void GetRotateInput()
    {
        rotae_x = Input.GetAxis("Mouse Y");
        rotate_y = Input.GetAxis("Mouse X");
    }

    [ServerRpc]
    private void RefeshAnglesServerRpc(Vector4 temp)
    {
        if ((temp.z == 0) || (temp.x == 0 && temp.y == 0)) return;
        
        if(temp.y!=0)
        {
            CharacterNextRotation.Value += temp.y * temp.z;
        }
        if(temp.x!=0)
        {
            CamerarotationTotal.Value -= temp.x * temp.z;
            CamerarotationTotal.Value = Mathf.Clamp(CamerarotationTotal.Value, -temp.w, temp.w);
        }
    }

    private void Rotate()
    {
        transform.localEulerAngles = new Vector3(0,CharacterNextRotation.Value,0);
        CharacterCameraTransform.localEulerAngles = new Vector3(CamerarotationTotal.Value, 0, 0);
    }
    #endregion

    #region move_function
    private void FixedUpdate()
    {
        if(IsClient)
        {
            if(IsLocalPlayer)
            {
                //如果本地客户端接受到输入信息,则向服务器发送位置更新请求
                GetMoveInput();
                Vector2 temp = new Vector2(move_x, move_z);
                RefeshNextPositionServerRpc(temp,moveSpeed);
            }
        }
        if(IsServer)
        {
            foreach (ulong uid in NetworkManager.Singleton.ConnectedClientsIds)
            {
                NetworkManager.Singleton.SpawnManager.GetPlayerNetworkObject(uid).GetComponent().Move();
            }
        }
    }


    private void GetMoveInput()
    {
        move_x = Input.GetAxis("Horizontal");
        move_z = Input.GetAxis("Vertical");
    }

    [ServerRpc]
    private void RefeshNextPositionServerRpc(Vector2 InputMessage,float speed) 
    {
        if ((speed == 0) || (InputMessage.x == 0 && InputMessage.y == 0)) return;

        velocity = new Vector3(InputMessage.x * speed, 0,InputMessage.y*speed);
        velocity = transform.TransformDirection(velocity);
        new_position = transform.position + velocity * Time.fixedDeltaTime;
    }

    private void Move()
    {
        transform.position = new_position;
    }
    #endregion
}

脚本的命名必须与类名一致,不然会在添加组件时找不到脚本,而且还不会报错,很难找到哪出错了

将角色控制器添加到角色;

Unity 使用NetCode for GameObject(NGO)实现联机_第17张图片

角色控制器上的速度大小等变量会在角色实例化后被设置,游戏运行后也可以去调节

将角色拖到Prefabs文件夹中,并将场景中的角色删除;

Unity 使用NetCode for GameObject(NGO)实现联机_第18张图片

场景中的物体删除后ctrl+z撤销,但是预制体删除后撤销不了,只能从电脑的回收站里恢复

 现在已经可以开始运行了,但是运行时会有一个弹窗;

Unity 使用NetCode for GameObject(NGO)实现联机_第19张图片

在左上角打开File找到Build Settings点击Add Open Sence即可;

Unity 使用NetCode for GameObject(NGO)实现联机_第20张图片

选中NetworkManager物体(单机左键),点击运行游戏,在右侧属性面板点击StartHost,在主机端就生成了一个玩家,并且可以控制其移动;

Unity 使用NetCode for GameObject(NGO)实现联机_第21张图片

Unity 使用NetCode for GameObject(NGO)实现联机_第22张图片

到这,我们就完成了一个玩家预制体的设置。

这时游戏还有不少问题,当游戏运行时有多个摄像机同时存在,还有缺少地面和参考物;


完善游戏:

为了方便调试,先建立一个ui界面;

在场景中新建一个幕布(Canvas)类型的物体,命名为UI,点击Sence画面里的2D,方便调整画面内容;

Unity 使用NetCode for GameObject(NGO)实现联机_第23张图片

为它新建一个Button类型的子物体,点击下方出现的两个import,导入相关资源 

Unity 使用NetCode for GameObject(NGO)实现联机_第24张图片

 复制出两个Button,总共三个Button,分别命名为HostButton,ServerButton,ClientButton,调整按钮的位置,修改按钮的文本信息;(按钮名字尽量相同,文本信息不影响)

Unity 使用NetCode for GameObject(NGO)实现联机_第25张图片

新建一个UI脚本:

UI.cs

using System.Collections;
using System.Collections.Generic;
using Unity;
using UnityEngine;
using UnityEngine.UI;
using Unity.Netcode;
public class UI : MonoBehaviour
{
    Canvas ui;
    
    public GameObject Sencecamera;

    Button Host;
    Button Server;
    Button Client;

    Button[] arrayButton;

    private void Awake()
    {
        ui = GameObject.FindFirstObjectByType();

        Sencecamera = GameObject.FindGameObjectWithTag("MainCamera");

        if (ui == null)
        {
            Debug.Log("Can't find canvas!!!");
        }

        if (Sencecamera == null)
        {
            Debug.Log("UI.cs:Can't find Sence Camera!!!");
        }

        arrayButton = ui.GetComponentsInChildren

ui脚本负责管理三个按钮,当点击完按钮后,会将按钮全部销毁.

并且如果选择启动主机端或者客户端,ui脚本会禁用掉自己世界里的场景相机,也就是MainCamera;

在场景中新建一个空物体命名为GamManager,将UI挂载到GameManager上;

Unity 使用NetCode for GameObject(NGO)实现联机_第26张图片

到这,我们就处理好了场景相机与角色相机的冲突问题;

接下来处理多个玩家加入游戏时,玩家间也也会出现多个相机冲突的问题;

在Character文件夹内新建一个脚本名为CharacterSetup,将脚本添加到角色上

CharacterSetup.cs



using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;

public class CharacterSetup : NetworkBehaviour
{

    //发生冲突的组件
    //数组定义为Behaviour和Compent类型都能添加组件,Compent继承自Unity.Engine类,并无enabled成员,无法禁用组件;
    GameObject Sencecamera;

    private void Awake()
    {
        Sencecamera = GameObject.Find("GameManager").GetComponent().Sencecamera;

        if (Sencecamera==null)
        {
            Debug.Log("Setup.cs:Can't find SenceCamera!");
        }
    }

    private void Start()
    {
        if (!IsOwner)
        {
            if (transform.Find("CharacterCamera") != null)
            {
                transform.Find("CharacterCamera").GetComponent().gameObject.SetActive(false);//GetCompent获取到组件类型,但是组件类型没有enabled成员,所以无法禁用组件
                transform.Find("CharacterCamera").GetComponent().gameObject.SetActive(false);
            }
            else Debug.Log("NULL CharacterCamera");
        }
    }



    //当玩家物体被卸载出场景或被禁用时调用OnDisable()函数,当玩家物体被销毁时可调用OnDestroy()函数
    private void OnDisable()
    {
        if (Sencecamera != null)
        {
            Sencecamera.gameObject.SetActive(true);
        }
    }
}

最后将场景布置一下就实现了一个简单的联机demo

Unity 使用NetCode for GameObject(NGO)实现联机_第27张图片

可以将环境灯光和场景添加到一个名为Environment的空物体上,方便管理

在左上角File找到Build Settings,在Player Settings里将创建的项目改为窗口化,然后创建项目

Unity 使用NetCode for GameObject(NGO)实现联机_第28张图片

成功实现联机

Unity 使用NetCode for GameObject(NGO)实现联机_第29张图片


四、联机两种实现方式

unity实现联机有两种: 1、服务器权威  2、客户端权威

1.服务器权威

服务器作为数据计算的中心,并且执行相关逻辑。然后将信息同步给所有客户端,提供了一个相对公平的游戏环境;

unity默认使用服务器权威,对应使用NetworkTransform;

  • 优点:防止作弊,对于玩家来说作弊难度大
  • 缺点;延迟高,用户体验感不好

服务器权威的角色控制器:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;

public class Character_serverauthoritative_move : NetworkBehaviour
{
    #region move_variable
    [SerializeField]
    private float moveSpeed;
    private float move_x;
    private float move_z;
    private Vector3 velocity;
    private Vector3 new_position = Vector3.zero;
    private Vector2 temp1=Vector2.zero;
    #endregion

    #region rotate_variable
    [SerializeField]
    private float rotaeSpeed;
    [SerializeField]
    private float maxRotation;
    private float rotae_x;
    private float rotate_y;
    private Vector4 temp2 = Vector4.zero;

    private NetworkVariable CharacterNextRotation = new NetworkVariable();//角色绕y轴旋转量,不使用网络同步变量也可以
    private NetworkVariable CamerarotationTotal = new NetworkVariable();//相机绕x轴旋转总量
    Transform CharacterCameraTransform;
    #endregion

    private void Awake()
    {
        CharacterCameraTransform = transform.Find("CharacterCamera");//获取为角色物体子对象的相机(命名一定要与角色相机的名称相同,否者找不到,返回相机的transform组件)

        if (CharacterCameraTransform == null)
        {
            Debug.Log("Can't find Character'Camera,please check!!!");
        }
    }

    private void Start()
    {
        #region move_variable_initialization
        moveSpeed = 5f;
        move_x = 0;
        move_z = 0;
        #endregion

        #region rotate_variable_initialization
        rotaeSpeed = 8f;
        maxRotation = 80f;
        rotae_x = 0;
        rotate_y = 0;
        #endregion

        Cursor.lockState = CursorLockMode.Locked;
    }

    private void FixedUpdate()
    {
        if (IsLocalPlayer)
        {
            //如果本地客户端接受到输入信息,则向服务器发送位置更新请求
            GetMoveInput();
            temp1 = new Vector2(move_x, move_z);
            RefeshNextPositionServerRpc(temp1, moveSpeed);
            GetRotateInput();
            temp2 = new Vector4(rotae_x, rotate_y, rotaeSpeed, maxRotation);
            RefeshAnglesServerRpc(temp2);
            //Rotate(); 这一行代码加不加都不影响同步,因为服务器端已经操作过了,NetworkTransform组件会将位置信息进行同步到客户端.(如果不用服务器权威的话,使用客户端权威.那么就要将NetworkTransform改为ClientNetworkTransform,然后将这行代码加上,将服务器端的位置更新操作删除掉,虽然也不影响同步,但是有点多余)
        }
        if (IsServer)
        {
            //遍历所有客户端,在服务器端将服务器本地的角色的位置更新,然后Network Transform组件会将客户端的物体与服务器端的进行同步(所以要进行同步的话,NetworkTransform组件是必不可少的)
            foreach (ulong uid in NetworkManager.Singleton.ConnectedClientsIds)
            {
                NetworkManager.Singleton.SpawnManager.GetPlayerNetworkObject(uid).GetComponent().Move();
                NetworkManager.Singleton.SpawnManager.GetPlayerNetworkObject(uid).GetComponent().Rotate();
            }
        }
    }

    #region rotate_function
    private void GetRotateInput()
    {
        rotae_x = Input.GetAxis("Mouse Y");
        rotate_y = Input.GetAxis("Mouse X");
    }

    [ServerRpc]
    private void RefeshAnglesServerRpc(Vector4 temp)
    {
        if ((temp.z == 0) || (temp.x == 0 && temp.y == 0)) return;

        if (temp.y != 0)
        {
            CharacterNextRotation.Value += temp.y * temp.z;
        }
        if (temp.x != 0)
        {
            CamerarotationTotal.Value -= temp.x * temp.z;
            CamerarotationTotal.Value = Mathf.Clamp(CamerarotationTotal.Value, -temp.w, temp.w);
        }
    }

    private void Rotate()
    {
        transform.localEulerAngles = new Vector3(0, CharacterNextRotation.Value, 0);
        CharacterCameraTransform.localEulerAngles = new Vector3(CamerarotationTotal.Value, 0, 0);
    }
    #endregion

    #region move_function
    private void GetMoveInput()
    {
        //GetAxis()的返回值是小数,GetAxisRaw()的返回值是(-1,0,1)这三个值,所以用GetAxis()处理移动会更加平滑;
        move_x = Input.GetAxis("Horizontal");
        move_z = Input.GetAxis("Vertical");
    }

    [ServerRpc]
    private void RefeshNextPositionServerRpc(Vector2 InputMessage, float speed) //将本地的输入和速度大小参数传到服务器端,在服务器端计算下一帧的位置
    {
        if ((speed == 0) || (InputMessage.x == 0 && InputMessage.y == 0)) return;//减少不必要的计算

        velocity = new Vector3(InputMessage.x * speed, 0, InputMessage.y * speed);
        velocity = transform.TransformDirection(velocity);
        new_position = transform.position + velocity * Time.fixedDeltaTime;
    }

    private void Move()
    {
        transform.position = new_position;
    }
    #endregion
}

2.客户端权威

客户端权威是在本地进行数据的计算并且完成本地玩家的移动,然后本地将同步信息发送给服务器端,服务器端根据发送过来的消息将服务器自身的这个玩家进行信息同步,然后服务器端将信息发送给其他客户端,让其他客户端去同步这个玩家的信息;

 使用客户端权威,对应使用ClientNetworkTransform

  • 优点:用户体验感好
  • 缺点:容易作弊

unity里没有ClientNetworkTransfoem组件, 需要自己添加;

ClientNetworkTransform组件实现方法:

第一种:(推荐)

新建一个脚本ClientNetworkTransform.cs

using Unity.Netcode.Components;
using UnityEngine;

namespace Unity.Multiplayer.Samples.Utilities.ClientAuthority
{
    /// 
    /// Used for syncing a transform with client side changes. This includes host. Pure server as owner isn't supported by this. Please use NetworkTransform
    /// for transforms that'll always be owned by the server.
    /// 
    [DisallowMultipleComponent]
    public class ClientNetworkTransform : NetworkTransform
    {
        /// 
        /// Used to determine who can write to this transform. Owner client only.
        /// This imposes state to the server. This is putting trust on your clients. Make sure no security-sensitive features use this transform.
        /// 
        protected override bool OnIsServerAuthoritative()
        {
            return false;
        }
    }
}

第二种:

在Package Manager里选择Add packages from git URL

Unity 使用NetCode for GameObject(NGO)实现联机_第30张图片

添加链接

https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop.git?path=/Packages/com.unity.multiplayer.samples.coop#main

Unity 使用NetCode for GameObject(NGO)实现联机_第31张图片

客户端权威角色控制器:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;

//客户端权威,前提需要将NetworkTransform转换为ClientNetworkTransform
//优点:延迟低,用户体验感好
//缺点;容易作弊
public class Character_clientauthoritative_move : NetworkBehaviour
{
    #region move_variable
    [SerializeField]
    private float moveSpeed;

    private float move_x;
    private float move_z;
    private Vector3 velocity;

    #endregion

    #region roate_variable
    [SerializeField]
    private float rotaeSpeed;

    [SerializeField]
    private float maxRotation;

    private float rotae_x;
    private float rotate_y;
    private float rotationTotal;//旋转总量

    Transform CharacterCameraTransform;
    #endregion

    private void Awake()
    {
        CharacterCameraTransform = transform.Find("CharacterCamera");//获取为角色物体子对象的相机(命名一定要与角色相机的名称相同,否者找不到,返回相机的transform组件)

        if (CharacterCameraTransform == null)
        {
            Debug.Log("Can't find Character'Camera,please check!!!");
        }
    }

    private void Start()
    {
        #region move_variable_initialization
        moveSpeed = 5f;
        move_x = 0;
        move_z = 0;
        #endregion

        #region rotae_variable_initialization
        rotaeSpeed = 8f;
        rotae_x = 0;
        rotate_y = 0;
        rotationTotal = CharacterCameraTransform.localEulerAngles.x;
        maxRotation = 80f;
        #endregion

        //锁定鼠标
        Cursor.lockState = CursorLockMode.Locked;

    }

    private void Update()
    {
        ViewsChange();

    }

    private void FixedUpdate()
    {
        Move();
    }

    #region Move_functions
    private void Move()
    {
        //GetAxis()的返回值是小数,GetAxisRaw()的返回值是(-1,0,1)这三个值,所以用GetAxis()处理移动会更加平滑;
        move_x = Input.GetAxis("Horizontal");
        move_z = Input.GetAxis("Vertical");

        if ((moveSpeed != 0) && (!((move_x == 0 && move_z == 0))))//减少不必要的计算
        {
            velocity = new Vector3(move_x * moveSpeed, 0, move_z * moveSpeed);
            velocity = transform.TransformDirection(velocity);
            transform.position += velocity * Time.fixedDeltaTime;
        }
    }
    #endregion

    #region ViewChange_functions
    private void ViewsChange()
    {
        rotae_x = Input.GetAxis("Mouse Y");//Mouse Y是鼠标垂直轴移动
        rotate_y = Input.GetAxis("Mouse X");//Mouse X是鼠标水平移动

        if (rotaeSpeed == 0 || (rotae_x == 0 && rotate_y == 0)) return;

        if (rotate_y != 0)
        {
            transform.localEulerAngles = new Vector3(0, transform.localEulerAngles.y + rotate_y * rotaeSpeed, 0);
        }

        if (rotae_x != 0)
        {
            rotationTotal -= rotae_x * rotaeSpeed;
            rotationTotal = Mathf.Clamp(rotationTotal, -maxRotation, maxRotation);
            CharacterCameraTransform.localEulerAngles = new Vector3(rotationTotal, 0, 0);
        }
    }
    #endregion
}

这里运行程序,比较一下两种权威实现角色控制的不同,在本地窗口发现服务器端权威的角色控制

明显有延迟,而客户端权威的角色控制就很丝滑;


五、对于联机的理解

一个联机游戏的文件(就是玩游戏下载的软件),在服务器端,和各个客户端都有一份。

服务器作为游戏的核心,负责进行通信、数据的计算和信息的同步,所以服务器端的游戏启动,客户端才能加入游戏。

当客户端启动时,服务器端会为这个客户端生成一个Player Prefab字段的物体;

客户端在加入网络的这一帧,客户端会将服务器端的信息同步过来,也就是将服务器的游戏状态复刻过来;

这个过程是服务器将需要同步的数据打包,会发送给当前客户端;

同时服务器也会发送给其他所有客户端,因为对于其他客户端来说,游戏里新增了一个玩家,服务器游戏状态发生改变,客户端需要进行同步,那么就要让其他客户端的本地游戏也生成这个玩家;

这个数据包不会很大,因为该有的游戏资源客户端都有并且网络同步是周期性进行的。

如果一个客户端的玩家需要移动一个物体,那么这个客户端会发送信息给服务器端,服务器端的玩家会先完成移动;

然后服务器将玩家移动的信息打包成数据包发送给所有的客户端,所有的客户端都会根据数据包完成这个玩家的移动(每个客户端都有这个需要移动的玩家)。

网络同步是逐帧完成,或者说是周期性完成,以此保证所有端口的同步性,借此达到视觉上所有玩家都在同一个游戏世界的目的;

参考博客:

unity帧同步理解

unity IsOwner的概念

你可能感兴趣的:(Unity多人联机游戏开发日志,unity,游戏,个人开发,NGO,unity多人联机)