同一个局域网内联机

联机如何实现?

服务器判断:

玩家 a 移动,在其他玩家眼里也会动。一般通过一个中心服务器 Server 实现,首先玩家 a 的客户端 clientA 向 Server
发送控制移动的输入,Server 判断玩家 a 能否移动,Server 判断完毕后,将结果发送给 ClientA 和 ClientB 等其他
客户端。客户端接收到消息后再渲染出动画。问题就是有网络延迟。

unity2-1.jpg

客户端判断:

玩家 a 移动,在自己的客户端中判断是否能移动,然后将移动的消息传给 Server,但是 Server 不需要传回 ClinetA。
然后 Server 更新自己的信息,再同步发送到 ClientB 等其他客户端。问题就是客户端可以发送 Server 虚假的信息,
也就是外挂。

unity2-2.jpg

同步

多个客户端描述的同一个游戏场景,再 clinetA 中玩家 a,b,c 的坐标和 ClinetB 中 a,b,c 的坐标应该是一致的,
由于有网络延迟的存在,不可能完全一致。

例如由于网络延迟的原因会造成玩家不同的运动轨迹

unity2-3.jpg

权衡利弊后根据某个玩家的状态去同步其他玩家

实现

在 unity 中实现了很多网络通信的包,除了 Server 和 Clinet 外,还有一个 host。host 就是主机,即 Server 和 Client 合二为一

安装网络资源包

com.unity.netcode.gameobjects(需要更新到 Unity 2021.3 及以后版本才能看到)

  • 工具栏:Windows->Package Manager
  • 左上角改为: Unity Registry,找到对应的包,右下角 install(已经安装则显示 Remove)

unity2-4.png

配置网络组件

创建空 Object,命名为 NetworkManager 管理所有需要联机同步的物体,添加组件(add component) NetwortkManager

此时可能 NetworkManager 会有两个警告:安装包 Multiplaers Tools(按照上文方法);选择 NetworkTrnasport 后即可消除

unity2-18.png

添加需要同步的元素 Player

在 Assets 里创建一个文件夹 Prefabs,将 Player 拖到这个文件夹里,此时可以删除掉左侧的 Player

unity2-5.png

如果在 Clinet 中摧毁了一棵树,ClientB 怎么知道摧毁的是树而不是车?所以所有需要网络同步的物体需要有一个唯一 id

给 Player 添加一个 NetworkObject 组件,就会有一个全局哈希值作为唯一编号

unity2-6.png

添加 Player 到 NetworkPrefabs,在 Default Network Prefabs

unity2-7.png

在 Player 组件中再 add component 一个 Network Transform,用于选择需要同步的信息

根据提示,在 Player 组件中再 add component 一个 Network Rigidbody

此时 unity 可以自动帮我们同步 Player

unity2-8.png

除了同步玩家以外,还需要同步 Camera,双击Player,在Camera里同样也添加一个 Network Transform,用于选择需要同步的信息。
另外 Camera 的父组件 Player 已经有一个唯一编号,所以 Camera 不需要唯一编号

unity2-9.png

实现开局创建自动创建角色

点击 NetworkManager,将 Player 拖到 Player Perfab 上

unity2-10.png

此时点击运行游戏,会发现黑屏,原因是因为 Camera 是在 Player 上,开局并没有创建角色就没有 Camera

unity2-11.png

添加一个 Camera 用作展示初始界面,调整角度位置

unity2-12.png

开始运行游戏,点击左侧 Don’tDestroyOnload,点击 NetworkManager,右侧会出现三个选项:
startHost、StartServer 和 StartClinet

unity2-13.png

点击 starthost 发现自动创建了一个 Player,但是位置是初始位置,可以把Environment的纵坐标往下移动2个单位

unity2-19.png

unity2-14.png

添加 ui,把 startHost、StartServer 和 StartClinet 作为按钮放入到游戏里。右键新建一个 ui/canvas.
重命名为 menuUI。

unity2-15.png

在 canvas 里添加按钮,右键 MenuUI 新建 Button,如果有提示需要 import 就按照提示 import

unity2-16.png

此时运行游戏

unity2-17.png

写按钮的逻辑:

给 NetworkManager 添加一个脚本 NetworkManagerUI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode; // 引入 Unity Netcode 命名空间,用于网络功能
using UnityEngine;
using UnityEngine.UI;

