Unity3D本身是用來(lái)做客戶端的通用游戲引擎, 要建立網(wǎng)絡(luò)連接的話, 其實(shí)需要使用的是C#本身的網(wǎng)絡(luò)和線程模塊, 即System.Net.Sockets & System.Threading. 本文中我做了一個(gè)簡(jiǎn)單的例子, 適合那些需要做Unity客戶端連接服務(wù)器功能的人入門.
整體項(xiàng)目
分享地址已經(jīng)更新
客戶端項(xiàng)目地址: https://share.weiyun.com/5M9jp6c
服務(wù)器項(xiàng)目下載: https://share.weiyun.com/5TMCQYP
客戶端: 我做的項(xiàng)目主要是一個(gè)簡(jiǎn)單的Demo, 畫(huà)面上只有三個(gè)按鈕和兩個(gè)輸入框, 通過(guò)點(diǎn)擊按鈕可以實(shí)現(xiàn)相應(yīng)的操作.
服務(wù)端: 服務(wù)端是一個(gè)Python寫(xiě)的服務(wù)器. 這個(gè)部分不是我本文的重點(diǎn), 大家可以參考別的網(wǎng)上文章, 了解如何寫(xiě)一個(gè)C++, Python或者Java服務(wù)器, 無(wú)論什么語(yǔ)言寫(xiě)的服務(wù)器都是可以與Unity進(jìn)行交互的.



