文章目錄
- 一、前言
- 二、簡單的Socket通信:多人聊天室
- 1、服務端:python代碼
- 1.1、import socket
- 1.2、構造socket物件
- 1.3、系結/監聽埠
- 1.3、監聽客戶端連接
- 1.4、接收客戶端socket訊息
- 1.5、多執行緒
- 1.6、完整代碼:game_server.py
- 2、客戶端:Unity
- 2.1、創建工程,搭建場景
- 2.2、Socket封裝:ClientSocket.cs
- 2.2.1、構造Socket物件
- 2.2.2、連接服務器
- 2.2.3、斷開連接
- 2.2.4、發送訊息
- 2.2.5、接收服務端訊息
- 2.2.6、完整代碼:ClientSocket.cs
- 2.3、UI互動:TestPanel.cs
- 2.3.1、定義變數
- 2.3.2、登錄服務端
- 2.3.3、斷開連接
- 2.3.4、發送訊息
- 2.3.5、接收訊息
- 2.3.6、完整代碼:TestPanel.cs
- 2.4、掛腳本,賦值成員物件
- 3、打包客戶端
- 4、運行測驗
- 5、工程原始碼
- 三、拓展:Mirror Networking
- 1、局域網多人聯機Demo的救星:Mirror
- 2、關于Mirror
- 3、Mirror插件下載
- 4、Mirror 案例測驗:多人坦克對戰
- 5、Mirror 案例講解:多人坦克對戰
- 5.1、NetworkManager物體
- 5.1.1、NetworkManager組件
- 5.1.2、NetworkManagerHUD組件
- 5.1.3、KcpTransport組件
- 5.2、地面(帶導航功能)
- 5.2.1、創建Plane
- 5.2.2、導航烘焙:Navigation
- 5.3、坦克生成點:NewworkStartPosition
- 5.4、坦克身上的組件
- 5.4.1、坦克預設
- 5.4.2、NavMeshAgent組件
- 5.4.4、Animator組件
- 5.4.5、NetworkTransform組件
- 5.4.6、NetworkIdentity組件
- 5.4.7、NetworkBehaviour組件: Tank
- 5.5、賦值PlayerPrefab
- 5.6、炮彈預設
- 5.7、坦克腳本:Tank.cs
- 5.8、Transform的網路同步:NetworkTransform.cs
- 5.9、炮彈腳本:Projectile.cs
- 四、完畢
一、前言
嗨,大家好,我是新發,
事情是這樣的,上次有同學問我能不能出一期 網路 相關的教程,

然而我眼花看錯了,看成了 網格,我還專門寫了一篇文章:《【游戲開發進階】Unity網格探險之旅(Mesh | 動態合批 | 骨骼影片 | 蒙皮 )》
直到有同學在評論里提醒我,真是尷尬…

嘛,沒事,今天就補上,寫一篇 網路 相關文章,
我準備做個例子,使用.Net原生的Socket模塊來實作簡單的多人聊天室功能,
話不多說,我們開始吧~
二、簡單的Socket通信:多人聊天室
Unity中我們要實作網路通信,可以使用.Net的Socket模塊來實作,
為了演示,我就用python寫個簡單的服務端,用Unity作為客戶端,
先畫個 流程圖,
服務端(python)流程圖:

客戶端(Unity)流程圖:

1、服務端:python代碼
新建一個python腳本:game_server.py,如下

1.1、import socket
因為我們要使用socket,所以先引入socket模塊:
import socket
1.2、構造socket物件
g_socket_server = None
g_socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
關于socket的python函式原型可以使用help(socket)查看,

