Unity如何連線伺服器: 一個簡單的例子

weixin_34120274發表於2018-05-20

Unity3D本身是用來做客戶端的通用遊戲引擎, 要建立網路連線的話, 其實需要使用的是C#本身的網路和執行緒模組, 即System.Net.Sockets & System.Threading. 本文中我做了一個簡單的例子, 適合那些需要做Unity客戶端連線伺服器功能的人入門.

整體專案

客戶端專案地址: https://share.weiyun.com/5M9jp6c
伺服器專案下載: https://share.weiyun.com/5TMCQYP

客戶端: 我做的專案主要是一個簡單的Demo, 畫面上只有三個按鈕和兩個輸入框, 通過點選按鈕可以實現相應的操作.

服務端: 服務端是一個Python寫的伺服器. 這個部分不是我本文的重點, 大家可以參考別的網上文章, 瞭解如何寫一個C++, Python或者Java伺服器, 無論什麼語言寫的伺服器都是可以與Unity進行互動的.

2870635-48667c923b7a2112.png
Unity Network Demo
2870635-8841bab05922abf4.png
login點選後, console上顯示了發出的訊息

2870635-650b32755bd5eeae.png
server顯示成功登陸

下載專案後, 使用Unity匯入, 可以看到Scripts資料夾中有六個指令碼, 其中NetworkCore和UIManager是主要的指令碼, Json開頭的指令碼不是重點, 他們只是Json編碼解碼相關的一個庫(文中我是直接使用的https://github.com/gering/Tiny-JSON這個老外寫的純C#版本Json Parser), Json的編碼和解析也不是本文重點, 只要找到一個庫能用即可.

後續補充: Json的工具庫現在推薦使用Newtonsoft出品的json.NET. 下載地址https://github.com/JamesNK/Newtonsoft.Json/releases, 在Unity2018.1中, 請使用其中的Bin\net20\Newtonsoft.Json.dll這個大小513KB的DLL(此處我也在微雲存了一個供大家快速下載https://share.weiyun.com/5pky2k3), 由於Unity2018用的還是.NET2.0版本, 因此要用老的.

2870635-4a2d751ab260f315.png
指令碼一覽

學習步驟

下載客戶端和服務端, 執行起來. 之後主要學習NetworkCore.cs和UIManager.cs這兩個指令碼的內容(兩個指令碼並不複雜), 最關鍵的部分是如何建立連線, 建立後臺執行緒, 傳送和接收資料, 以及Json相關的字典操作.

指令碼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網路程式設計中的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));  // 退出的時候先發一個退出的訊號給伺服器, 使得連線被正確關閉
        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);  // 傳入函式ReceiveData作為thread的任務
            _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() {  // 這個函式被後臺執行緒執行, 不斷地在while迴圈中跑著
        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 ();  // 這個其實是多餘的, 因為isConnected = false後, 執行緒while條件為假自動停止
            _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, 注意要解決粘包的問題, 這個程式寫法同GameLobby中的相應模組一模一樣
    // 參考 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 關鍵字用於在程式中包含名稱空間。一個程式可以包含多個 using 語句。

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

後續持續開發優化建議

Unity客戶端網路應該是使用佇列模式(生產者消費者), 可以參見我的SurvivalShooterServer中客戶端的NetworkMaster的程式碼https://github.com/imcheney/SurvivalShooterServer/blob/master/client/Scripts/Network/NetworkMaster.cs

相關文章