作者:肥爪
文章来源:http://taik.io/99
之前的文章中说u3d可以用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.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中:
[C#] 纯文本查看 复制代码
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(); btnText.text = server.Name; serverObj.GetComponent().onClick.AddListener(() => { Debug.Log("进入"+server.Name); GameManager.Instance.Enter(server); }); } } }
把服务器关联的Button做成了一个Prefab,拖放关联起来。然后添加点击处理函数,进入指定的服务器。
界面出来之后差不多这样:
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。
[C#] 纯文本查看 复制代码
public class ConcurrentQueue { private readonly Queue _queue = new Queue(); 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来实现ICommand,而ChatCommand继承CommandBase就可以了。
大概是这样:
public interface ICommand { int Name { get; } void Execute(object package); object DeserializePackage(MemoryStream ms); } public abstract class Command : ICommand where T : class, global::ProtoBuf.IExtensible { public abstract int Name { get; } public void Execute(object package) { _onExecute(package as T); } private readonly Action _onExecute; public object DeserializePackage(MemoryStream ms) { return Serializer.Deserialize(ms); } public Command(Action onExecute) { _onExecute = onExecute; } }
ChatCommand则是这样:
public class ChatCommand : Command { public override int Name { get { return (int) CMD_Chat.Send; } } public ChatCommand(Action onExecute) : base(onExecute) { } }
实现ChatHandle,因为CommandBase需要一个委托作为参数,用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(); } } }
PS: 我使用的是unity5