在这篇文章中,我们会完成网络通信模块的搭建,让这个单人游戏变成一个多人联机游戏。

通信协议

通信协议是通信双方约定俗成的一种控制协议,用于对彼此传递的数据进行解析,如摩尔斯电码就是一种同学协议,发报人以一种特定的形式加密数据,而收报人则通过指定规则对数据进行解析;这种通信协议可以做的很复杂,
也可以做的很简单。针对我们的这个游戏,我们可以将玩家角色状态总结为以下几点:

玩家状态

  • 移动状态
  • 待机状态
  • 攻击状态
  • 受伤状态
  • 死亡状态

书上将通信协议设定为:消息名|参数1,参数2,参数3...
Ps:Move|127.0.0.1,10,0,8 表示的就是传递Move动作,127.0.0.1表示玩家信息,10,0,8表示要移动的点。

搭建网络模块

using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using UnityEngine;

namespace _Scripts
{


    public static class NetManager
    {
        /// <summary>
        /// Socket实例
        /// </summary>
        public static Socket Socket { get; set; }
        /// <summary>
        /// 接收缓冲区
        /// </summary>
        public static byte[] ReadBuff { private get; set; }=new byte[1024];

        /// <summary>
        /// 处理委托
        /// </summary>
        /// <param name="str"></param>
        public delegate void MsgListener(string str);

        /// <summary>
        /// 监听字典
        /// </summary>
        public static Dictionary<string,MsgListener> Listeners { get; set; }=new Dictionary<string, MsgListener>();

        /// <summary>
        /// 信息队列
        /// </summary>
        public static Queue<string> MsgList { get; set; }=new Queue<string>();

        /// <summary>
        /// 添加监听
        /// </summary>
        /// <param name="msgName">信息名</param>
        /// <param name="listener">绑定的委托</param>
        public static void AddListener(string msgName, MsgListener listener)
        {
            Listeners[msgName] = listener;
        }

        /// <summary>
        /// 获得描述
        /// </summary>
        /// <returns></returns>
        public static string GetDesc()
        {
            if (Socket==null)
            {
                return "";
            }

            if (!Socket.Connected)
            {
                return "";
            }

            return Socket.LocalEndPoint.ToString();
        }

        /// <summary>
        /// 连接服务器
        /// </summary>
        /// <param name="ip">IP地址</param>
        /// <param name="port">端口号</param>
        public static void Connect(string ip, int port)
        {
            Socket=new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
            Socket.Connect(ip,port);
            Socket.BeginReceive(ReadBuff, 0, 1024, SocketFlags.None,callback: ReceiveCallBack, Socket);
        }

        /// <summary>
        /// 接收回调
        /// </summary>
        /// <param name="ar">异步结果参数</param>
        private static void ReceiveCallBack(IAsyncResult ar)
        {
            try
            {
                var socket = ar.AsyncState as Socket;
                var count = socket.EndReceive(ar);
                var recvStr = Encoding.UTF8.GetString(ReadBuff, 0, count);
                MsgList.Enqueue(recvStr);    //向消息队列添加消息
                //继续监听接收
                socket.BeginReceive(ReadBuff, 0, ReadBuff.Length, SocketFlags.None, ReceiveCallBack, socket);
            }
            catch (SocketException e)
            {
                Debug.Log("Socket 接收错误:"+e.Message);
            }
        }

        /// <summary>
        /// 发送消息
        /// </summary>
        /// <param name="sendStr">消息体</param>
        public static void Send(string sendStr)
        {
            if (Socket==null)
            {
                return;
            }

            if (!Socket.Connected)
            {
                return;
            }

            var sendBytes = Encoding.UTF8.GetBytes(sendStr);
            Socket.Send(sendBytes);
        }

        /// <summary>
        /// 持续监听处理
        /// </summary>
        public static void Update()
        {
            if (MsgList.Count<=0)
            {
                return;
            }

            //解析数据
            var msgStr = MsgList.Dequeue();    //获得消息队列的首个元素并出队
            var data = msgStr.Split('|');
            var msgName = data[0];
            var msgArgs = data[1];

            //回调监听
            if (Listeners.ContainsKey(msgName))
            {
                Listeners[msgName](msgArgs);
            }


        }

    }
}

创建GameManager

