Unity如何连接服务器: 一个简单的例子

Unity3D本身是用来做客户端的通用游戏引擎, 要建立网络连接的话, 其实需要使用的是C#本身的网络和线程模块, 即System.Net.Sockets & System.Threading. 本文中我做了一个简单的例子, 适合那些需要做Unity客户端连接服务器功能的人入门.

整体项目

分享地址已经更新
客户端项目地址: https://share.weiyun.com/5M9jp6c
服务器项目下载: https://share.weiyun.com/5TMCQYP

客户端: 我做的项目主要是一个简单的Demo, 画面上只有三个按钮和两个输入框, 通过点击按钮可以实现相应的操作.

服务端: 服务端是一个Python写的服务器. 这个部分不是我本文的重点, 大家可以参考别的网上文章, 了解如何写一个C++, Python或者Java服务器, 无论什么语言写的服务器都是可以与Unity进行交互的.

Unity Network Demo
login点击后, console上显示了发出的消息

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版本, 因此要用老的.

脚本一览

学习步骤

下载客户端和服务端, 运行起来. 之后主要学习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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,475评论 4 372
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,744评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,101评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,732评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,141评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,049评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,188评论 2 320
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,965评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,716评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,867评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,341评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,663评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,376评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,200评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,990评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,179评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,979评论 2 279

推荐阅读更多精彩内容

  • Unity UI架构设计理念 1.以ARPG为例,多个场景会反复出现相同的“UI窗体”,造成多个场景中反复加载相同...
    Magic_Dong阅读 14,759评论 2 29
  • 周末两天又捡起了以前粗略接触过的Photon服务器,当时只是学会了怎么用PUN插件现学现卖一个远程共享操作,对原理...
    晓梦蝉君阅读 15,020评论 5 35
  • 洪流学堂,让你快人几步!你好,我是你的技术探路者郑洪智,你可以叫我大智(vx: zhz11235)。 本节课,我们...
    洪智阅读 12,738评论 3 7
  • 我是颖王爷,我和朋友公子胡吃在一个古镇上开了一家叫“食不语”的小店,专做美食,也讲故事。我们想给每一道料理写一个故...
    切花换酒食不语阅读 581评论 10 12
  • 第 3 章关于能力和成就的真相 3.1思维模式和成绩 3.1.1具有固定型思维模式的学生在面对艰难的转折期时,视其...
    杨秀兵阅读 346评论 0 0