Unity3D的网络通讯 - 对接RoyNet的聊天室例子
作者:肥爪 文章来源:http://taik.io/99
在之前的文章中提到,Unity3D(简称u3d)可以使用WWW来进行HTTP通讯,但本文使用WebRequest(HttpWebRequest)实现网络通讯。本次示例将创建三个场景:登录场景loginScene、服务器选择场景servScene和游戏主场景mainScene。
一、创建登录场景loginScene
登录场景界面包含三个元素:用户名输入框、密码输入框和登录按钮(暂不开放注册功能)。
实现步骤
- 创建一个名为
LoginHandle的类,并将其挂载到登录按钮上。同时,将两个输入框作为成员字段进行引用。 - 在
Start函数中,为登录按钮的onClick事件添加监听回调。
代码示例
public InputField InputUserName;
public InputField InputPassword;
private Button _button;
void Start()
{
_button = GetComponent<Button>();
_button.onClick.AddListener(OnClick);
}
private void OnClick()
{
string username = InputUserName.text;
string password = InputPassword.text;
WebRequest request = WebRequest.Create(new Uri(string.Format("http://123.56.119.97:8080/login/{0}/{1}", username, password)));
using (WebResponse response = request.GetResponse())
{
using (Stream stream = response.GetResponseStream())
{
byte[] buffer = new byte[1024];
if (stream != null && stream.CanRead)
{
int ss = stream.Read(buffer, 0, buffer.Length);
string responseMsg = Encoding.UTF8.GetString(buffer, 0, ss);
JsonData jobject = JsonMapper.ToObject(responseMsg);
if (jobject["result"].ToString() == "OK")
{
GameManager.Instance.Load(jobject);
Application.LoadLevelAsync("servScene");
}
else
{
//todo: 登录失败
}
}
}
}
}
代码解释
由于服务器返回的是JSON数据,因此使用了LitJson这个JSON解析类库。解析之后,初始化GameManager,它是一个单例,在游戏运行过程中不会被销毁。登录成功后,加载servScene场景。
二、服务器选择界面的制作
服务器选择界面主要使用UGUI的GridLayoutGroup表格组件,设置列数为2,布局方式为靠左靠上。
实现步骤
- 编写一个名为
ServerListView的脚本,用于创建服务器列表对应的按钮,并将其添加到Grid中。 - 将服务器关联的
Button制作成一个预制体(Prefab),并进行拖放关联。 - 为按钮添加点击处理函数,实现进入指定服务器的功能。
代码示例
public class ServerListView : MonoBehaviour
{
public GameObject ServerItemPrefab;
void Start()
{
for (int i = 0; i < GameManager.Instance.ServerList.Count; i++)
{
var server = GameManager.Instance.ServerList[i];
var serverObj = GameObject.Instantiate(ServerItemPrefab);
serverObj.transform.SetParent(this.transform);
var btnText = serverObj.transform.GetChild(0).GetComponent<Text>();
btnText.text = server.Name;
serverObj.GetComponent<Button>().onClick.AddListener(() =>
{
Debug.Log("进入" + server.Name);
GameManager.Instance.Enter(server);
});
}
}
}
进入服务器的代码实现
if (_stream == null || !_stream.CanWrite)
{
_client = new TcpClient();
_client.Connect(server.IP, server.Port);
_stream = _client.GetStream();
BeginRece(_stream);
}
var converter = EndianBitConverter.Big;
byte[] body = Encoding.UTF8.GetBytes(_token);
int length = body.Length;
byte[] data = new byte[length + 10];
int offset = 0;
data[3] = 0x01; //cmd
offset += 4;
converter.CopyBytes((ushort)(length + 4), data, offset);
offset += 2;
converter.CopyBytes((int)CMD_Chat.Send, data, offset);
offset += 4;
Buffer.BlockCopy(body, 0, data, offset, length);
_stream.Write(data, 0, data.Length);
代码解释
服务器在收到数据并确认后,会返回玩家的游戏存档数据。由于这部分数据可能较多,且进入游戏主界面可能需要较长的加载时间,因此采用异步加载场景并同时播放Loading动画的方式。
三、报文的接收和任务派发
报文接收代码
void BeginRece(NetworkStream stream)
{
byte[] recedata = new byte[1024];
stream.BeginRead(recedata, 0, recedata.Length, (a) =>
{
int receLength = stream.EndRead(a);
if (receLength == 0)
{
//Log("服务器关闭了连接。可能是顶号。");
Debug.Log("服务器关闭了连接。可能是顶号。");
_client.Close();
}
else if (receLength == 1)
{
//Log("登录成功!");
ActionQueue.Enqueue(() =>
{
Application.LoadLevelAsync("mainScene");
});
BeginRece(stream);
}
else
{
var converter = EndianBitConverter.Big;
int offset = 0;
while (offset < receLength + 2)
{
int length = converter.ToInt16(recedata, offset);
offset += 2;
int cmd = converter.ToInt32(recedata, offset);
offset += 4;
lock (_syncCmd)
{
ICommand recMsg;
if (_commands.TryGetValue(cmd, out recMsg))
{
using (var receMs = new MemoryStream())
{
receMs.Write(recedata, offset, length - 4);
receMs.Position = 0;
var package = recMsg.DeserializePackage(receMs);
ActionQueue.Enqueue(() =>
{
recMsg.Execute(package);
});
//Debug.Log(package.Text);
offset += length;
}
}
}
}
BeginRece(stream);
}
}, null);
}
代码解释
在接收代码中,使用了commands字典和命令模式进行任务派发。该字典保存了所有可接收的报文和对应的处理器Command,其中存储的是实现了ICommand接口的Command实例。GameManager提供了一个注册方法RegisterCommand,用于注册Command。
注册命令的代码
public void RegisterCommand(ICommand command)
{
lock (_syncCmd)
{
_commands.Add(command.Name, command);
}
}
代码解释
本来应该创建一个单独的线程来持续接收报文,但这里使用了BeginRead方法,它是异步的,内部通过线程池实现,不会阻塞主UI线程。为了缓存接收到的数据,使用了一个队列ActionQueue,并注意了线程安全问题,通过lock关键字进行同步。
队列的实现代码
public class ConcurrentQueue<T>
{
private readonly Queue<T> _queue = new Queue<T>();
private readonly object _syncObject = new object();
public void Enqueue(T item)
{
lock (_syncObject)
{
_queue.Enqueue(item);
}
}
public bool TryDequeue(out T item)
{
lock (_syncObject)
{
if (_queue.Count > 0)
{
item = _queue.Dequeue();
return true;
}
item = default(T);
return false;
}
}
public int Count
{
get
{
lock (_syncObject)
{
return _queue.Count;
}
}
}
}
代码解释
队列中存放的是Action,例如收到进入游戏确认报文后,Action就是加载mainScene场景的方法。
四、聊天室的示例
聊天界面由一个Text组件用于输出聊天信息,一个InputField组件用于输入聊天内容,以及一个Button组件用于发送消息。
实现步骤
- 创建一个名为
ChatHandle的脚本,并挂载到一个空的GameObject上。 - 在
Start函数中,为发送按钮的onClick事件添加监听回调,并注册ChatCommand到GameManager。 - 实现
SendChat方法,用于发送聊天消息。 - 在
Update函数中,监听回车键事件,实现按回车键发送消息的功能。
代码示例
public class ChatHandle : MonoBehaviour
{
public Button ButtonSendChat;
public InputField InputFieldChat;
public Text OutputText;
void Start()
{
ButtonSendChat.onClick.AddListener(SendChat);
GameManager.Instance.RegisterCommand(new ChatCommand(e =>
{
OutputText.text += Environment.NewLine + e.Text;
}));
}
void SendChat()
{
GameManager.Instance.Send((int)CMD_Chat.Send, new Chat_Send() { Text = InputFieldChat.text });
InputFieldChat.text = "";
}
void Update()
{
if (Input.GetKeyDown(KeyCode.KeypadEnter) || Input.GetKeyDown(KeyCode.Return))
{
SendChat();
}
}
}
命令接口和基类的定义
public interface ICommand
{
int Name { get; }
void Execute(object package);
object DeserializePackage(MemoryStream ms);
}
public abstract class Command<T> : ICommand where T : class, global::ProtoBuf.IExtensible
{
public abstract int Name { get; }
public void Execute(object package)
{
_onExecute(package as T);
}
private readonly Action<T> _onExecute;
public object DeserializePackage(MemoryStream ms)
{
return Serializer.Deserialize<T>(ms);
}
public Command(Action<T> onExecute)
{
_onExecute = onExecute;
}
}
public class ChatCommand : Command<Chat_Send>
{
public override int Name
{
get { return (int)CMD_Chat.Send; }
}
public ChatCommand(Action<Chat_Send> onExecute) : base(onExecute)
{
}
}
代码解释
CommandBase需要一个委托作为参数,这里使用了lambda表达式,利用其闭包特性,将UI的引用带入,从而将聊天信息显示到Text组件上。
代码获取
全部的代码可以在https://github.com/Roytin/Hero7获取。
注意事项
本文使用的是Unity 5。