第一個引數是socket domains(通信協議族),有兩種型別:AF_UNIX、AF_INET,它們的區別:
| 通信協議族 | 說明 |
|---|---|
| AF_UNIX | 本機通信;另,它只能夠用于單一的Unix系統行程間通信,不能在Windows系統中使用 |
| AF_INET | TCP/IP通信 |
第二個引數是socket type(套接字型別),有SOCKET_STREAM、SOCK_DGRAM、SOCK_RAW三種,
| 套接字型別 | 說明 |
|---|---|
| SOCKET_STREAM | 流式套接字,基于TCP通信,資料有保障(即能保證資料正確傳送到對方),多用于資料(如檔案)傳送 |
| SOCK_DGRAM | 資料報套接字,基于UDP通信,資料是無保障的 , 主要用于在網路上發廣播資訊 |
| SOCK_RAW | 原始套接字,普通的套接字無法處理ICMP、IGMP等網路報文,而SOCK_RAW可以;SOCK_RAW也可以處理特殊的IPv4報文;此外,利用原始套接字,可以通過IP_HDRINCL套接字選項由用戶構造IP頭 |
1.3、系結/監聽埠
ADDRESS = ('127.0.0.1', 8712)
g_socket_server.bind(ADDRESS)
g_socket_server.listen(5)
1.3、監聽客戶端連接
client, info = g_socket_server.accept()
1.4、接收客戶端socket訊息
data = client.recv(1024)
msg = data.decode(encoding='utf8')
使用json對訊息欄位進行決議:
import json
jd = json.loads(jsonstr)
protocol = jd['protocol']
uname = jd['uname']
msg = jd['msg']
1.5、多執行緒
由于監聽客戶端(socket.accept)和接收訊息(socket.recv)都是 阻塞 的,為了不阻塞主執行緒,我們使用 子執行緒 來處理,
創建不帶引數的執行緒:
thread = Thread(target=thread_func)
thread.start()
def thread_func():
pass
創建帶引數的執行緒:
thread = Thread(target=thread_func, args=(p1, p2, p3))
thread.start()
def thread_func(p1, p2, p3):
pass
1.6、完整代碼:game_server.py
最終,game_server.py完整代碼如下:
'''
作者:林新發,博客:https://blog.csdn.net/linxinfa
功能:簡單的Socket通信,聊天室服務端
python版本:3.6.4
'''
import socket # 匯入 socket 模塊
from threading import Thread
import time
import json
ADDRESS = ('127.0.0.1', 8712) # 系結地址
g_socket_server = None # 負責監聽的socket
g_conn_pool = {} # 連接池
def accept_client():
global g_socket_server
g_socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
g_socket_server.bind(ADDRESS)
g_socket_server.listen(5) # 最大等待數(有很多人理解為最大連接數,其實是錯誤的)
print("server start,wait for client connecting...")
'''
接收新連接
'''
while True:
client, info = g_socket_server.accept() # 阻塞,等待客戶端連接
# 給每個客戶端創建一個獨立的執行緒進行管理
thread = Thread(target=message_handle, args=(client, info))
thread.setDaemon(True)
thread.start()
def message_handle(client, info):
'''
訊息處理
'''
handle_id = info[1]
# 快取客戶端socket物件
g_conn_pool[handle_id] = client
while True:
try:
data = client.recv(1024)
jsonstr = data.decode(encoding='utf8')
jd = json.loads(jsonstr)
protocol = jd['protocol']
uname = jd['uname']
if 'login' == protocol:
print('on client login, ' + uname)
# 轉發給所有客戶端
for u in g_conn_pool:
g_conn_pool[u].sendall((uname + " 進入了房間").encode(encoding='utf8'))
elif 'chat' == protocol:
# 收到客戶端聊天訊息
print(uname + ":" + jd['msg'])
# 轉發給所有客戶端
for key in g_conn_pool:
g_conn_pool[key].sendall((uname + " : " + jd['msg']).encode(encoding='utf8'))
except Exception as e:
remove_client(handle_id)
break
def remove_client(handle_id):
client = g_conn_pool[handle_id]
if None != client:
client.close()
g_conn_pool.pop(handle_id)
print("client offline: " + str(handle_id))
if __name__ == '__main__':
# 新開一個執行緒,用于接收新連接
thread = Thread(target=accept_client)
thread.setDaemon(True)
thread.start()
# 主執行緒邏輯
while True:
time.sleep(0.1)
2、客戶端:Unity
2.1、創建工程,搭建場景
新建一個Unity工程,

使用UGUI簡單搭建一下界面,如下

養成好習慣,界面保存為預設:TestPanel.prefab,