using System;
using UnityEngine;

namespace _Scripts
{
    public class GameManager : MonoBehaviour
    {
        private void Start()
        {   
            //绑定
            NetManager.AddListener("Enter",OnEnter);
            NetManager.AddListener("Move",OnMove);
            NetManager.AddListener("Leave",OnLeave);

            NetManager.Connect("127.0.0.1",2233);    //连接

        }



        /// <summary>
        /// 进入
        /// </summary>
        /// <param name="msg"></param>
        void OnEnter(string msg)
        {
            Debug.Log("OnEnter:"+msg);
        }

        /// <summary>
        /// 移动
        /// </summary>
        /// <param name="msg"></param>
        void OnMove(string msg)
        {
            Debug.Log("OnMove:"+msg);

        }

        /// <summary>
        /// 离开
        /// </summary>
        /// <param name="msg"></param>
        void OnLeave(string msg)
        {
            Debug.Log("OnLeave:"+msg);

        }
    }
}

在CtrlHuman脚本中的玩家移动部分添加一条测试语句NetManager.Send("Enter|127.1.1.1,100,200,300,45");

服务器端采用之前实现的Select方式

代码如下:

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace Socket_Server
{
    class ClientEntity
    {
        public Socket Socket { get; set; }

        public byte[] ReadBuff { get; set; }

        /// <summary>
        /// 有效数据长度
        /// </summary>
        public int DataLength { get; set; }
    }

    class Program
    {
        const string ip = "127.0.0.1";
        const int port = 2233;
        static Socket serverSocket;
        static Dictionary<Socket, ClientEntity> clientEntities = new Dictionary<Socket, ClientEntity>();

        static void Main(string[] args)
        {
            IPAddress iPAddress = IPAddress.Parse(ip);
            IPEndPoint iPEndPoint = new IPEndPoint(iPAddress, port);

            serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            serverSocket.Bind(iPEndPoint);
            serverSocket.Listen(0);
            Console.WriteLine("[服务器]启动成功");
            List<Socket> checkRead = new List<Socket>();

            while (true)
            {
                checkRead.Clear();
                checkRead.Add(serverSocket);
                foreach (var item in clientEntities.Values)
                {
                    checkRead.Add(item.Socket);
                }
                //select
                Socket.Select(checkRead, null, null, 1000);
                foreach (var item in checkRead)
                {
                    if (item == serverSocket)
                    {
                        ReadListenfd(item);
                    }
                    else
                    {
                        ReadClientfd(item);
                    }
                }
            }

        }



        public static void ReadListenfd(Socket listenfd)
        {
            Console.WriteLine("Accept");
            Socket clientSocket = listenfd.Accept();
            ClientEntity clientEntity = new ClientEntity() { Socket = clientSocket, ReadBuff = new byte[1024] };

            clientEntities.Add(clientSocket, clientEntity);
        }

        public static bool ReadClientfd(Socket clientfd)
        {
            ClientEntity entity = clientEntities[clientfd];
            int count = 0;
            try
            {
                count = clientfd.Receive(entity.ReadBuff);

            }
            catch (SocketException e)
            {
                clientfd.Close();
                clientEntities.Remove(clientfd);
                Console.WriteLine("Receive 错误" + e.Message);
                return false;
            }

            if (count == 0)
            {
                clientfd.Close();
                clientEntities.Remove(clientfd);
                Console.WriteLine("Socket Close");
                return false;
            }
            //广播
            string recvStr = Encoding.UTF8.GetString(entity.ReadBuff, 0, count);
            Console.WriteLine("Receive\t" + recvStr);
            //string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr;
            string sendStr = recvStr;
            byte[] sendBytes = Encoding.UTF8.GetBytes(sendStr);
            foreach (var item in clientEntities.Values)
            {
                item.Socket.Send(sendBytes);
            }
            return true;

        }


    }
}

开始测试

  1. 启动服务器端
  2. 启动客户端
  3. 点击鼠标右键移动人物
  4. 查看服务器端输出

发现客户端与服务器的的连接已经正常建立,如下图所示:

在启动服务器后,输出服务器启动,在有客户点连接时输出Accept,在玩家移动时给服务器发送消息,服务器负责将消息广播到连接的所有客户端,客户端负责将这些消息解析,同步其他角色的行为。