Unity教程之-Unity3d的网络通讯:对接RoyNet的聊天室例子

 

之前的文章中说unity3d可以用WWW来做http通讯。但我还是使用了WebRequest(HttpWebRequest)。本次示例创建三个Scene:登录场景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,所以我用了一个Json解析类库LitJson,解析之后初始化GameManager,这是一个单例,在游戏过程中不会被销毁。
登录成功之后加载场景servScene
二、服务器选择界面的制作
主要是一个表格组件,可以用UGUI的GridLayoutGroup,设置列数为2,靠左靠上。
我写了一个脚本,用来创建服务器列表对应的按钮并添加到Grid中:


public class ServerListView : MonoBehaviour
{
public GameObject ServerItemPrefab;

void Start()
{
for (int i = 0; i < GameManager.Instance.ServerList.Count; i++)
{
var server = GameManager.Instance.ServerList;
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);
});
}
}
}

把服务器关联的Button做成了一个Prefab,拖放关联起来。然后添加点击处理函数,进入指定的服务器。
界面出来之后差不多这样:
 

这里我给GameManager添加了一个函数Enter(),参数就是点击的服务器对象。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动画就是给这里用的~
不过确认进入是在收到那个报文时候的事,那么怎么接收报文呢?
这就是重头戏了,要接收的报文肯定不止一条,进入游戏也只是其中的一种而已。
如果看过前面RoyNet制作的文章,你可能会想到,接收是一条单独的线程,接收之后存入队列,然后还要有一个线程来消费队列中等待的数据,因为是做界面,这个消费线程必然就是UI也就是我们的主线程了。

三、报文的接收和任务派发
首先是接收,接收其实很简单,大概的代码就是这样:


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提供一个注册方法可以注册Command。


public void RegisterCommand(ICommand command)
{
lock (_syncCmd)
{
_commands.Add(command.Name, command);
}
}

本来应该是建一个单独的线程然后不停地接收的。但这里我用了BeginRead,这个方法也是异步的,内部应该是通过线程池来实现,反正不会是主UI线程。
所以,又有了一个队列,用来缓存接收到的数据。因为是多线程,还需要注意线程安全,所以我封装了下,加了个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做发送。
然后加入一个EmptyGameObject,写一个脚本,叫ChatHandle,在Start的时候注册ChatCommand到GameManager。
ChatCommand需要继承ICommand,但是我又希望ICommand在解析报文的时候能够提供对应得报文类型信息,所以又加了个Commandbase<T>来实现ICommand,而ChatCommand继承CommandBase<T>就可以了。
大概是这样:


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;
}
}

ChatCommand则是这样:


public class ChatCommand : Command<Chat_Send>
{
public override int Name
{
get { return (int) CMD_Chat.Send; }
}

public ChatCommand(Action<Chat_Send> onExecute) : base(onExecute)
{
}
}

实现ChatHandle,因为CommandBase<T>需要一个委托作为参数,用lambda实现,lambda的好处就是闭包,闭包的时候把UI的引用带进去就能把输出显示到Text上了。


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();
}
}
}

最后的效果
http://www.unitymanual.com/data/attachment/forum/201505/23/001406fm9nuu0f3fusju9j.png 

好了本篇的unity3d教程到此结束,下篇我们再会!