联机
思维导图
复习一下移动原理
以图中为例,我们按下W,并在两帧后松开。
- 当我们按下W后,下一帧的
PlayerInput
中Update()
函数检测到W被按下,会计算出角色的移动向量,并传递给PlayerController
,PlayerController
中的FixedUpdate()
函数会改变角色坐标,使其运动。 FixedUpdate()
每隔0.02秒执行一次,只要角色速度不为0,角色就会一直移动。- 再下一帧,仍检测到W被按下,角色保持运动
- 再下一帧,检测到W松开,因此更新
PlayerController
中的速度为0,角色在FixedUpdate()
下一次执行时停止运动
角色旋转同理
联机结构
联机是不同的玩家进入同一个对局,组队或者敌对,可以互相看到对方角色的移动并进行攻击
联机主要有两种模式
Server模式
Server模式指玩家依靠一个服务器来进行联机。假设有三名玩家A, B, C, 大家进入同一个对局。此时对局里有三名玩家,且A, B, C三名玩家看到的对局应该是一模一样的(理想情况)
此时Server的作用是
- 维护对局,Server里也会有一个对局,跟玩家中的一模一样
- 当一个玩家进行操作时,会先将该操作传递给Server,Server里判断该操作会产生什么结果。当Server计算好之后,将产生的结果广播给所有玩家,这样所有玩家都能看到操作后的结果
- 以图为例,A玩家执行了一个移动操作,A的电脑将该操作传递给了Server,Server计算出该玩家移动后的位置以及会触发的一些结果,并将A的新位置和触发结果广播给了所有玩家。所有玩家都看到了A移动并且触发了一些结果
问题:网络通信有延迟,Server中的对局与玩家电脑上的对局不会一致,导致操作会不跟手,射击时也可能存在瞄准后依然打不中敌人的情况
考虑到游戏体验,射击游戏中Server模式并不常用,而Client模式比较常用
Client模式
Client模式跟Server模式的区别在于,当一个玩家进行操作后,该操作产生的结果现在A中显示出来,之后将该结果传递给Server,Server负责将结果广播给其他玩家。
这种处理的好处在于,玩家操作起来的延迟非常低。
问题
- 比Server更难避免外挂,一些AI外挂几乎无法检测
同步问题
我们希望所有玩家以及服务器内的对局是一模一样的,但是由于网络延迟的存在,不同主机上的对局都会有一定的差别,因此涉及到同步的问题。
联机同步会出现轨迹问题。举例来说,比如A操控角色,一开始在 x0 , 之后移动到 x1 ,再移动到 x2 。由于网络延迟的存在,因此B玩家的主机接收到A移动至 X1 的信息会比较迟,而在B的对局中A正在向 x1 移动,此时A移向 x2 的信息传来,这时B中的A角色就不会先移动到 x1 位置再移动到 x2 位置了,而是直接向 x2 移动(或者以一个曲线方式平滑的靠近 x1 再到 x2)。C玩家同理。因此A角色的运动终点一样,但移动轨迹不一样。网络延迟越大,移动轨迹和操作的差别越大
这就引出射击游戏的一个问题:谁来判定是否击中敌人?如图,B尝试向A射击。在A的画面中,A已经完全躲进的掩体后,B无法击中他;但在B的画面中,A还没有完全躲进掩体中,且已经被瞄准射击,B应该击中了A。那么B是否击中了A?一般的FPS判断逻辑,B击中了A。认为B击中了A,主要是照顾到射击玩家的游戏体验,即玩家瞄准到了目标并射击,就应该击中目标。而对于A,其实A并不能保证自己完全躲进了掩体而不会被击中,所以如此的处理也不会过分侵害A的游戏体验
事实上,上述的判断取舍并不通用。当网络延迟很大的时候,A仍然能很明显的感知到自己明明完全躲进了掩体后,却依然被击中;或者A在跟B对枪的时候,明明已经打中B很多枪,但由于B的延迟比较低,B打死A的消息先传给服务器,因此结果仍然是A死亡。为了解决这种问题,游戏制作方一般会采取延迟补偿的措施来使游戏体验更好。
Host
Host = 主机 + Server,一个玩家的主机既做主机又做Server
实现联机
下载资源包
Unity实现网络通信需要加入Network Manager的网络管理包,帮助我们同步每个窗口里的物体。
- com.unity.netcode.gameobjects(需要更新到Unity 2021.3及以后版本才能看到)
点击Unity左上角的
Window
, 点击里面的 Package Manager
见到图中页面,把左上角的
Packages
改成 Unity Registry
, 然后往下翻,找到 Netcode for GameObjects
, 点击 Install
(也可以直接在右上角搜索后安装)
添加组件
创建一个空物体,命名为
NetworkManager
,并添加组件 Network Manager
。这里提示要装一个包 Multiplayer Tools
, 方法同上。
下载完之后,点击
NetworkManager
物体,右边还会出现一个警告。依据这个警告,我们选择 UnityTransport
即可
同步对局
对局中不动的东西,比如地图、障碍物等,不需要同步,减少网络带宽的占用。游戏角色等会运动的物体需要同步。
玩家角色一开始并不会出现在对局中,只有加入对局才会出现。为了复用我们上次创建好的玩家模型,我们可以将玩家物体存入 Assets
中,然后删除地图里的玩家。
为了正确同步对局内物体,每一个需要被同步的物体都要有一个唯一的编号。要加上这个编号,需要添加
Network Object组件
(这里给 Player
添加)。组件里的 GlobalObjectHash
就是类似编号的东西。
所有需要同步的物体都要在
NetworkManager
中统一管理起来。点击 NetworkManager
,找到右边 Network Manager
组件中的 Network Prefabs
,点击下面的加号,然后把需要同步的物体(这里是 Player
)拖进来即可。
除了将要同步的物体放进
NetworkManager
中管理之外,要需要给要同步的物体添加组件 Network Transform
来指示哪些信息需要同步。这里同步 Player
,需要同步位置信息和围绕y轴旋转的信息(注意围绕x轴旋转的是摄像头,不是角色本身),其他信息不用同步。不用同步的信息建议去掉,以降低网络带宽的占用。注意下面有个警告说 Player
里有刚体,推荐添加一个刚体的同步,按照警告添加即可。
同步摄像头视角。同样,点击
Player
物体,给摄像头添加一个 Network Transform
组件,同步的信息只用x轴旋转的信息即可。摄像头不用 Network Object
,因为父组件 Player
有添加,可以通过父子关系索引到。
回到上一个界面
我们希望开局通过菜单进入游戏后就创建一个玩家角色。在
NetworkManager
物体中,右边有一个 Player Prefab
, 将开局想创建的物体拖进去即可,这里拖进去的是 Player
游戏中的摄像头在角色上,但开始运行游戏时游戏中并没有角色,因此会出现黑屏的现象。为了解决这个问题,可以创建一个开局的摄像头。如下图所示,摄像机角度自行调整。
点击运行后,在右边的 DontDestroyOnLoad
里有 NetworkManager
,点击 NetworkManager
,右边往下翻可以看到三个按钮 Start Host
Start Server
和 Start Client
。我们点击 Start Host
可以进入Host模式。如果此时角色看不到地图,有可能是角色嵌入地图中了,这时可以把地图的y轴坐标调小,直到角色在地图上面。
添加UI
在 NetworkManager
里开启游戏较为复杂,这里做一个游戏UI。先在右边新建一个物体 canvas
。canvas
是一个2D物体,可以点击右上角的 2D
进入2D视图编辑它。双击 canvas
使它来到画面中心,然后右键 canvas
在 canvas
里创建一个 Button
创建好之后会有提示需要import一些组件,按照提示做即可
添加好之后修改 Button
的名字。在 Button
里有一个 Text
,里面的编辑框里可以修改 Button
在界面上的文字。复制两份 Button
,三个 Button
分别负责点击 Start Host
Start Server
和 Start Client
。
给UI添加逻辑
回到3D,给 NetworkManager
添加脚本 NetworkManagerUI.cs
, 打开脚本。
先通过 SerializeField
将三个按钮与脚本绑定。在 Start()
函数中为三个按钮添加监听函数和点击后触发的功能。(监听函数只用加一次,放在 Start()
中)
hostBtn.onClick.AddListener(() =>
{
NetworkManager.Singleton.StartHost();
});
出现的问题
此时游戏有两个大问题
- 当我们开启对局,对局里有两个玩家时,一个玩家可以同时控制两个角色。这是因为游戏在接收用户输入控制角色时接收的是全局输入,也就是一个玩家移动鼠标或按下按键,这个信息会被所有角色接收并处理
- 游戏中存在两个摄像头,摄像头中都有
Audio Listener
, 会出现冲突。
为了解决以上问题,需要进行修改:禁用除了本地玩家以外的所有玩家角色的 PlayerInput
PlayerController
和camera&audio listener。
方法:
- 在
Player
中添加脚本PlayerSetup.cs
- 因为要判断是否是本地玩家,需要使用网络包的一些API,因此将
MonoBehaviour
修改为NetworkBehaviour
- 此时修改如果没有补全,可以先手动添加上
using Unity.Netcode
,然后在修改
- 此时修改如果没有补全,可以先手动添加上
- 使用
SerializeField
来添加那些要被禁用的组件- 注意这里添加的组件是
Player
自己的 - 添加
camera
里的Audio Listener
时常规操作添加不到,可以先在右上角将Player
的Inspector
锁住然后右键
Inspector
,在Add Tab
中选择Inspector
将新添加的
Inspector
拖到界面里,点击camera
,这时新添加的Inspector
就是camera
的属性了。这时可以很方便的将
Audio Listener
拖进禁用组件中了 - 删掉刚创建的
Inspector
,可以右键Inspector
后选择Close Tab
(记得把锁住的
Inspector
解锁)
- 注意这里添加的组件是
- 禁用操作只用执行一次,在初始时执行,因此在
Start()
中编写禁用的代码 - 对于本地玩家,需要禁用全局的相机。全局相机不太能用
SerializeField
方式获取,可以通过tag来获取。将SceneCamera
的tag设为MainCamera
然后在
Start()
函数中获取并禁用。当玩家消失的时候还需要将全局相机恢复,在OnDisable()
函数中实现
此时联机后会发现,客户端无法移动角色,只用Host能移动,这是因为Unity默认对客户端是不信任的,选择的是Server模式而不是Client模式。切换成Client模式需要用代码操作。
创建 ClientNetworkTransform.cs
脚本,添加以下代码
using Unity.Netcode.Components;
using UnityEngine;
namespace Unity.Multiplayer.Samples.Utilities.ClientAuthority
{
[DisallowMultipleComponent]
public class ClientNetworkTransform : NetworkTransform
{
protected override bool OnIsServerAuthoritative()
{
return false;
}
}
}
这段代码主要做的是将 NetworkTransform
重载为了 ClientNetworkTransform
, 并且取消了只能由Server端授权的限制。因此上文添加的所有 NetworkTransform
类都要修改为 ClientNetworkTransform
类
- 修改
Player
:由于Player
里有一个Network Rigidbody
依赖于Network Transform
,因此先删Network Rigidbody
再删Network Transform
,然后添加
Client Network Transform
和Network Rigidbody
。 - 修改
camera
:删除Network Transform
, 添加Client Network Transform
- 记得修改后选择一下要同步的信息,同上。
阶段成品
课上代码
PlayerSetup.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;
public class PlayerSetup : NetworkBehaviour
{
[SerializeField]
private Behaviour[] componentsToDisable;
[SerializeField]
private Camera sceneCamera;
// Start is called before the first frame update
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);
}
}
}
private void OnDisable()
{
if (sceneCamera != null)
{
sceneCamera.gameObject.SetActive(true);
}
}
}
NetworkManagerUI.cs
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.UI;
public class NetworkManagerUI : MonoBehaviour
{
// 将三个button与脚本绑定
[SerializeField]
private Button hostBtn;
[SerializeField]
private Button serverBtn;
[SerializeField]
private Button clientBtn;
// Start is called before the first frame update
void Start()
{
// 监听函数,当点击时触发
hostBtn.onClick.AddListener(() =>
{
NetworkManager.Singleton.StartHost();
});
serverBtn.onClick.AddListener(() =>
{
NetworkManager.Singleton.StartServer();
});
clientBtn.onClick.AddListener(() =>
{
NetworkManager.Singleton.StartClient();
});
}
}
著作权信息
- 源视频作者:yxc
- 文章作者: jaylenwanghitsz
- 链接:https://www.acwing.com/file_system/file/content/whole/index/content/8111736/
- 来源:AcWing
- 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。