Unity如何連接服務(wù)器: 一個(gè)簡(jiǎn)單的例子

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)行交互的.

Unity Network Demo
login點(diǎn)擊后, console上顯示了發(fā)出的消息

server顯示成功登陸

下載項(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Unity UI架構(gòu)設(shè)計(jì)理念 1.以ARPG為例,多個(gè)場(chǎng)景會(huì)反復(fù)出現(xiàn)相同的“UI窗體”,造成多個(gè)場(chǎng)景中反復(fù)加載相同...
    Magic_Dong閱讀 15,368評(píng)論 2 29
  • 周末兩天又撿起了以前粗略接觸過(guò)的Photon服務(wù)器,當(dāng)時(shí)只是學(xué)會(huì)了怎么用PUN插件現(xiàn)學(xué)現(xiàn)賣一個(gè)遠(yuǎn)程共享操作,對(duì)原理...
    曉夢(mèng)蟬君閱讀 15,864評(píng)論 5 35
  • 洪流學(xué)堂,讓你快人幾步!你好,我是你的技術(shù)探路者鄭洪智,你可以叫我大智(vx: zhz11235)。 本節(jié)課,我們...
    洪智閱讀 13,030評(píng)論 3 7
  • 我是穎王爺,我和朋友公子胡吃在一個(gè)古鎮(zhèn)上開(kāi)了一家叫“食不語(yǔ)”的小店,專做美食,也講故事。我們想給每一道料理寫(xiě)一個(gè)故...
    切花換酒食不語(yǔ)閱讀 688評(píng)論 10 12
  • 第 3 章關(guān)于能力和成就的真相 3.1思維模式和成績(jī) 3.1.1具有固定型思維模式的學(xué)生在面對(duì)艱難的轉(zhuǎn)折期時(shí),視其...
    楊秀兵閱讀 451評(píng)論 0 0

友情鏈接更多精彩內(nèi)容