2.2、Socket封裝:ClientSocket.cs
我們先封裝一個ClientSocket.cs,實作Socket的創建、連接和收發訊息等功能,
2.2.1、構造Socket物件
// using System.Net.Sockets;
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
2.2.2、連接服務器
socket.Connect(host, port);
2.2.3、斷開連接
socket.Shutdown(SocketShutdown.Both);
socket.Close();
socket = null;
2.2.4、發送訊息
// byte[] bytes 你的訊息的位元組陣列
NetworkStream netstream = new NetworkStream(socket);
netstream.Write(bytes, 0, bytes.Length);
2.2.5、接收服務端訊息
// 回呼函式物件
AsyncCallback recvCb = new AsyncCallback(RecvCallBack);
// 資料快取
byte[] recvBuff = new byte[0x4000];
// 訊息佇列
Queue<string> msgQueue = new Queue<string>();
// 每幀呼叫此方法
socket.BeginReceive(recvBuff, 0, recvBuff.Length, SocketFlags.None, recvCb, this);
// 接收訊息回呼函式
private void RecvCallBack(IAsyncResult ar)
{
var len = socket.EndReceive(ar);
byte[] msg = new byte[len];
Array.Copy(m_recvBuff, msg, len);
var msgStr = System.Text.Encoding.UTF8.GetString(msg);
// 將訊息塞入佇列中
msgQueue.Enqueue(msgStr);
}
// 從訊息佇列中取出訊息(供外部呼叫)
public string GetMsgFromQueue()
{
if (msgQueue.Count > 0)
return msgQueue.Dequeue();
return null;
}
2.2.6、完整代碼:ClientSocket.cs
最終,ClientSocket.cs完整代碼如下:
/*
* Socket封裝
* 作者:林新發 博客:https://blog.csdn.net/linxinfa
*/
using System;
using System.Net.Sockets;
using UnityEngine;
using System.Collections.Generic;
public class ClientSocket
{
private Socket init()
{
Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 接收的訊息資料包大小限制為 0x4000 byte, 即16KB
m_recvBuff = new byte[0x4000];
m_recvCb = new AsyncCallback(RecvCallBack);
return clientSocket;
}
/// <summary>
/// 連接服務器
/// </summary>
/// <param name="host">ip地址</param>
/// <param name="port">埠號</param>
public void Connect(string host, int port)
{
if (m_socket == null)
m_socket = init();
try
{
Debug.Log("connect: " + host + ":" + port);
m_socket.SendTimeout = 3;
m_socket.Connect(host, port);
connected = true;
}
catch (Exception ex)
{
Debug.LogError(ex);
}
}
/// <summary>
/// 發送訊息
/// </summary>
public void SendData(byte[] bytes)
{
NetworkStream netstream = new NetworkStream(m_socket);
netstream.Write(bytes, 0, bytes.Length);
}
/// <summary>
/// 嘗試接收訊息(每幀呼叫)
/// </summary>
public void BeginReceive()
{
m_socket.BeginReceive(m_recvBuff, 0, m_recvBuff.Length, SocketFlags.None, m_recvCb, this);
}
/// <summary>
/// 當收到服務器的訊息時會回呼這個函式
/// </summary>
private void RecvCallBack(IAsyncResult ar)
{
var len = m_socket.EndReceive(ar);
byte[] msg = new byte[len];
Array.Copy(m_recvBuff, msg, len);
var msgStr = System.Text.Encoding.UTF8.GetString(msg);
// 將訊息塞入佇列中
m_msgQueue.Enqueue(msgStr);
// 將buffer清零
for (int i = 0; i < m_recvBuff.Length; ++i)
{
m_recvBuff[i] = 0;
}
}
/// <summary>
/// 從訊息佇列中取出訊息
/// </summary>
/// <returns></returns>
public string GetMsgFromQueue()
{
if (m_msgQueue.Count > 0)
return m_msgQueue.Dequeue();
return null;
}
/// <summary>
/// 關閉Socket
/// </summary>
public void CloseSocket()
{
Debug.Log("close socket");
try
{
m_socket.Shutdown(SocketShutdown.Both);
m_socket.Close();
}
catch(Exception e)
{
//Debug.LogError(e);
}
finally
{
m_socket = null;
connected = false;
}
}
public bool connected = false;
private byte[] m_recvBuff;
private AsyncCallback m_recvCb;
private Queue<string> m_msgQueue = new Queue<string>();
private Socket m_socket;
}
2.3、UI互動:TestPanel.cs
然后再創建一個腳本:TestPanel.cs,用于實作UI部分的互動邏輯,
2.3.1、定義變數
先定義一些變數:
private const string IP = "127.0.0.1";
private const int PORT = 8712;
// 用戶名輸入
public InputField unameInput;
// 訊息輸入
public InputField msgInput;
// 登錄按鈕
public Button loginBtn;
// 發送按鈕
public Button sendBtn;
// 連接狀態文本
public Text stateTxt;
// 連接按鈕文本
public Text connectBtnText;
// 聊天室聊天文本
public Text chatMsgTxt;
// 封裝的ClientSocket物件
private ClientSocket clientSocket = new ClientSocket();
2.3.2、登錄服務端
// 連接
clientSocket.Connect(IP, PORT);
stateTxt.text = clientSocket.connected ? "已連接" : "未連接";
connectBtnText.text = clientSocket.connected ? "斷開" : "連接";
if (clientSocket.connected)
unameInput.enabled = false;
// 登錄
Send("login");
2.3.3、斷開連接
clientSocket.CloseSocket();
stateTxt.text = "已斷開";
connectBtnText.text = "連接";
unameInput.enabled = true;
2.3.4、發送訊息
這里用了一個迷你版的
json庫:JSONConvert,原始碼可以參見我之前寫的這篇文章:《用C#實作一個迷你json庫,無需引入dll(可直接放到Unity中使用)》
private void Send(string protocol, string msg = "")
{
JSONObject jsonObj = new JSONObject();
jsonObj["protocol"] = protocol;
jsonObj["uname"] = unameInput.text;
jsonObj["msg"] = msg;
// JSONObject轉string
string jsonStr = JSONConvert.SerializeObject(jsonObj);
// string轉byte[]
byte[] data = System.Text.Encoding.UTF8.GetBytes(jsonStr);
// 發送訊息給服務端
clientSocket.SendData(data);
}
2.3.5、接收訊息
private void Update()
{
if (clientSocket.connected)
{
clientSocket.BeginReceive();
}
var msg = clientSocket.GetMsgFromQueue();
if (!string.IsNullOrEmpty(msg))
{
// 顯示到聊天室文本中
chatMsgTxt.text += msg + "\n";
Debug.Log("RecvCallBack: " + msg);
}
}
2.3.6、完整代碼:TestPanel.cs
最終,TestPanel.cs完整代碼如下:
/*
* 聊天室客戶端 UI互動
* 作者:林新發 博客:https://blog.csdn.net/linxinfa
*/
using UnityEngine;
using UnityEngine.UI;
public class TestPanel : MonoBehaviour
{
private const string IP = "127.0.0.1";
private const int PORT = 8712;
// 用戶名輸入
public InputField unameInput;
// 訊息輸入
public InputField msgInput;
// 登錄按鈕
public Button loginBtn;
// 發送按鈕
public Button sendBtn;
// 連接狀態文本
public Text stateTxt;
// 連接按鈕文本
public Text connectBtnText;
// 聊天室聊天文本
public Text chatMsgTxt;
// 封裝的ClientSocket物件
private ClientSocket clientSocket = new ClientSocket();
private ClientSocket clientSocket = new ClientSocket();
void Start()
{
chatMsgTxt.text = "";
loginBtn.onClick.AddListener(() =>
{
if (clientSocket.connected)
{
// 斷開
clientSocket.CloseSocket();
stateTxt.text = "已斷開";
connectBtnText.text = "連接";
unameInput.enabled = true;
}
else
{
// 連接
var address = unameInput.text.Split(':');
clientSocket.Connect(IP, PORT);
stateTxt.text = clientSocket.connected ? "已連接" : "未連接";
connectBtnText.text = clientSocket.connected ? "斷開" : "連接";
if (clientSocket.connected)
unameInput.enabled = false;
// 登錄
Send("login");
}
});
sendBtn.onClick.AddListener(() =>
{
Send("chat", msgInput.text);
});
}
private void Update()
{
if (clientSocket.connected)
{
clientSocket.BeginReceive();
}
var msg = clientSocket.GetMsgFromQueue();
if (!string.IsNullOrEmpty(msg))
{
chatMsgTxt.SetAllDirty();
chatMsgTxt.text += msg + "\n";
Debug.Log("RecvCallBack: " + msg);
}
}
private void Send(string protocol, string msg = "")
{
JSONObject jsonObj = new JSONObject();
jsonObj["protocol"] = protocol;
jsonObj["uname"] = unameInput.text;
jsonObj["msg"] = msg;
// JSONObject轉string
string jsonStr = JSONConvert.SerializeObject(jsonObj);
// string轉byte[]
byte[] data = System.Text.Encoding.UTF8.GetBytes(jsonStr);
// 發送訊息給服務端
clientSocket.SendData(data);
}
private void OnApplicationQuit()
{
if (clientSocket.connected)
{
clientSocket.CloseSocket();
}
}
}
2.4、掛腳本,賦值成員物件
給TestPanel界面掛上TestPanel.cs腳本,賦值成員物件,如下

