当前位置:   article > 正文

【Unity2022】Unity多人游戏开发教程-Netcode for GameObjects-使用命令行启动多人游戏_unity netcode

unity netcode

官方文档

首先亮出文档,可以直接去看官方文档。
本文章大部分内容来源于官方文档,另一部分为笔者讲解的教程。
如果英语不好,或看不懂文档的人,可以阅读本文章。
官方文档
官方文档的中文翻译
中文翻译

前言

教程的开发环境

本教程使用的开发环境如下:

  • Windows10
  • Unity 2022.3.0f1c1
  • Netcode for GameObjects 1.5.2

预备知识

本教程需要具备以下预备知识:

  • C#编程语言
  • Unity基础知识

教程内也会讲解一些C#编程的知识,包括部分Unity的知识,不过并不会全面讲解。

1 简介

1.1 Netcode for GameObjects

以前的多人游戏开发,有些项目或者教程可能使用的是UNet,当然也有别的方案,但是UNet已经被Unity官方弃用了,也就是说UNet是一个过时的方案,目前正在开发一种新的多人游戏和网络解决方案,名字叫做Netcode for GameObjects。
本篇文章的多人游戏解决方案采用的就是Netcode for GameObjects。
Netcode for GameObjects(简称Netcode或NGO)是一个为Unity构建的高级网络库,可用于抽象化网络逻辑,抽象化网络逻辑是指将网络通信的复杂性和细节隐藏在一个高级接口之后,使开发者能够更专注于构建游戏,而无需深入了解底层的网络协议和通信机制。
Netcode提供了简单的网络操作,让我们能够更方便的将GameObject和世界数据通过网络会话发送给多个玩家或接收,并在多个玩家之间同步数据。

1.2 NGO支持的Unity版本

使用Netcode,我们的Unity需要是2021.3或者更高的版本。并且脚本后端是Mono和IL2CPP。
Unity有两种脚本后端:Mono和IL2CPP(Intermediate Language To C++),它们使用不同的编译技术,Mono使用即时(JIT)编译,在运行时按需编译代码。而IL2CPP使用提前(AOT)编译,在运行应用程序之前对整个应用程序进行编译。
Mono是一种开源的跨平台的.NET实现,允许开发者在不同的操作系统上运行.NET应用程序。它提供了一系列工具和库,使开发者能够使用C#等.NET编程语言来创建和运行应用程序。

1.3NGO支持的平台

NGO支持如下平台:
Windows、MacOS和Linux
iOS和Android
运行在Windows、Android和iOS操作系统上的XR平台
大多数封闭平台,如游戏主机。
WebGL(需要NGO 1.2.0+和UTP 2.0.0+)。注意:尽管NGO 1.2.0引入了WebGL支持,但NGO 1.2.0中存在影响WebGL兼容性的错误,因此建议使用NGO 1.3.0+。

2 开始旅程

2.1 安装NGO

首先我们需要新建一个项目,当然如果你也可以打开已有项目。
进入项目后打开Package Manager,在编辑器的菜单栏选择“Window > Package Manager”,即可打开Package Manager。然后点击左上角的加号“+”,选择“Add package by name…”。然后在包名称的输入框中输入“com.unity.netcode.gameobjects”,然后选择“Add”,这样就为你的项目导入了NGO。
请添加图片描述
请添加图片描述

2.2 运行项目

运行多人游戏,那就需要启动多个游戏实例,将多个游戏实例以不同端来启动,比如主机端或者客户端,启动方法也有很多,例如可以在程序中通过制作网络连接的UI界面选择启动端。也可以通过命令行启动,获取命令行参数来选择对应端。
这里先介绍第二种方法,也就是通过命令行来启动多端。

2.2.1 Unity基础

获取命令行参数

通过命令行启动unity程序的时候,我们可以获取其命令行参数。创建一个Unity项目,然后创建一个Text,用于一会显示我们获取到的参数,然后创建一个脚本,该脚本内容如下:

using UnityEngine;
using TMPro;

