Unity3D本身是用来做客户端的通用游戏引擎, 要建立网络连接的话, 其实需要使用的是C#本身的网络和线程模块, 即System.Net.Sockets & System.Threading. 本文中我做了一个简单的例子, 适合那些需要做Unity客户端连接服务器功能的人入门.
整体项目
客户端: 我做的项目主要是一个简单的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 dict = new Dictionary()
{
{"code", "exit"}
};
SendData(Encode(dict)); // 退出的时候先发一个退出的信号给服务器, 使得连接被正确关闭
Debug.Log("exit sent!");
CloseConnection ();
}
// --------------------public--------------------
public void Login() {
SetupConnection();
Dictionary dict = new Dictionary()
{
{"code", "login"},
{"username", username},
{"password", password}
};
SendData(Encode(dict));
Debug.Log("start!");
}
public void SendGameData(int score, int health) {
Dictionary dict = new Dictionary()
{
{"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 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 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>(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();
}
// 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();
}
}
后续持续开发优化建议