3、打包客戶端
因為我們要測驗多個客戶端連接一個服務端,為了方便測驗,我們打個Windows平臺的exe,
在Build Settings中添加要打包的場景,選擇PC, Mac & Linux Standalone平臺,

我們不想全屏顯示客戶端,在Player Settings中,找到Resolution and Presentation,設定Fullscreen Mode為Windowed,設定視窗默認寬高為640 x 360,

執行打包,

打包成功,

4、運行測驗
先使用python運行服務端,

開啟多個客戶端,分別登錄服務端,用戶名分別是皮皮貓和林新發吧~

服務端的輸出:

開始聊天,

服務端的輸出:

運行一切正常,完美,
5、工程原始碼
上面這個簡單聊天室工程原始碼已上傳到CODE CHINA,感興趣的同學可自行下載下來進行學習,
工程地址:https://codechina.csdn.net/linxinfa/UnitySocketDemo
注:我使用的Unity版本:Unity 2021.1.9f1c1 (64-bit),
另外關于
CODE CHINA的使用教程我之前也寫了一篇文章,感興趣的同學可以看看:
《CODE.CHINA使用教程,創建專案倉庫并上傳代碼(git)》

三、拓展:Mirror Networking
1、局域網多人聯機Demo的救星:Mirror
上面的簡單聊天室功能,我們是做了一個獨立的服務端負責訊息的轉發,聊天本身的邏輯非常簡單,我們把大部分作業花在了維護Socket上,要解決多執行緒問題,要解決連接斷開,要解決訊息的序列化和反序列化等等,
有些同學做了一個單機版的小Demo,想改成局域網多人聯機版,要處理好多復雜的同步問題,比如物理碰撞、狀態同步等等,這個對于Unity萌新來說,不大友好,