public class NetworkManagerUI : MonoBehaviour
{
// 序列化字段,用于在 Unity 编辑器中绑定 UI 按钮
[SerializeField]
private Button hostBtn; // 用于启动主机的按钮
[SerializeField]
private Button serverBtn; // 用于启动服务器的按钮
[SerializeField]
private Button clientBtn; // 用于启动客户端的按钮

// Start 方法在游戏对象初始化时调用
void Start()
{
// 为 "Host" 按钮添加点击事件监听器
hostBtn.onClick.AddListener(() =>
{
// 调用 NetworkManager 的单例方法 StartHost,启动主机
// 主机既是服务器又是客户端
NetworkManager.Singleton.StartHost();
});

// 为 "Server" 按钮添加点击事件监听器
serverBtn.onClick.AddListener(() =>
{
// 调用 NetworkManager 的单例方法 StartServer,启动纯服务器
// 服务器不参与游戏逻辑,仅处理网络同步
NetworkManager.Singleton.StartServer();
});

// 为 "Client" 按钮添加点击事件监听器
clientBtn.onClick.AddListener(() =>
{
// 调用 NetworkManager 的单例方法 StartClient,启动客户端
// 客户端连接到服务器并参与游戏
NetworkManager.Singleton.StartClient();
});
}
}

此时点击相应的 button 就有相应的操作

禁用非本地玩家的组件

此时直接运行 host 和 client,会发现两个角色会同时移动,并且 unity 中显示有多个 Audio 被使用的警告
原因是在一个 Player 中,没有禁用其他 Player 的输入控制和 Camera;并且开始菜单的 Camera 和本地 Player 的 Cameara 也会有冲突
在 Player 中添加一段脚本 PlayerSetup 解决此问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode; // 引入 Unity Netcode 命名空间,用于网络功能
using UnityEngine;

// PlayerSetup 类用于设置玩家对象的初始状态
// 继承自 NetworkBehaviour,表示这是一个网络行为脚本
public class PlayerSetup : NetworkBehaviour // 注意修改本行
{
// 序列化字段,用于在 Unity 编辑器中绑定需要禁用的组件
[SerializeField]
private Behaviour[] componentsToDisable; // 需要禁用的组件数组

// 场景主摄像机
private Camera sceneCamera;

// Start 方法在游戏对象初始化时调用
void Start()
{
// 判断当前玩家对象是否为本机玩家
if (!IsLocalPlayer) // 如果不是本机玩家
{
// 遍历需要禁用的组件数组
for (int i = 0; i < componentsToDisable.Length; i++)
{
// 禁用组件
componentsToDisable[i].enabled = false;
}
}
else // 如果是本机玩家
{
// 获取场景主摄像机
sceneCamera = Camera.main;
if (sceneCamera != null)
{
// 关闭场景主摄像机
sceneCamera.gameObject.SetActive(false);
}
}
}

// OnDisable 方法在游戏对象被禁用或销毁时调用
private void OnDisable()
{
// 如果场景主摄像机存在
if (sceneCamera != null)
{
// 重新启用场景主摄像机
sceneCamera.gameObject.SetActive(true);
}
}
}

将需要被禁用的组件拖到列表中:

unity2-20.png

注意代码sceneCamera = Camera.main;需要把SceneCamera的tag改为main
unity2-21.png

此时开启两个窗口运行游戏,发现一个能移动,一个不行,这是因为unity默认是以服务器判断方式。Unity默认不信任客户端,没有授权,需要自己写一段脚本打开授权

unity2-22.png

在Assets/Scripts/Network/里新建一个脚本ClientNetworkTransform.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 引入必要的命名空间
using Unity.Netcode.Components;
using UnityEngine;

// 定义一个命名空间,用于组织代码
namespace Unity.Multiplayer.Sample.Utilities.ClientAuthority
{
// [DisallowMultipleComponent] 是一个特性,表示这个脚本不允许在同一个GameObject上添加多个实例
[DisallowMultipleComponent]
// 继承自 NetworkTransform,这是一个用于同步物体位置、旋转和缩放的网络组件
public class ClientNetworkTransform : NetworkTransform
{
// 重写 NetworkTransform 中的 OnIsServerAuthoritative 方法
protected override bool OnIsServerAuthoritative()
{
// 返回 false,表示这个 NetworkTransform 不由服务器(Server)进行权威控制
// 而是由客户端(Client)进行控制,即客户端拥有对该物体的位置、旋转和缩放的最终决定权
return false;
}
}
}

代码含义:重载NetWorkTransform中的授权函数
所以,要将用到NetWorkTransform的地方改为ClientNetWorkTransform

Player:
删除 Network RigidBodyNetwork Trnasform
添加 ClientNetworkTransformNetwork RigidBody

unity2-23.png

unity2-24.png

此时就实现了本地联机并且多个Player都可以移动