官方文檔
首先亮出文檔,可以直接去看官方文檔。
本文章大部分內(nèi)容來源于官方文檔,另一部分為筆者講解的教程。
如果英語不好,或看不懂文檔的人,可以閱讀本文章。
官方文檔
官方文檔的中文翻譯:
中文翻譯
前言
教程的開發(fā)環(huán)境
本教程使用的開發(fā)環(huán)境如下:
- Windows10
- Unity 2022.3.0f1c1
- Netcode for GameObjects 1.5.2
預(yù)備知識
本教程需要具備以下預(yù)備知識:
- C#編程語言
- Unity基礎(chǔ)知識
教程內(nèi)也會講解一些C#編程的知識,包括部分Unity的知識,不過并不會全面講解。
1 簡介
1.1 Netcode for GameObjects
以前的多人游戲開發(fā),有些項目或者教程可能使用的是UNet,當(dāng)然也有別的方案,但是UNet已經(jīng)被Unity官方棄用了,也就是說UNet是一個過時的方案,目前正在開發(fā)一種新的多人游戲和網(wǎng)絡(luò)解決方案,名字叫做Netcode for GameObjects。
本篇文章的多人游戲解決方案采用的就是Netcode for GameObjects。
Netcode for GameObjects(簡稱Netcode或NGO)是一個為Unity構(gòu)建的高級網(wǎng)絡(luò)庫,可用于抽象化網(wǎng)絡(luò)邏輯,抽象化網(wǎng)絡(luò)邏輯是指將網(wǎng)絡(luò)通信的復(fù)雜性和細(xì)節(jié)隱藏在一個高級接口之后,使開發(fā)者能夠更專注于構(gòu)建游戲,而無需深入了解底層的網(wǎng)絡(luò)協(xié)議和通信機(jī)制。
Netcode提供了簡單的網(wǎng)絡(luò)操作,讓我們能夠更方便的將GameObject和世界數(shù)據(jù)通過網(wǎng)絡(luò)會話發(fā)送給多個玩家或接收,并在多個玩家之間同步數(shù)據(jù)。
1.2 NGO支持的Unity版本
使用Netcode,我們的Unity需要是2021.3或者更高的版本。并且腳本后端是Mono和IL2CPP。
Unity有兩種腳本后端:Mono和IL2CPP(Intermediate Language To C++),它們使用不同的編譯技術(shù),Mono使用即時(JIT)編譯,在運(yùn)行時按需編譯代碼。而IL2CPP使用提前(AOT)編譯,在運(yùn)行應(yīng)用程序之前對整個應(yīng)用程序進(jìn)行編譯。
Mono是一種開源的跨平臺的.NET實(shí)現(xiàn),允許開發(fā)者在不同的操作系統(tǒng)上運(yùn)行.NET應(yīng)用程序。它提供了一系列工具和庫,使開發(fā)者能夠使用C#等.NET編程語言來創(chuàng)建和運(yùn)行應(yīng)用程序。
1.3NGO支持的平臺
NGO支持如下平臺:
Windows、MacOS和Linux
iOS和Android
運(yùn)行在Windows、Android和iOS操作系統(tǒng)上的XR平臺
大多數(shù)封閉平臺,如游戲主機(jī)。
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
首先我們需要新建一個項目,當(dāng)然如果你也可以打開已有項目。
進(jìn)入項目后打開Package Manager,在編輯器的菜單欄選擇“Window > Package Manager”,即可打開Package Manager。然后點(diǎn)擊左上角的加號“+”,選擇“Add package by name…”。然后在包名稱的輸入框中輸入“com.unity.netcode.gameobjects”,然后選擇“Add”,這樣就為你的項目導(dǎo)入了NGO。
2.2 運(yùn)行項目
運(yùn)行多人游戲,那就需要啟動多個游戲?qū)嵗?,將多個游戲?qū)嵗圆煌藖韱?,比如主機(jī)端或者客戶端,啟動方法也有很多,例如可以在程序中通過制作網(wǎng)絡(luò)連接的UI界面選擇啟動端。也可以通過命令行啟動,獲取命令行參數(shù)來選擇對應(yīng)端。
這里先介紹第二種方法,也就是通過命令行來啟動多端。
2.2.1 Unity基礎(chǔ)
獲取命令行參數(shù)
通過命令行啟動unity程序的時候,我們可以獲取其命令行參數(shù)。創(chuàng)建一個Unity項目,然后創(chuàng)建一個Text,用于一會顯示我們獲取到的參數(shù),然后創(chuàng)建一個腳本,該腳本內(nèi)容如下:
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()方法來獲取命令行參數(shù),該方法返回一個字符串?dāng)?shù)組。
現(xiàn)在我們保存代碼和場景,然后構(gòu)建程序,并通過命令行的方式執(zhí)行。如圖所示,直接通過指定路徑的方式執(zhí)行程序,Learn_2D是你程序的名字,前面的路徑就是該程序所在的路徑。然后,在執(zhí)行程序的命令后面,我們跟上命令行參數(shù),輸入什么都可以,隨便寫點(diǎn)字符串。
運(yùn)行命令后,程序就啟動了,如下圖所示,程序顯示了我們獲取到的命令行參數(shù)。注意,你輸入的啟動程序的命令,也是命令行參數(shù)的一個。而且是第一個命令行參數(shù),也就是args[0]。
判斷當(dāng)前是否在編輯器中運(yùn)行
在Unity中,我們可以通過Application類的isEditor字段來判斷當(dāng)前運(yùn)行的游戲是否是在編輯器中運(yùn)行的,Application.isEditor是一個靜態(tài)只讀的布爾值,定義如下:
// 摘要:
// Are we running inside the Unity editor? (Read Only)
public static bool isEditor => true;
當(dāng)我們在 Unity 編輯器中運(yùn)行游戲時,Application.isEditor的值將為 true。
當(dāng)我們在游戲的構(gòu)建版本(即發(fā)布版本)中運(yùn)行游戲時,Application.isEditor的值將為 false。
Application.isEditor在某些時候非常有用。通過它,可以根據(jù)我們在編輯器還是構(gòu)建版本中,來執(zhí)行不同的邏輯和功能。這對于在開發(fā)過程中進(jìn)行調(diào)試、測試和實(shí)現(xiàn)編輯器專用功能有一定的幫助。
這里我們寫一個測試代碼如下:
if(Application.isEditor)
{
text.text = "此時正在編輯器中運(yùn)行";
}
else
{
text.text = "此時正在發(fā)布版本中運(yùn)行";
}
運(yùn)行效果如下,當(dāng)我們在編輯中運(yùn)行時:
當(dāng)我們把項目Build后再次運(yùn)行,結(jié)果如下:
發(fā)布版本的Log日志輸出
其實(shí)當(dāng)我們將項目Build后,運(yùn)行的程序,也會輸出日志文件,沒錯就是你在代碼中使用Debug.Log方法輸出的日志文件,那么問題來了,我們的發(fā)布版本又沒有Console窗口,怎么輸出日志文件?其實(shí),日志文件被存儲在了一個名叫Player.log的文件中。
Player.log 文件是用來記錄詳細(xì)的日志信息的。無論是在編輯器中還是在發(fā)布版本中,Debug.Log 輸出的內(nèi)容都會被記錄在 Player.log 文件中。
Player.log文件默認(rèn)存放在如下路徑中:
C:\Users\username\AppData\LocalLow\CompanyName\ProductName\Player.log
具體來說,username 是指當(dāng)前計算機(jī)上登錄的用戶名,而 CompanyName 和 ProductName 是指 Unity 項目中的公司名稱和項目名稱。如果你沒有明確填寫公司名稱,那么這里的CompanyName就是“DefaultCompany”。
例如如下路徑:
C:\Users\Zhi\AppData\LocalLow\DefaultCompany\Learn_2D\Player.log
想必你已經(jīng)知道日志文件默認(rèn)在什么位置了,接下來我們嘗試輸出一些日志看看。
編寫如下代碼:
void Start()
{
Debug.Log("此處為輸出的日志");
}
代碼中,我們簡單的打印了一行日志。
接下來構(gòu)建項目并運(yùn)行,然后去前面說的路徑中,找到Player.log文件,打開該文件,可以看到如下內(nèi)容:
里面就有我們在程序中輸出的日志。
我們剛剛說了,那是默認(rèn)路徑,也就是說,我們可以通過某種方法,改變其存放的位置。通過改變其存放位置,可以讓我們更方便的打開這個日志文件,當(dāng)然你也可以存放在默認(rèn)路徑,只要你覺得方便查看輸出信息即可。
那么如何改變存放位置呢?我們可以通過-logfile命令,來改變其存放的位置并且改變?nèi)罩疚募拿帧?br> 首先,打開命令提示符窗口,可以通過Win+R鍵,打開運(yùn)行窗口。然后在運(yùn)行窗口中輸入cmd命令,回車,即可打開命令提示符窗口。
在進(jìn)行下一步之前,請確保你已經(jīng)構(gòu)建好了一個Unity程序。
我們的logfile命令的格式如下:-logfile 文件名
后面的文件名,就是你想要將Player.log文件改變的名字,注意要加文件后綴名。
比如:-logfile log-server.log
如此一來,將會吧Player.log文件更名為log-server.log文件,并在當(dāng)前命令提示符所在的文件路徑生成它。
現(xiàn)在,讓我們實(shí)踐一下。構(gòu)建一個程序,然后通過命令提示符來運(yùn)行他,為其傳遞命令行參數(shù) -logfile weLog.txt
。
運(yùn)行后,發(fā)現(xiàn)確實(shí)生成了,而且剛好是在我們命令提示符當(dāng)前所在的目錄下,也就是“C:\Users\28446”路徑下。
那么我們?nèi)绾胃淖儺?dāng)前命令提示符所在的目錄,讓其生成到指定的位置呢?
接下來我們需要了解一下命令提示符窗口的cd命令,cd(change directory)是用于在命令行中切換當(dāng)前工作目錄的命令。
默認(rèn)情況下,打開cmd進(jìn)入的是當(dāng)前windows用戶的文件夾下。
我們可以通過cd ..
命令,來返回上一級目錄,也就是父目錄。
然后通過dir
命令,我們可以看到當(dāng)前文件夾下有哪些文件或文件夾。
如果我們想要進(jìn)入某一個文件夾下,可以再次使用cd命令+文件夾的名字來進(jìn)入,例如cd 28446
當(dāng)然,我們也可以輸入一大串的路徑,然后直接通過cd命令進(jìn)入。比如說:
但大家要注意,cd命令無法跨越盤符,也就是說,如果這一大串的路徑是D盤的 ,那我們使用cd命令是無法直接跳轉(zhuǎn)過去的,如圖所示。
一般情況下,我們在計算機(jī)里分了很多的區(qū),或者說是盤,比如說C盤或者是D、E、F盤等等。而我們的應(yīng)用程序,一般是不會放在C盤的。那么,如果我們想要進(jìn)入到其他的盤符,應(yīng)該怎么做呢?
這時候,直接輸入“盤符:”即可,例如d:
如圖,我們進(jìn)入了D盤。
這時候在輸入那一大串的路徑,直接跳轉(zhuǎn)到想去的文件夾。
然后,我們就可以進(jìn)入到游戲所在的路徑,使用-logfile命令,來將我們的player.log文件生成到游戲可執(zhí)行程序所在的目錄下了,這樣方便我們打開并觀察日志。
2.2.2 C#基礎(chǔ)
判斷字符串前綴
我們剛剛學(xué)習(xí)了如何獲取命令行參數(shù),獲取數(shù)據(jù)后,接下來我們就要對其進(jìn)行處理了,在對命令行參數(shù)進(jìn)行處理之前,我們先來學(xué)習(xí)一些C#的知識。
首先就是一個字符串處理函數(shù),StartsWith(),這個函數(shù)我們在學(xué)習(xí)C#的時候都接觸過,它的作用是用來判斷字符串開頭的字符,具體來講,就是判斷字符串的開頭是否為指定的字符串,如果是,就返回True,如果不是就返回False。
這是一個實(shí)例方法,也就是說,我們需要在一個字符串的實(shí)例上使用這個方法。
具體來舉個例子:
string str = "Hello, world!";
bool startsWithHello = str.StartsWith("Hello");
Console.WriteLine(startsWithHello);
我們聲明了一個字符串,里面是Hello,world,通過StartWIth方法, 判斷其開頭是否為"Hello"。
運(yùn)行結(jié)果如圖所示:
但是需要注意一下的是,StartWith方法是區(qū)分大小寫的,也就是說如果這里判斷的值是hello時,結(jié)果就會是False。
string str = "Hello, world!";
bool startsWithHello = str.StartsWith("hello");
Console.WriteLine(startsWithHello);
此時運(yùn)行結(jié)果為False。
但是我們可以通過一個枚舉值,來使其不區(qū)分大小寫。將StringComparison.OrdinalIgnoreCase枚舉值作為第二個參數(shù),StartWith就不會區(qū)分大小寫了。
string str = "Hello, world!";
bool startsWithHello = str.StartsWith("hello", StringComparison.OrdinalIgnoreCase);
Console.WriteLine(startsWithHello);
此時運(yùn)行結(jié)果為:True。
2.2.1.2 空值合并操作符
空值合并操作符是由兩個問號組成的“??”。
expression1 ?? expression2
它的作用是用來檢查問號左側(cè)的表達(dá)式是否為null,如果為null,就返回右側(cè)表達(dá)式的值,如果不為null,就返回左側(cè)表達(dá)式的值。
舉個例子:
string name = null;
string result = name ?? "無名";
Console.WriteLine(result);
運(yùn)行結(jié)果顯示為:無名。
這里我們用了一個name變量,給其null值,然后通過空值合并操作符,來判斷其是否為null,如果此時name不是null值,就會返回name ,把name的值,賦值給result,如果name為null,就會返回右側(cè)表達(dá)式的值,也就是"無名"二字。
由于這里我們給name賦值為null,理所當(dāng)然的就會返回右側(cè)表達(dá)式的值了。
2.2.1.3 獲取字典中的值
復(fù)習(xí)一下,在學(xué)習(xí)C#的時候,我們應(yīng)該學(xué)習(xí)過TryGetValue方法,我們可以通過TryGetValue來從字典中獲取值。這個方法是字典類的一個成員方法(比如Dictionary<TKey, TValue>)。這是一個可以安全的獲取字典中的值的方法,因?yàn)樗苊饬嗽谧值渲胁檎益I時引發(fā)的異常。
方法的定義如下:
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value);
該方法有兩個參數(shù),第一個參數(shù)是鍵。
第二個參數(shù)是值,當(dāng)找到匹配的鍵時,會通過該參數(shù)將值返回出來。如果沒找到對應(yīng)的鍵值對,就會將值類型的默認(rèn)值賦給這個變量,并返回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'");
}
運(yùn)行結(jié)果如下所示:
在代碼中,我們使用TryGetValue()方法嘗試獲取鍵為1的值,由于字典中存在鍵1,方法返回true,并將對應(yīng)的值"Unity"賦值給result變量。然后,我們再嘗試獲取鍵為3的值,由于字典中不存在鍵3,方法返回false,并將默認(rèn)值(null)賦給result變量。
使用TryGetValue()方法時,可以避免查找字典中不存在的鍵時引發(fā)的異常,非常滴好用。
2.2.3 創(chuàng)建命令行測試助手
接下來,我們就要使用命令行來啟動多端了,首先進(jìn)入到你的項目,新建一個UI-Text-TextMeshPro。調(diào)整其位置為左上角,設(shè)置Anchor Presets為left top。創(chuàng)建這個UI的目的是為了顯示當(dāng)前實(shí)例是客戶端還是服務(wù)器端、主機(jī)端。
最后效果如圖所示:
然后在游戲的場景中新建一個空物體,命名為NetworkManager
。我們點(diǎn)擊該物體的Add Component,搜索NetworkManager,為其掛載該組件,后續(xù)我們會通過該組件來啟動不同的端。該組件是整個NGO中最為重要的組件,包含了你項目中所有與網(wǎng)絡(luò)代碼相關(guān)的設(shè)置,可以說netcode的中心。
然后再右鍵單擊NetworkManger,為其創(chuàng)建子物體,同樣也是空物體,并將該物體命名為NetworkCommandLine
。
緊接著,我們來創(chuàng)建一個腳本,用于識別命令行的命令,并根據(jù)不同的命令來啟動不同的端。
創(chuàng)建一個腳本,命名為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;
}
}
相信有了我之前講解的基礎(chǔ)知識,這段代碼對于你來說會非常好理解。
我們首先在Start方法中獲取了當(dāng)前物體的NetworkManager組件,之后將通過該組件,來啟動不同的端。
接下來判斷是否是在編輯器中運(yùn)行,如果當(dāng)前在編輯器中運(yùn)行,則會直接return,也就是不執(zhí)行后面的代碼。
后面我們定義了一個GetCommandlineArgs()來獲取命令行參數(shù),該方法使用了我們在前幾小節(jié)中講解的知識,首先獲取命令行參數(shù),然后遍歷參數(shù)數(shù)組,找到以“-”開頭的字符串,這個字符串,我們視其為命令,然后進(jìn)行進(jìn)一步的處理,判斷在這個命令的后面,是否緊跟著參數(shù),比方說輸入一個“-mode”,那么在“-mode”后面,是否緊跟著這個命令的參數(shù),如果跟著了,那就把這個命令和參數(shù)以鍵值對的形式,存儲到字典中。比如說“-mode host”,我們就把-mode作為字典的鍵,host作為值存儲起來,如果輸入的是“-mode”,后面沒有參數(shù),那我們就把-mode作為鍵,null作為參數(shù)。
接下來,我們將腳本掛載到NetworkCommandLine物體上,并且將之前創(chuàng)建好的TextUI拖拽到Text字段上。
這樣我們就完成了準(zhǔn)備工作。接下來點(diǎn)擊菜單欄的File - Build Settings… 。點(diǎn)擊Add Open Scenes來將我們的場景添加到Build中。
接著點(diǎn)擊Build,為你的程序選擇一個合適的位置,來構(gòu)建發(fā)布版程序。然后我們打開CMD,進(jìn)入到你程序生成的位置,這對你來說應(yīng)該并不難,因?yàn)樵谇懊嫘〗Y(jié)有講解過,然后輸入如下命令:你程序的名字.exe -mode host
這個命令的含義是,以主機(jī)端啟動你的程序,結(jié)合之前的NetworkCommandLine 腳本代碼并不難理解,-mode host這段命令是通過這個腳本解析的。
接下來程序就會運(yùn)行,如下圖所示,我們的左上角顯示了Host,表示我們是以主機(jī)端啟動的,當(dāng)然你也可以嘗試使用客戶端和服務(wù)器端。
這時我們可以注意到,程序的左下角有報錯信息,這是由于我們沒有選擇transport和設(shè)置玩家預(yù)制體導(dǎo)致的。關(guān)于這部分內(nèi)容,之后會有講解。
之前我們通過一行命令啟動了一個主機(jī)端,那么如果我們想要同時啟動多個端,豈不是要執(zhí)行多次命令,這樣難免有些麻煩。但其實(shí),我們可以通過一行命令,同時啟動多端。命令如下,我們只需要在兩條啟動命令之間使用&符號,即可同時啟動。這里需要將Learn.exe替換成你的程序名。Learn.exe -mode host & Learn.exe -mode client
起動效果如下圖所示,可以看到,一次性打開了兩個窗口,同時啟動了客戶端和主機(jī)端。
在啟動了想要的端之后,也不要關(guān)閉命令提示符窗口,也就是CMD窗口,在這里有一個小技巧,可以方便我們快速的輸入命令。
在命令提示符窗口中,我們通過按上下方向鍵,即可切換之前輸入的命令。這樣在使用命令行調(diào)試的情況下非常好用。
簡單的RPCs
RPC
RPC 是遠(yuǎn)程過程調(diào)用(Remote Procedure Calls)的縮寫。這是一種用于在網(wǎng)絡(luò)上進(jìn)行通信的機(jī)制,允許在客戶端和服務(wù)器之間調(diào)用函數(shù)。通過使用 RPC,我們可以在不同的網(wǎng)絡(luò)終端之間傳遞消息并執(zhí)行代碼,從而實(shí)現(xiàn)跨網(wǎng)絡(luò)的交互。
在NGO中,通過定義在網(wǎng)絡(luò)上調(diào)用的函數(shù),并使用 [Rpc] 特性進(jìn)行標(biāo)記,可以將這些函數(shù)指定為 RPCs。當(dāng)調(diào)用標(biāo)記為 RPC 的函數(shù)時,NGO將自動處理相關(guān)的網(wǎng)絡(luò)通信,確保 RPC 在服務(wù)器和客戶端之間進(jìn)行同步執(zhí)行。文章來源:http://www.zghlxwxcb.cn/news/detail-721413.html
持續(xù)更新中,由于筆者水平有限,如有錯誤,請在評論區(qū)指正
很好,這些基礎(chǔ)的東西學(xué)完了,你可以去看官方文檔了。
官方文檔
官方文檔的中文翻譯:
中文翻譯文章來源地址http://www.zghlxwxcb.cn/news/detail-721413.html
到了這里,關(guān)于【Unity2022】Unity多人游戲開發(fā)教程-Netcode for GameObjects-使用命令行啟動多人游戲的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!