有沒有什么好用的網路庫可以讓開發更高效呢?有,那就是:Mirror!
注:在
Unity 5.1 ~ Unity2018中你可以使用UNet(全稱Unity Networking),到Unity 2019之后UNet就被廢棄了,Mirror就是來替代UNet的,你在網上搜到的Unity Netwoking的教程就是UNet,它已經過時了,不要再使用UNet了!
2、關于Mirror

Mirror是Unity的高級網路 API,支持不同的低級傳輸(UDP、TCP、KCP等等),
使用 Mirror,客戶端、服務端是在同一個工程中的,這就是為什么它叫Mirror,也就是說它沒有一個獨立的服務端,而是由一臺客戶端作為Host,它既是客戶端又是服務端,其他客戶端連接這臺Host客戶端,畫成圖是這樣子:

Mirror是開源的,它的社區很活躍,配套的檔案也很詳盡,大家可以從官網進行學習,不過是全英文的,
Mirror官網:
https://mirror-networking.com/
Mirror GitHub:
https://github.com/vis2k/Mirror
Mirror Asset Store:
https://assetstore.unity.com/packages/tools/network/mirror-129321
Mirror 官方檔案:
https://mirror-networking.gitbook.io/docs/
Mirror API手冊:
https://mirror-networking.com/docs/api/Mirror.html
Unity 與 Mirror的兼容:
Mirror最適合Unity 2019 LTS,
Mirror通常也適用于所有較新的LTS版本(即2020 LTS),
3、Mirror插件下載
建議從Asset Store上下載Mirror版本,因為GitHub的版本不一定穩定,
Asset Store地址:
https://assetstore.unity.com/packages/tools/network/mirror-129321

將Mirror插件添加到自己的賬號中,然后回到Unity,在Package Manager中就可以下載了,

下載下來匯入Unity中,

4、Mirror 案例測驗:多人坦克對戰
Mirror中給我們提供了幾個例子,

我以多人坦克對戰為例,雙擊Assets / Mirror / Examples / Tanks / Scenes/ Scene進入場景,

運行后左上角出現三個按鈕,如下

要開啟兩個客戶端,為了方便演示,我先打出個exe,

打包成功后,運行兩個客戶端,其中一個作為Host,另一個客戶端連接Host,運行效果如下:

可以看到我們對坦克的控制是實時同步到另一個端的,
5、Mirror 案例講解:多人坦克對戰
下面,我以多人坦克對戰案例為例,給大家講下制作程序,
為了讓大家有個直觀理解,我畫個圖:

5.1、NetworkManager物體
先創建一個空物體,重命名為NetworkManager,掛以下三個腳本:
NetworkManager、NetworkManagerHUD、KcpTransport,

5.1.1、NetworkManager組件
我們先看下官方手冊:https://mirror-networking.gitbook.io/docs/components/network-manager

意思就是,NetworkManager是管理多個客戶端連接的組件,它是多人聯機游戲的核心控制組件,
一個場景中只能有一個激活的NetworkManager(它是單例模式的),
連接的服務端IP地址在NetworkManager中進行設定,Max Connections是最大連接數,
(注意:任何一個客戶端都可以同時是一個服務端)

