【Unity2022】Unity多人游戏开发教程-安装Netcode for GameObjects

官方文档

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

前言

教程的开发环境

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

  • 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 C#基础

2.2.1.1 判断字符串前缀

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

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

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

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

此时运行结果为False。

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

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

此时运行结果为:True。

2.2.1.2 空值合并操作符

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

string name = null;
string result = name ?? "无名";
Console.WriteLine(result);

运行结果显示为:无名。
这里我们用了一个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);

该方法有两个参数,第一个参数是键。
第二个参数是值,当找到匹配的键时,会通过该参数将值返回出来。如果没找到对应的键值对,就会将值类型的默认值赋给这个变量,并返回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'");
 }

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

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

2.2.2 Unity基础

2.2.2.1 获取命令行参数

通过命令行启动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";
        }
    }
}

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

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

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

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

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

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

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

if(Application.isEditor)
{
    
    
    text.text = "此时正在编辑器中运行";
}
else
{
    
    
    text.text = "此时正在发布版本中运行";
}

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

2.2.2.3 发布版本的Log日志输出

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

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

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

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

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

例如如下路径:

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

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

void Start()
{
    
    
    Debug.Log("此处为输出的日志");
}

代码中,我们简单的打印了一行日志。
接下来构建项目并运行,然后去前面说的路径中,找到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.3 创建命令行测试助手

接下来,我们就要使用命令行来启动多端了,在运行程序之前,我们来创建一个脚本,用于识别命令行的命令,并根据不同的命令来启动不同的端。
创建一个脚本,命名为NetworkCommandLine,然后在其中编写如下代码:

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

public class NetworkCommandLine : MonoBehaviour
{
    
    
   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();
                   break;
               case "host":
                   netManager.StartHost();
                   break;
               case "client":

                   netManager.StartClient();
                   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;
   }
}

相信有了我之前讲解的基础知识,这段代码对于你来说会非常好理解。
我们首先在Start方法中获取了当前物体的NetworkManager组件,之后将通过该组件,来启动不同的端。
接下来判断是否是在编辑器中运行,如果当前不在编辑器中运行,才会执行后续操作。
使用GetCommandlineArgs()来获取命令行参数,并且对命令行参数进行解析和处理。

2.3 Hello World

接下来我们开始使用NGO的基本功能,实现一个Hello World项目。
之前我们已经创建了一个Unity项目,就继续用那个项目了。
首先创建一个空物体,然后为该命名为NetworkManager,并为其挂载NetworkManager组件。
在Inspector标签中查看该组件,将它的协议类型改为Unity Transport。
在这里插入图片描述
选择协议类型为UnityTransport后,就会发现该物体又挂载了一个UnityTransport组件。
在这里插入图片描述
接下来我们开始创建玩家的预制体。

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

猜你喜欢

转载自blog.csdn.net/weixin_44499065/article/details/131741643