下載項(xiàng)目后, 使用Unity導(dǎo)入, 可以看到Scripts文件夾中有六個(gè)腳本, 其中NetworkCore和UIManager是主要的腳本, Json開(kāi)頭的腳本不是重點(diǎn), 他們只是Json編碼解碼相關(guān)的一個(gè)庫(kù)(文中我是直接使用的https://github.com/gering/Tiny-JSON這個(gè)老外寫(xiě)的純C#版本Json Parser), Json的編碼和解析也不是本文重點(diǎn), 只要找到一個(gè)庫(kù)能用即可.
后續(xù)補(bǔ)充: Json的工具庫(kù)現(xiàn)在推薦使用Newtonsoft出品的json.NET. 下載地址https://github.com/JamesNK/Newtonsoft.Json/releases, 在Unity2018.1中, 請(qǐng)使用其中的Bin\net20\Newtonsoft.Json.dll這個(gè)大小513KB的DLL(此處我也在微云存了一個(gè)供大家快速下載https://share.weiyun.com/5pky2k3), 由于Unity2018用的還是.NET2.0版本, 因此要用老的.

學(xué)習(xí)步驟
下載客戶端和服務(wù)端, 運(yùn)行起來(lái). 之后主要學(xué)習(xí)NetworkCore.cs和UIManager.cs這兩個(gè)腳本的內(nèi)容(兩個(gè)腳本并不復(fù)雜), 最關(guān)鍵的部分是如何建立連接, 建立后臺(tái)線程, 發(fā)送和接收數(shù)據(jù), 以及Json相關(guān)的字典操作.
腳本1: NetworkCore.cs
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;
using Tiny;
public class NetworkCore : MonoBehaviour {
public string serverAddress = "127.0.0.1";
public int serverPort = 5000;
public string username = "chen";
public string password = "123";
private TcpClient _client;
private NetworkStream _stream; // C#中采用NetworkStream的方式, 可以類比于python網(wǎng)絡(luò)編程中的socket
private Thread _thread;
private byte[] _buffer = new byte[1024]; // 接收消息的buffer
private string receiveMsg = "";
private bool isConnected = false;
void Start() {
}
public void OnApplicationQuit() {
Dictionary<string, string> dict = new Dictionary<string, string>()
{
{"code", "exit"}
};
SendData(Encode(dict)); // 退出的時(shí)候先發(fā)一個(gè)退出的信號(hào)給服務(wù)器, 使得連接被正確關(guān)閉
Debug.Log("exit sent!");
CloseConnection ();
}
// --------------------public--------------------
public void Login() {
SetupConnection();
Dictionary<string, string> dict = new Dictionary<string, string>()
{
{"code", "login"},
{"username", username},
{"password", password}
};
SendData(Encode(dict));
Debug.Log("start!");
}
public void SendGameData(int score, int health) {
Dictionary<string, string> dict = new Dictionary<string, string>()
{
{"code", "gds"},
{"score", score.ToString()},
{"health", health.ToString()}
};
SendData(Encode(dict));
}
// -----------------------private---------------------
private void SetupConnection() {
try {
_thread = new Thread(ReceiveData); // 傳入函數(shù)ReceiveData作為thread的任務(wù)
_thread.IsBackground = true;
_client = new TcpClient(serverAddress, serverPort);
_stream = _client.GetStream();
_thread.Start(); // background thread starts working while loop
isConnected = true;
} catch (Exception e) {
Debug.Log (e.ToString());
CloseConnection ();
}
}
private void ReceiveData() { // 這個(gè)函數(shù)被后臺(tái)線程執(zhí)行, 不斷地在while循環(huán)中跑著
Debug.Log ("Entered ReceiveData function...");
if (!isConnected) // stop the thread
return;
int numberOfBytesRead = 0;
while (isConnected && _stream.CanRead) {
try {
numberOfBytesRead = _stream.Read(_buffer, 0, _buffer.Length);
receiveMsg = Encoding.ASCII.GetString(_buffer, 0, numberOfBytesRead);
_stream.Flush();
Debug.Log(receiveMsg);
receiveMsg = "";
} catch (Exception e) {
Debug.Log (e.ToString ());
CloseConnection ();
}
}
}
private void SendData(String msgToSend)
{
byte[] bytesToSend = Encoding.ASCII.GetBytes(msgToSend);
if (_stream.CanWrite)
{
_stream.Write(bytesToSend, 0, bytesToSend.Length);
}
}
private void CloseConnection() {
if (isConnected) {
_thread.Interrupt (); // 這個(gè)其實(shí)是多余的, 因?yàn)閕sConnected = false后, 線程while條件為假自動(dòng)停止
_stream.Close ();
_client.Close ();
isConnected = false;
receiveMsg = "";
}
}
// ---------------------util----------------------
// encode dict to to json and wrap it with \r\n as delimiter
string Encode(Dictionary<string, string> dict)
{
string json = Json.Encode(dict);
string header = "\r\n" + json.Length.ToString() + "\r\n";
string result = header + json;
Debug.Log("encode result:" + result);
return result;
}
// decode data, 注意要解決粘包的問(wèn)題, 這個(gè)程序?qū)懛ㄍ珿ameLobby中的相應(yīng)模塊一模一樣
// 參考 https://github.com/imcheney/GameLobby/blob/master/server/util.py
Dictionary<string, string> Decode(string raw)
{
string payload_str = "";
string raw_leftover = raw;
if (raw.Substring(0, 2).Equals("\r\n"))
{
int index = raw.IndexOf("\r\n", 2);
int payload_length = int.Parse(raw.Substring(2, index - 2 + 1)); // 注意, C#'s substring takes start and length as args
if (raw.Length >= index + 2 + payload_length)
{
payload_str = raw.Substring(index + 2, payload_length);
raw_leftover = raw.Substring(index + 2 + payload_length);
}
}
return Json.Decode<Dictionary<string, string>>(payload_str);
}
}
腳本2: UIManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; //using 關(guān)鍵字用于在程序中包含命名空間。一個(gè)程序可以包含多個(gè) using 語(yǔ)句。
public class UIManager : MonoBehaviour {
public InputField scoreInputField;
public InputField healthInputField;
NetworkCore networkCore;
// Use this for initialization
void Start () {
networkCore = GetComponent<NetworkCore>();
}
// Update is called once per frame
void Update () {
}
public void OnLoginButton() {
networkCore.Login();
}
public void OnSendButton() {
int score = int.Parse(scoreInputField.text);
int health = int.Parse(healthInputField.text);
networkCore.SendGameData(score, health);
}
public void OnQuitButton()
{
int score = int.Parse(scoreInputField.text);
int health = int.Parse(healthInputField.text);
networkCore.SendGameData(score, health);
Application.Quit();
}
}
后續(xù)持續(xù)開(kāi)發(fā)優(yōu)化建議
Unity客戶端網(wǎng)絡(luò)應(yīng)該是使用隊(duì)列模式(生產(chǎn)者消費(fèi)者), 可以參見(jiàn)我的SurvivalShooterServer中客戶端的NetworkMaster的代碼https://github.com/imcheney/SurvivalShooterServer/blob/master/client/Scripts/Network/NetworkMaster.cs