5.1.2、NetworkManagerHUD組件
NetworkManagerHUD組件是下面這個GUI的邏輯,通過它我們可以方便地進行測驗,

5.1.3、KcpTransport組件
Mirror幫我們封裝了各種不同等級的傳輸協議(各種Transport組件),常用的是KcpTransport和TelepathyTransport,
KcpTransport是使用可靠UDP協議,TelepathyTransport是使用TCP協議,

Transport組件中可以設定埠號、最大延遲等等引數:

5.2、地面(帶導航功能)
5.2.1、創建Plane
創建一個Plane作為地面地面,重命名為Ground,給它賦值一個材質球,

效果如下:

5.2.2、導航烘焙:Navigation
接下來我們對地面執行導航系統烘焙,這樣方便限制坦克的活動范圍,
我們將地面設定為靜態物件,

點擊選單Window / AI / Navigation,打開Navigation(導航/尋路系統)視圖,

在Navigation視圖中點擊Bake標簽按鈕,點擊Bake按鈕,對地面進行導航烘焙,

看到藍色網格則說明烘焙成功,

5.3、坦克生成點:NewworkStartPosition
創建四個空物體,重名命為Spawn,掛上NewworkStartPosition,
注:如果不創建生成點,則坦克默認在
(0, 0, 0)坐標點出生成,

調節四個生成點的位置,分散在地面的四個角落,如下

5.4、坦克身上的組件
5.4.1、坦克預設
準備一個坦克模型,

包裝成坦克預設:Tank.prefab,

坦克預設上掛以下腳本:

5.4.2、NavMeshAgent組件
NavMeshAgent組件是導航代理組件,掛上這個組件就具備了導航功能;
關于導航系統的使用,可以參見我之前寫的文章:《Unity游戲開發——新發教你做游戲(五):導航系統Navigation》

《[原創] 用Unity等比例制作廣州地鐵,廣州加油,早日戰勝疫情(Unity | 地鐵地圖 | 第三人稱視角)》

5.4.4、Animator組件
影片控制器,用于控制坦克的行駛、開炮等影片,

關于Animator相關的教程,我之前寫過兩篇文章:《Unity影片狀態機Animator使用》、
《Animator控制角色影片播放》,感興趣的同學可以看看,
5.4.5、NetworkTransform組件
我們先看下官方手冊:https://mirror-networking.gitbook.io/docs/components/network-transform

意思就是說,NetworkTransform組件會通過網路自動同步position、rotation和scale,
帶NetworkTransform組件的物體必須也帶NetworkIdentity組件,

我們可以設定Positon、Rotation、Scale同步的敏感度,

為了讓同步有一個平滑效果(不會一卡一卡的),我們可以勾選平滑差值,

5.4.6、NetworkIdentity組件
我們先看下官方手冊:https://mirror-networking.gitbook.io/docs/components/network-identity

意思就是說,NetworkIdentity組件提供了游戲物體在網路中的唯一標識(ID),
游戲運行程序中,我們在Inspector視圖中預覽到NetworkIdentity的資訊,

5.4.7、NetworkBehaviour組件: Tank
Tank腳本是坦克行為腳本,它繼承NetworkBehaviour,
這里只講NetworkBehaviour組件,Tank具體代碼后面再講~
我們先看看官方手冊:https://mirror-networking.gitbook.io/docs/guides/networkbehaviour