public class GetArgs: MonoBehaviour
{
    public TextMeshProUGUI text;
    // Start is called before the first frame update
    void Start()
    {
        var args = System.Environment.GetCommandLineArgs();
        for(int i=0; i < args.Length; i++)
        {
            text.text +=$"args[{i}]: "+args[i]+"\n";
        }
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

在代码中,我们通过System.Environment.GetCommandLineArgs()方法来获取命令行参数,该方法返回一个字符串数组。
现在我们保存代码和场景,然后构建程序,并通过命令行的方式执行。如图所示,直接通过指定路径的方式执行程序,Learn_2D是你程序的名字,前面的路径就是该程序所在的路径。然后,在执行程序的命令后面,我们跟上命令行参数,输入什么都可以,随便写点字符串。
在这里插入图片描述
运行命令后,程序就启动了,如下图所示,程序显示了我们获取到的命令行参数。注意,你输入的启动程序的命令,也是命令行参数的一个。而且是第一个命令行参数,也就是args[0]。
获取到的参数

判断当前是否在编辑器中运行

在Unity中,我们可以通过Application类的isEditor字段来判断当前运行的游戏是否是在编辑器中运行的,Application.isEditor是一个静态只读的布尔值,定义如下:

// 摘要:
//     Are we running inside the Unity editor? (Read Only)
public static bool isEditor => true;
  • 1
  • 2
  • 3

当我们在 Unity 编辑器中运行游戏时,Application.isEditor的值将为 true。
当我们在游戏的构建版本(即发布版本)中运行游戏时,Application.isEditor的值将为 false。

Application.isEditor在某些时候非常有用。通过它,可以根据我们在编辑器还是构建版本中,来执行不同的逻辑和功能。这对于在开发过程中进行调试、测试和实现编辑器专用功能有一定的帮助。

这里我们写一个测试代码如下:

if(Application.isEditor)
{
    text.text = "此时正在编辑器中运行";
}
else
{
    text.text = "此时正在发布版本中运行";
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

运行效果如下,当我们在编辑中运行时:
在这里插入图片描述
当我们把项目Build后再次运行,结果如下:
在这里插入图片描述

发布版本的Log日志输出

其实当我们将项目Build后,运行的程序,也会输出日志文件,没错就是你在代码中使用Debug.Log方法输出的日志文件,那么问题来了,我们的发布版本又没有Console窗口,怎么输出日志文件?其实,日志文件被存储在了一个名叫Player.log的文件中。

Player.log 文件是用来记录详细的日志信息的。无论是在编辑器中还是在发布版本中,Debug.Log 输出的内容都会被记录在 Player.log 文件中。

Player.log文件默认存放在如下路径中:

C:\Users\username\AppData\LocalLow\CompanyName\ProductName\Player.log
  • 1

具体来说,username 是指当前计算机上登录的用户名,而 CompanyName 和 ProductName 是指 Unity 项目中的公司名称和项目名称。如果你没有明确填写公司名称,那么这里的CompanyName就是“DefaultCompany”。

例如如下路径:

C:\Users\Zhi\AppData\LocalLow\DefaultCompany\Learn_2D\Player.log
  • 1

想必你已经知道日志文件默认在什么位置了,接下来我们尝试输出一些日志看看。
编写如下代码:

void Start()
{
    Debug.Log("此处为输出的日志");
}
  • 1
  • 2
  • 3
  • 4

代码中,我们简单的打印了一行日志。
接下来构建项目并运行,然后去前面说的路径中,找到Player.log文件,打开该文件,可以看到如下内容:
在这里插入图片描述
里面就有我们在程序中输出的日志。
我们刚刚说了,那是默认路径,也就是说,我们可以通过某种方法,改变其存放的位置。通过改变其存放位置,可以让我们更方便的打开这个日志文件,当然你也可以存放在默认路径,只要你觉得方便查看输出信息即可。
那么如何改变存放位置呢?我们可以通过-logfile命令,来改变其存放的位置并且改变日志文件的名字。
首先,打开命令提示符窗口,可以通过Win+R键,打开运行窗口。然后在运行窗口中输入cmd命令,回车,即可打开命令提示符窗口。
在进行下一步之前,请确保你已经构建好了一个Unity程序。
我们的logfile命令的格式如下:
-logfile 文件名
后面的文件名,就是你想要将Player.log文件改变的名字,注意要加文件后缀名。
比如:-logfile log-server.log
如此一来,将会吧Player.log文件更名为log-server.log文件,并在当前命令提示符所在的文件路径生成它。

现在,让我们实践一下。构建一个程序,然后通过命令提示符来运行他,为其传递命令行参数 -logfile weLog.txt
在这里插入图片描述
运行后,发现确实生成了,而且刚好是在我们命令提示符当前所在的目录下,也就是“C:\Users\28446”路径下。

那么我们如何改变当前命令提示符所在的目录,让其生成到指定的位置呢?
接下来我们需要了解一下命令提示符窗口的cd命令,cd(change directory)是用于在命令行中切换当前工作目录的命令。
默认情况下,打开cmd进入的是当前windows用户的文件夹下。
在这里插入图片描述
我们可以通过cd ..命令,来返回上一级目录,也就是父目录。
在这里插入图片描述
然后通过dir命令,我们可以看到当前文件夹下有哪些文件或文件夹。
在这里插入图片描述
如果我们想要进入某一个文件夹下,可以再次使用cd命令+文件夹的名字来进入,例如cd 28446
在这里插入图片描述
当然,我们也可以输入一大串的路径,然后直接通过cd命令进入。比如说:
在这里插入图片描述
但大家要注意,cd命令无法跨越盘符,也就是说,如果这一大串的路径是D盘的 ,那我们使用cd命令是无法直接跳转过去的,如图所示。
在这里插入图片描述
一般情况下,我们在计算机里分了很多的区,或者说是盘,比如说C盘或者是D、E、F盘等等。而我们的应用程序,一般是不会放在C盘的。那么,如果我们想要进入到其他的盘符,应该怎么做呢?
这时候,直接输入“盘符:”即可,例如d:
在这里插入图片描述
如图,我们进入了D盘。
这时候在输入那一大串的路径,直接跳转到想去的文件夹。
在这里插入图片描述
然后,我们就可以进入到游戏所在的路径,使用-logfile命令,来将我们的player.log文件生成到游戏可执行程序所在的目录下了,这样方便我们打开并观察日志。
在这里插入图片描述

2.2.2 C#基础

判断字符串前缀

我们刚刚学习了如何获取命令行参数,获取数据后,接下来我们就要对其进行处理了,在对命令行参数进行处理之前,我们先来学习一些C#的知识。
首先就是一个字符串处理函数,StartsWith(),这个函数我们在学习C#的时候都接触过,它的作用是用来判断字符串开头的字符,具体来讲,就是判断字符串的开头是否为指定的字符串,如果是,就返回True,如果不是就返回False。
这是一个实例方法,也就是说,我们需要在一个字符串的实例上使用这个方法。
具体来举个例子:

string str = "Hello, world!";
bool startsWithHello = str.StartsWith("Hello");
Console.WriteLine(startsWithHello);
  • 1
  • 2
  • 3

我们声明了一个字符串,里面是Hello,world,通过StartWIth方法, 判断其开头是否为"Hello"。
运行结果如图所示:
运行结果
但是需要注意一下的是,StartWith方法是区分大小写的,也就是说如果这里判断的值是hello时,结果就会是False。

string str = "Hello, world!";
bool startsWithHello = str.StartsWith("hello");
Console.WriteLine(startsWithHello);
  • 1
  • 2
  • 3

此时运行结果为False。

但是我们可以通过一个枚举值,来使其不区分大小写。将StringComparison.OrdinalIgnoreCase枚举值作为第二个参数,StartWith就不会区分大小写了。

string str = "Hello, world!";
bool startsWithHello = str.StartsWith("hello", StringComparison.OrdinalIgnoreCase);
Console.WriteLine(startsWithHello);
  • 1
  • 2
  • 3

此时运行结果为:True。

2.2.1.2 空值合并操作符

空值合并操作符是由两个问号组成的“??”。
expression1 ?? expression2
它的作用是用来检查问号左侧的表达式是否为null,如果为null,就返回右侧表达式的值,如果不为null,就返回左侧表达式的值。
举个例子:

string name = null;
string result = name ?? "无名";
Console.WriteLine(result);
  • 1
  • 2
  • 3

运行结果显示为:无名。
这里我们用了一个name变量,给其null值,然后通过空值合并操作符,来判断其是否为null,如果此时name不是null值,就会返回name ,把name的值,赋值给result,如果name为null,就会返回右侧表达式的值,也就是"无名"二字。
由于这里我们给name赋值为null,理所当然的就会返回右侧表达式的值了。

2.2.1.3 获取字典中的值

复习一下,在学习C#的时候,我们应该学习过TryGetValue方法,我们可以通过TryGetValue来从字典中获取值。这个方法是字典类的一个成员方法(比如Dictionary<TKey, TValue>)。这是一个可以安全的获取字典中的值的方法,因为它避免了在字典中查找键时引发的异常。
方法的定义如下:

public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value);
  • 1

该方法有两个参数,第一个参数是键。
第二个参数是值,当找到匹配的键时,会通过该参数将值返回出来。如果没找到对应的键值对,就会将值类型的默认值赋给这个变量,并返回false。
下面是一个示例程序:

 Dictionary<int, string> dict = new Dictionary<int, string>();
 dict.Add(1, "Unity");
 dict.Add(2, "UE5");

 string result;
 if (dict.TryGetValue(1, out result))
 {
     Console.WriteLine("找到键 '1' 的值: " + result);
 }
 else
 {
     Console.WriteLine("未找到键 '1'");
 }

 if (dict.TryGetValue(3, out result))
 {
     Console.WriteLine("找到键 '3' 的值: " + result);
 }
 else
 {
     Console.WriteLine("未找到键 '3'");
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

运行结果如下所示:
在这里插入图片描述
在代码中,我们使用TryGetValue()方法尝试获取键为1的值,由于字典中存在键1,方法返回true,并将对应的值"Unity"赋值给result变量。然后,我们再尝试获取键为3的值,由于字典中不存在键3,方法返回false,并将默认值(null)赋给result变量。

使用TryGetValue()方法时,可以避免查找字典中不存在的键时引发的异常,非常滴好用。

2.2.3 创建命令行测试助手

接下来,我们就要使用命令行来启动多端了,首先进入到你的项目,新建一个UI-Text-TextMeshPro。调整其位置为左上角,设置Anchor Presets为left top。创建这个UI的目的是为了显示当前实例是客户端还是服务器端、主机端。
在这里插入图片描述
最后效果如图所示:
在这里插入图片描述

然后在游戏的场景中新建一个空物体,命名为NetworkManager。我们点击该物体的Add Component,搜索NetworkManager,为其挂载该组件,后续我们会通过该组件来启动不同的端。该组件是整个NGO中最为重要的组件,包含了你项目中所有与网络代码相关的设置,可以说netcode的中心。

然后再右键单击NetworkManger,为其创建子物体,同样也是空物体,并将该物体命名为NetworkCommandLine
在这里插入图片描述
紧接着,我们来创建一个脚本,用于识别命令行的命令,并根据不同的命令来启动不同的端。
创建一个脚本,命名为NetworkCommandLine,然后在其中编写如下代码:

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

public class NetworkCommandLine : MonoBehaviour
{
   public TextMeshProUGUI text;
   private NetworkManager netManager;

   void Start()
   {
       netManager = GetComponentInParent<NetworkManager>();

       if (Application.isEditor) return;

       var args = GetCommandlineArgs();

       if (args.TryGetValue("-mode", out string mode))
       {
           switch (mode)
           {
               case "server":
                   netManager.StartServer();
                   text.text = "Server";
                   break;
               case "host":
                   netManager.StartHost();
                   text.text = "Host";
                   break;
               case "client":

                   netManager.StartClient();
                   text.text = "Client";
                   break;
           }
       }
   }

   private Dictionary<string, string> GetCommandlineArgs()
   {
       Dictionary<string, string> argDictionary = new Dictionary<string, string>();

       var args = System.Environment.GetCommandLineArgs();

       for (int i = 0; i < args.Length; ++i)
       {
           var arg = args[i].ToLower();
           if (arg.StartsWith("-"))
           {
               var value = i < args.Length - 1 ? args[i + 1].ToLower() : null;
               value = (value?.StartsWith("-") ?? false) ? null : value;

               argDictionary.Add(arg, value);
           }
       }
       return argDictionary;
   }
}
  • 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
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

相信有了我之前讲解的基础知识,这段代码对于你来说会非常好理解。
我们首先在Start方法中获取了当前物体的NetworkManager组件,之后将通过该组件,来启动不同的端。

接下来判断是否是在编辑器中运行,如果当前在编辑器中运行,则会直接return,也就是不执行后面的代码。

后面我们定义了一个GetCommandlineArgs()来获取命令行参数,该方法使用了我们在前几小节中讲解的知识,首先获取命令行参数,然后遍历参数数组,找到以“-”开头的字符串,这个字符串,我们视其为命令,然后进行进一步的处理,判断在这个命令的后面,是否紧跟着参数,比方说输入一个“-mode”,那么在“-mode”后面,是否紧跟着这个命令的参数,如果跟着了,那就把这个命令和参数以键值对的形式,存储到字典中。比如说“-mode host”,我们就把-mode作为字典的键,host作为值存储起来,如果输入的是“-mode”,后面没有参数,那我们就把-mode作为键,null作为参数。

接下来,我们将脚本挂载到NetworkCommandLine物体上,并且将之前创建好的TextUI拖拽到Text字段上。
在这里插入图片描述
这样我们就完成了准备工作。接下来点击菜单栏的File - Build Settings… 。点击Add Open Scenes来将我们的场景添加到Build中。
在这里插入图片描述
接着点击Build,为你的程序选择一个合适的位置,来构建发布版程序。然后我们打开CMD,进入到你程序生成的位置,这对你来说应该并不难,因为在前面小结有讲解过,然后输入如下命令:你程序的名字.exe -mode host
这个命令的含义是,以主机端启动你的程序,结合之前的NetworkCommandLine 脚本代码并不难理解,-mode host这段命令是通过这个脚本解析的。
接下来程序就会运行,如下图所示,我们的左上角显示了Host,表示我们是以主机端启动的,当然你也可以尝试使用客户端和服务器端。
在这里插入图片描述
这时我们可以注意到,程序的左下角有报错信息,这是由于我们没有选择transport和设置玩家预制体导致的。关于这部分内容,之后会有讲解。
之前我们通过一行命令启动了一个主机端,那么如果我们想要同时启动多个端,岂不是要执行多次命令,这样难免有些麻烦。但其实,我们可以通过一行命令,同时启动多端。命令如下,我们只需要在两条启动命令之间使用&符号,即可同时启动。这里需要将Learn.exe替换成你的程序名。
Learn.exe -mode host & Learn.exe -mode client
起动效果如下图所示,可以看到,一次性打开了两个窗口,同时启动了客户端和主机端。
在这里插入图片描述
在启动了想要的端之后,也不要关闭命令提示符窗口,也就是CMD窗口,在这里有一个小技巧,可以方便我们快速的输入命令。
在命令提示符窗口中,我们通过按上下方向键,即可切换之前输入的命令。这样在使用命令行调试的情况下非常好用。

简单的RPCs

RPC

RPC 是远程过程调用(Remote Procedure Calls)的缩写。这是一种用于在网络上进行通信的机制,允许在客户端和服务器之间调用函数。通过使用 RPC,我们可以在不同的网络终端之间传递消息并执行代码,从而实现跨网络的交互。

在NGO中,通过定义在网络上调用的函数,并使用 [Rpc] 特性进行标记,可以将这些函数指定为 RPCs。当调用标记为 RPC 的函数时,NGO将自动处理相关的网络通信,确保 RPC 在服务器和客户端之间进行同步执行。

持续更新中,由于笔者水平有限,如有错误,请在评论区指正

很好,这些基础的东西学完了,你可以去看官方文档了。
官方文档
官方文档的中文翻译:
中文翻译

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/116512
推荐阅读
相关标签
  

闽ICP备14008679号