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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

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