意思就是說,NetworkBehaviour腳本處理具有NetworkIdentity組件的游戲物件,NetworkBehaviour的子類中可以處理高級API功能,例如Commands、ClientRpc's、SyncEvents、SyncVars,
NetworkBehaviour組件具有以下功能:
Synchronized variables:同步變數
Network callbacks:網路回呼
Server and client functions:服務端和客戶端函式
Sending commands:發送命令
Client RPC calls:客戶端遠程程序呼叫
Networked events:網路事件
NetworkBehaviour提供了一些 網路回呼:
OnStartServer回呼
這個回呼函式只在服務端呼叫,當在服務端生成一個游戲物件,或者服務端啟動時被回呼,
OnStopServer回呼
這個回呼函式只在服務端呼叫,當在服務端銷毀一個游戲物件,或者服務端停止時被回呼,
OnStartClient回呼
這個回呼函式只在客戶端呼叫,當客戶端生成一個游戲物件,或者客戶端連接到服務端時被回呼,
OnStopClient回呼
這個回呼函式只在客戶端呼叫,當服務端銷毀一個游戲物件時被回呼,
OnStartLocalPlayer回呼
這個回呼函式只在客戶端呼叫,當客戶端生成一個玩家物件時被回呼,
OnStartAuthority回呼
這個回呼函式只在客戶端呼叫,當游戲物件拿到控制權時,
OnStopAuthority回呼
這個回呼函式只在客戶端呼叫,當游戲物件失去控制權時,
標記服務端函式或客戶端函式:
在NetworkBehaviour中,我們可以使用[Server]、[ServerCallback]、[Client]、[ClientCallback]這些注解對函式進行標注,
[Server]、[ServerCallback]表示函式為服務端函式,只在服務端執行;
[Client]、[ClientCallback]表示為客戶端函式,只在客戶端執行,
Command 命令:
使用[Command]注解對函式進行標記,表示這個函式是由客戶端呼叫,由服務端來執行,具體原理我下文會通過反編譯dll來解釋,
被[Command]標記的函式約定以Cmd開頭,
Client RPC 客戶端遠程程序呼叫:
使用[ClientRpc]注解對函式進行標記,表示這個函式是由服務端呼叫,由客戶端來執行,具體原理我下文會通過反編譯dll來解釋,
被[ClientRpc]標記的函式約定以Rpc開頭,
Networked Events 網路事件(觀察者模式):
類似于Client RPC呼叫,不同之處是它觸發的是事件,
使用[SyncEvent]對事件進行標記,被[SyncEvent]標記的事件變數必須以Event開頭,例EventTakeDamage,例子可以參見官方手冊:https://mirror-networking.gitbook.io/docs/guides/synchronization/syncevent
Mirror提供的函式注解如下(部分注解我們上面已做了介紹),具體的注解可以參見Mirror官方手冊:https://mirror-networking.gitbook.io/docs/guides/attributes

5.5、賦值PlayerPrefab
選中NetworkManager物體,給NetworkManager組件賦值PlayerPrefab為坦克預設,

5.6、炮彈預設
準備一個炮彈模型,

包裝成炮彈預設:Projectile.prefab,

炮彈預設上掛以下腳本:

NetworkIdentity:因為炮彈也是一個網路物件,所以它需要NetworkIdentity組件;
炮彈的Transform資訊不使用NetworkTransform進行同步,而是通過Rigibody剛體組件的力來使炮彈飛行,所以只需要同步一下力即可,在Projectile腳本中實作炮彈的邏輯,
5.7、坦克腳本:Tank.cs
網路物件的行為腳本需要繼承NetworkBehaviour,所以Tank類需要繼承NetworkBehaviour,
public class Tank : NetworkBehaviour
{
}
Tank腳本要實作的邏輯是坦克的 移動 / 旋轉 、開炮,
其中移動的同步會自動通過NetworkTransform進行同步,所以我們只需對本地坦克進行控制即可,
// Tank.cs
void Update()
{
// isLocalPlayer是父類NetworkBehaviour的屬性,用于判斷當前NetworkBehaviour物件是否為本地物件;
if (!isLocalPlayer) return;
// 旋轉
float horizontal = Input.GetAxis("Horizontal");
transform.Rotate(0, horizontal * rotationSpeed * Time.deltaTime, 0);
// 移動
float vertical = Input.GetAxis("Vertical");
Vector3 forward = transform.TransformDirection(Vector3.forward);
agent.velocity = forward * Mathf.Max(vertical, 0) * agent.speed;
animator.SetBool("Moving", agent.velocity != Vector3.zero);
// ...
}
開炮需要由服務端來執行,
// Tank.cs
void Update()
{
// ...
if (Input.GetKeyDown(shootKey))
{
CmdFire();
}
}
// this is called on the server
[Command]
void CmdFire()
{
GameObject projectile = Instantiate(projectilePrefab, projectileMount.position, transform.rotation);
NetworkServer.Spawn(projectile);
RpcOnFire();
}
// this is called on the tank that fired for all observers
[ClientRpc]
void RpcOnFire()
{
animator.SetTrigger("Shoot");
}
這里用到了兩個注解[Command]、[ClientRpc],我們上面講到它是NetworkBehaviour組件的函式注解,
上面我們講到[Command],它是由客戶端來呼叫,由服務端來執行,
這個怎么理解呢?
事實上Mirror實作了一些編譯器hack,會在編譯階段動態生成特定的代碼(也就是把你的代碼編譯為別的代碼),
這樣講好像不好理解,沒事,我們反編譯一下C#的dll就知道了,
進入工程路徑 / Library / ScriptAssemblies這個目錄,Mirror的案例代碼是編譯在Mirror.Examples.dll中,

我們使用ILSpy.exe對它進行反編譯,
注:
ILSpy反編譯工具可以從GitHub下載:https://github.com/icsharpcode/ILSpy
我們看到反編譯出來的Tank的CmdFire函式的代碼已經完全變了另外一個邏輯了,它發送了一個“CmdFire”訊息給服務端,

開炮流程變成了下面這樣子:

同理,[ClientRpc]是由服務端呼叫,由客戶端執行,
我們的代碼:

編譯后:


完整的Tank.cs代碼入下:
using UnityEngine;
using UnityEngine.AI;
namespace Mirror.Examples.Tanks
{
public class Tank : NetworkBehaviour
{
[Header("Components")]
public NavMeshAgent agent;
public Animator animator;
[Header("Movement")]
public float rotationSpeed = 100;
[Header("Firing")]
public KeyCode shootKey = KeyCode.Space;
public GameObject projectilePrefab;
public Transform projectileMount;
void Update()
{
// movement for local player
if (!isLocalPlayer) return;
// rotate
float horizontal = Input.GetAxis("Horizontal");
transform.Rotate(0, horizontal * rotationSpeed * Time.deltaTime, 0);
// move
float vertical = Input.GetAxis("Vertical");
Vector3 forward = transform.TransformDirection(Vector3.forward);
agent.velocity = forward * Mathf.Max(vertical, 0) * agent.speed;
animator.SetBool("Moving", agent.velocity != Vector3.zero);
// shoot
if (Input.GetKeyDown(shootKey))
{
CmdFire();
}
}
// this is called on the server
[Command]
void CmdFire()
{
GameObject projectile = Instantiate(projectilePrefab, projectileMount.position, transform.rotation);
NetworkServer.Spawn(projectile);
RpcOnFire();
}
// this is called on the tank that fired for all observers
[ClientRpc]
void RpcOnFire()
{
animator.SetTrigger("Shoot");
}
}
}
5.8、Transform的網路同步:NetworkTransform.cs
坦克身上掛NetworkTransform組件,坦克Transform的同步由它來負責,
5.9、炮彈腳本:Projectile.cs
炮彈也是一個網路物件,它的行為腳本也必須繼承NetworkBehaviour,
// Projectile.cs
public class Projectile : NetworkBehaviour
{
}
炮彈預設實體化后,需要給Rigibody一個力,從而讓炮彈向前飛行,
// Projectile.cs
void Start()
{
rigidBody.AddForce(transform.forward * force);
}
炮彈需要有一個生命周期控制,超過5秒自動銷毀,執行NetworkServer.Destroy(gameObject)來銷毀物件,
// Projectile.cs
public override void OnStartServer()
{
Invoke(nameof(DestroySelf), destroyAfter);
}
[Server]
void DestroySelf()
{
NetworkServer.Destroy(gameObject);
}
我們看到這里有一個[Server]注解,它表示只有服務端可以呼叫此函式,
我們反編譯可以看到它自動加了一個NetworkServer.active判斷,

我們再看[ServerCallback],它與[Server]一樣,只能在服務端呼叫,只是沒有Warning輸出而已,如下

編譯后:

完整的Projectile.cs代碼如下:
using UnityEngine;
namespace Mirror.Examples.Tanks
{
public class Projectile : NetworkBehaviour
{
public float destroyAfter = 5;
public Rigidbody rigidBody;
public float force = 1000;
public override void OnStartServer()
{
Invoke(nameof(DestroySelf), destroyAfter);
}
// set velocity for server and client. this way we don't have to sync the
// position, because both the server and the client simulate it.
void Start()
{
rigidBody.AddForce(transform.forward * force);
}
// destroy for everyone on the server
[Server]
void DestroySelf()
{
NetworkServer.Destroy(gameObject);
}
// ServerCallback because we don't want a warning if OnTriggerEnter is
// called on the client
[ServerCallback]
void OnTriggerEnter(Collider co)
{
NetworkServer.Destroy(gameObject);
}
}
}
四、完畢
好了,就先寫這么多吧~
最后再補充一個,我之前寫了一篇關于UnityWebRequest的文章,它也與網路通信相關,大家感興趣的也可以看下:《長江后浪推前浪,UnityWebRequest替代WWW》
我是新發,喜歡我的可以點贊、關注、收藏,如果有什么技術上的疑問,歡迎留言或私信,拜拜~
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/289693.html
標籤:其他

