《Unity3D网络游戏实战》第二章 大乱斗代码解析

目录

!!代码与书中相比有少许更改,解决了一些可能出现的问题

一、服务器

1、主程序:Program.cs

2、消息处理类:MsgHandler.cs

3、事件处理类:EventHandler.cs

二、客户端

1、player父类:BaseHuman.cs

2、本客户端玩家类:CtrlHuman.cs

3、其他客户端玩家类:SyncHuman.cs

4、通信管理类:NetManager.cs

5、通信使用类:Main.cs


一、服务器

1、主程序:Program.cs

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

//客户状态类,用来存储客户数据
public class ClientState
{
    public Socket socket;
    public byte[] readBuff = new byte[1024];
    public int hp = -100;
    public float x = 0;
    public float y = 0;
    public float z = 0;
    public float eulY = 0;
}

public class MainClass
{
    //监听Socket
    static Socket listenfd;
    //用字典来存储客户端Socket及状态信息
    public static Dictionary<Socket, ClientState> clients =
        new Dictionary<Socket, ClientState>();

    //程序入口函数
    public static void Main(string[] args)
    {
        //创建Socket
        listenfd = new Socket(AddressFamily.InterNetwork,
            SocketType.Stream, ProtocolType.Tcp);
        //连接Bind,给listenfd套接字绑定IP和端口
        IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
        IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
        listenfd.Bind(ipEp);
        //开启监听,等待客户端连接。参数可以指定队列中最多可容纳等待接受的连接数,0表示不限制
        listenfd.Listen(0);
        Console.WriteLine("[服务器]启动成功");
        //checkRead,后面用来筛选可读的socket
        List<Socket> checkRead = new List<Socket>();
        //主循环
        while (true)
        {
            //填充checkRead列表
            checkRead.Clear();
            checkRead.Add(listenfd);
            foreach (ClientState s in clients.Values)
            {
                checkRead.Add(s.socket);
            }
            //Select可以确定一个或多个Socket对象的状态
            //四个参数分别为:检查是否有可读的Socket列表,检查是否有可写的Socket列表,检查是否有出错的Socke列表,等待回应的时间
            Socket.Select(checkRead, null, null, 1000);//(时间单位为微妙,-1表示一直等待,0表示非阻塞)
            //筛选之后checkRead列表只会剩下可读的socket,不可读的就会被舍弃
            //检查遍历可读对象
            foreach (Socket s in checkRead)
            {
                if (s == listenfd)
                {
                    //如果服务器端的Socket可读,说明可能有客户端来连接了
                    ReadListenfd(s);
                }
                else
                {
                    //客户端的socket可读,说明客户端发消息了,来接收处理客户端消息
                    ReadClientfd(s);
                }
            }
        }
    }

    //用来给客户端发送消息的函数
    public static void Send(ClientState cs, string sendStr)
    {
        if (cs == null || cs.socket == null) return;
        if (!cs.socket.Connected) return;
        //GetString方法可以将byte型数组转换成字符串。System.Text.Encoding.Default.GetBytes可以将字符串转换成byte型数组。
        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
        cs.socket.Send(sendBytes);
    }

    //读取Listenfd
    public static void ReadListenfd(Socket listenfd)
    {
        Console.WriteLine("Accept");
        //Accept返回一个新客户端的Socket对象
        Socket clientfd = listenfd.Accept();
        ClientState state = new ClientState();
        state.socket = clientfd;
        //添加到clients字典中
        clients.Add(clientfd, state);
    }

    //读取Clientfd,读取客户端的消息
    public static bool ReadClientfd(Socket clientfd)
    {
        //通过字典获得当前客户端的状态
        ClientState state = clients[clientfd];
        //接收
        int count = 0;
        try
        {
            //接收消息存储在state.readBuff里面,返回字节数
            count = clientfd.Receive(state.readBuff);
        }
        catch (SocketException ex)
        {
            //关闭客户端

            //MethodInfo就是通过反射指定类获取到的 属性并提供对方法函数数据的访问。
            MethodInfo mei = typeof(EventHandler).GetMethod("OnDisconnect");
            object[] ob = { state };
            //通过mei调用所获得的函数,
            //第一个参数null代表this指针,由于消息处理方法都是静态方法,
            //第二个参数ob代表的是参数列表
            mei.Invoke(null, ob);

            clientfd.Close();
            clients.Remove(clientfd);
            Console.WriteLine("Receive SocketException" + ex.ToString());
            return false;
        }
        //客户端关闭
        if (count <= 0)
        {
            MethodInfo mei = typeof(EventHandler).GetMethod("OnDisconnect");
            object[] ob = { state };
            mei.Invoke(null, ob);

            clientfd.Close();
            clients.Remove(clientfd);
            Console.WriteLine("Socket Close");
            return false;
        }

        //消息处理
        string recvStr =
            System.Text.Encoding.Default.GetString(state.readBuff, 0, count);
        //有时候客户端连续发送两条消息给服务器端的时候,可能会导致两条消息合并发送过来,后面了解到这叫TCP粘包
        //这是由于底层的网络传输机制,多个小数据包可能会被合并成一个大数据包,这就导致了两个消息在一起发送的现象。
        //所以我在每条消息前面加了一个*号,然后根据*来先进行一次分割,即使粘包了,也可以根据*号来进行拆分。
        string[] allRecv = recvStr.Split('*');
        for(int i=0;i<allRecv.Length;i++)
        {
            if (allRecv[i] == "") continue;
            Console.WriteLine("Recv:" + allRecv[i]);//打印接收到的消息
            string[] split = allRecv[i].Split('|');//使用‘|’进行二次分割
            string msgName = split[0];//得到的第一个字符串就是协议的名字,比如Enter表示进入游戏,Move表示玩家移动等
            string msgArgs = split[1];//第二个字符串就是执行协议所需要的相关的参数
            string funName = "Msg" + msgName;
            //所有的消息处理函数都在 MsgHandler 里面,且都是静态的,通过反射获得函数信息
            MethodInfo mi = typeof(MsgHandler).GetMethod(funName);
            //这个例子所有的消息处理函数都需要这样两个参数ClientState c, string msgArgs
            object[] o = { state, msgArgs };
            mi.Invoke(null, o);
        }
        return true;
    }
}

2、消息处理类:MsgHandler.cs

using System;

class MsgHandler
{
    //Enter协议,表示有客户端进入游戏
    public static void MsgEnter(ClientState c, string msgArgs)
    {
        //解析参数
        string[] split = msgArgs.Split(',');
        string desc = split[0];
        float x = float.Parse(split[1]);
        float y = float.Parse(split[2]);
        float z = float.Parse(split[3]);
        float eulY = float.Parse(split[4]);
        //赋值
        c.hp = 100;
        c.x = x;
        c.y = y;
        c.z = z;
        c.eulY = eulY;
        //广播,告诉所有其他玩家客户端,有新人进入游戏了,然后其他玩家客户端就会加载这个玩家
        string sendStr = "*Enter|" + msgArgs;
        foreach (ClientState cs in MainClass.clients.Values)
        {
            MainClass.Send(cs, sendStr);
        }
    }

    //List协议,用来获得所有当前正在游玩的玩家的 位置和朝向 信息。玩家刚进入游戏时,要通过这个协议来得到其他玩家信息
    public static void MsgList(ClientState c, string msgArgs)
    {
        string sendStr = "*List|";
        foreach (ClientState cs in MainClass.clients.Values)
        {
            sendStr += cs.socket.RemoteEndPoint.ToString() + ",";
            sendStr += cs.x.ToString() + ",";
            sendStr += cs.y.ToString() + ",";
            sendStr += cs.z.ToString() + ",";
            sendStr += cs.eulY.ToString() + ",";
            sendStr += cs.hp.ToString() + ",";
        }
        Console.WriteLine(sendStr);
        MainClass.Send(c, sendStr);
    }

    //Move协议,每个玩家的每次移动都要向服务器端发送move协议,然后服务器再转发给所有其他客户端,这样能显示其他玩家的移动
    public static void MsgMove(ClientState c, string msgArgs)
    {
        //解析参数
        string[] split = msgArgs.Split(',');
        string desc = split[0];
        float x = float.Parse(split[1]);
        float y = float.Parse(split[2]);
        float z = float.Parse(split[3]);
        //赋值
        c.x = x;
        c.y = y;
        c.z = z;
        //广播
        string sendStr = "*Move|" + msgArgs;
        foreach (ClientState cs in MainClass.clients.Values)
        {
            MainClass.Send(cs, sendStr);
        }
    }

    //Attack协议,每个玩家的每次攻击都要向服务器端发送Attack协议,然后服务器再转发给所有其他客户端。
    public static void MsgAttack(ClientState c, string msgArgs)
    {
        //广播
        string sendStr = "Attack|" + msgArgs;
        foreach (ClientState cs in MainClass.clients.Values)
        {
            MainClass.Send(cs, sendStr);
        }
    }

    //Hit协议,玩家如果攻击到了其他玩家,就要向服务器发送Hit协议,告诉服务器哪个玩家被攻击了
    public static void MsgHit(ClientState c, string msgArgs)
    {
        //解析参数
        string[] split = msgArgs.Split(',');
        string attDesc = split[0];
        string hitDesc = split[1];
        //找出被攻击的角色
        ClientState hitCS = null;
        foreach (ClientState cs in MainClass.clients.Values)
        {
            if (cs.socket.RemoteEndPoint.ToString() == hitDesc)
                hitCS = cs;
        }
        if (hitCS == null)
            return;
        //扣血
        hitCS.hp -= 25;
        //死亡
        if (hitCS.hp <= 0)
        {
            Die协议,某个玩家hp小于0了,要告诉所有其他玩家某个玩家死亡了。
            string sendStr = "Die|" + hitCS.socket.RemoteEndPoint.ToString();
            foreach (ClientState cs in MainClass.clients.Values)
            {
                MainClass.Send(cs, sendStr);
            }
        }
    }
}

3、事件处理类:EventHandler.cs

using System;

public class EventHandler
{
    //某个客户端退出时调用,告诉其他客户端,有个客户端退出了
    public static void OnDisconnect(ClientState c)
    {
        
        string desc = c.socket.RemoteEndPoint.ToString();
        //Leave协议,客户端收到leave协议之后,就会把对应的玩家从场景中卸载
        string sendStr = "*Leave|" + desc + ",";
        Console.WriteLine("OnDisconnect:" + sendStr);
        foreach (ClientState cs in MainClass.clients.Values)
        {
            MainClass.Send(cs, sendStr);
        }
    }
}

二、客户端

1、player父类:BaseHuman.cs

using UnityEngine;
public class BaseHuman : MonoBehaviour
{
    //是否正在移动
    protected bool isMoving = false;
    //移动目标点
    private Vector3 targetPosition;
    //移动速度
    public float speed = 1.2f;
    //动画组件
    private Animator animator;
    //描述
    public string desc = "";

    //是否正在攻击
    internal bool isAttacking = false;
    internal float attackTime = float.MinValue;

    // Use this for initialization
    protected void Start()
    {
        animator = GetComponent<Animator>();//获得动画组件
    }

    // Update is called once per frame
    internal void Update()
    {
        MoveUpdate();
        AttackUpdate();
    }
    //移动到某处
    public void MoveTo(Vector3 pos)
    {
        targetPosition = pos;
        isMoving = true;
        animator.SetBool("isMoving", true);
    }

    //移动Update
    public void MoveUpdate()
    {
        if (isMoving == false)
        {
            return;
        }

        Vector3 pos = transform.position;
        transform.position = Vector3.MoveTowards(pos, targetPosition, speed * Time.deltaTime);
        transform.LookAt(targetPosition);//改变朝向
        if (Vector3.Distance(pos, targetPosition) < 0.05f)
        {
            isMoving = false;
            animator.SetBool("isMoving", false);
        }
    }

    //攻击动作
    public void Attack()
    {
        isAttacking = true;
        attackTime = Time.time;//记录当前攻击的时间
        animator.SetBool("isAttacking", true);
    }

    //攻击Update
    public void AttackUpdate()
    {
        if (!isAttacking) return;
        if (Time.time - attackTime < 1.2f) return;//冷却时间,1.2s内只能攻击一次
        isAttacking = false;
        animator.SetBool("isAttacking", false);
    }
}

2、本客户端玩家类:CtrlHuman.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CtrlHuman : BaseHuman
{

    // Use this for initialization
    new void Start()
    {
        base.Start();
    }

    // Update is called once per frame
    new void Update()
    {
        base.Update();
        //左键移动
        if (Input.GetMouseButtonDown(0))
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            Physics.Raycast(ray, out hit);
            //提前把可移动的地面的tag设置为Terrain
            if (hit.collider.tag == "Terrain")
            {
                MoveTo(hit.point);
                //发送Move协议给服务器端,以此来更新其他客户端的位置信息
                string sendStr = "*Move|";
                sendStr += NetManager.GetDesc() + ",";
                sendStr += hit.point.x + ",";
                sendStr += hit.point.y + ",";
                sendStr += hit.point.z + ",";
                NetManager.Send(sendStr);
            }
        }
        //右键攻击
        if (Input.GetMouseButtonDown(1))
        {
            if (isAttacking) return;
            if (isMoving) return;
            //获得右击的方向,并转向
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            Physics.Raycast(ray, out hit);

            transform.LookAt(hit.point);
            Attack();
            //攻击时给服务器发送attack协议,然后其他客户端才会显示攻击的动画等。
            string sendStr = "*Attack|";
            sendStr += NetManager.GetDesc() + ",";
            sendStr += transform.eulerAngles.y + ",";
            NetManager.Send(sendStr);

            //攻击判定,发射一条线段判断是否触碰到其他玩家(SyncHuman),表示攻击到了此玩家
            Vector3 lineEnd = transform.position + 0.5f * Vector3.up;
            Vector3 lineStart = lineEnd + 20 * transform.forward;
            if (Physics.Linecast(lineStart, lineEnd, out hit))
            {
                GameObject hitObj = hit.collider.gameObject;
                if (hitObj == gameObject)
                    return;
                SyncHuman h = hitObj.GetComponent<SyncHuman>();
                if (h == null)
                    return;
                //如果攻击到玩家之后,给服务器发送hit协议,
                sendStr = "*Hit|";
                sendStr += NetManager.GetDesc() + ",";
                sendStr += h.desc + ",";
                NetManager.Send(sendStr);
            }
        }
    }
}

3、其他客户端玩家类:SyncHuman.cs

using UnityEngine;

public class SyncHuman : BaseHuman
{
    // Use this for initialization
    new void Start()
    {
        base.Start();
    }
    // Update is called once per frame
    new void Update()
    {
        base.Update();
    }
    //攻击时调用的函数,设置朝向、播放攻击动画
    public void SyncAttack(float eulY)
    {
        transform.eulerAngles = new Vector3(0, eulY, 0);
        Attack();
    }
}

4、通信管理类:NetManager.cs

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

public static class NetManager
{
    //定义套接字
    static Socket socket;
    //接收缓冲区
    static byte[] readBuff = new byte[1024];
    //委托类型
    public delegate void MsgListener(string str);
    //监听列表
    private static Dictionary<string, MsgListener> listeners = new Dictionary<string, MsgListener>();
    //消息列表
    static List<string> msgList = new List<string>();

    //添加监听,添加协议对应的委托
    public static void AddListener(string msgName, MsgListener listener)
    {
        listeners[msgName] = listener;
    }

    //获取描述,获得客户端ip地址和端口号
    public static string GetDesc()
    {
        if (socket == null) return "";
        if (!socket.Connected) return "";
        return socket.LocalEndPoint.ToString();
    }

    //连接
    public static void Connect(string ip, int port)
    {
        //Socket
        socket = new Socket(AddressFamily.InterNetwork,
            SocketType.Stream, ProtocolType.Tcp);
        //Connect (用同步方式简化代码)
        socket.Connect(ip, port);
        //BeginReceive,开始接收客户端信息,使用异步的方法
        socket.BeginReceive(readBuff, 0, 1024, 0,ReceiveCallback, socket);
    }

    //Receive回调
    private static void ReceiveCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
            int count = socket.EndReceive(ar);
            string recvStr = System.Text.Encoding.Default.GetString(readBuff, 0, count);
            string[] allrecv = recvStr.Split('*');//因为每条独立的消息都是以*为开头
            foreach (string a in allrecv)
            { 
                if (a == "") continue;
                msgList.Add(a);//把消息放入消息列表
                Debug.Log("接收消息:" + recvStr);
            }
            socket.BeginReceive(readBuff, 0, 1024, 0,
                ReceiveCallback, socket);
        }
        catch (SocketException ex)
        {
            Debug.Log("Socket Receive fail" + ex.ToString());
        }
    }

    //给服务器发送消息
    public static void Send(string sendStr)
    {
        if (socket == null) return;
        if (!socket.Connected) return;

        byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
        socket.Send(sendBytes);
    }

    //Update,处理消息列表的消息
    public static void Update()
    {
        if (msgList.Count <= 0)
            return;
        string msgStr = msgList[0];
        Debug.Log("处理消息:"+msgStr);
        msgList.RemoveAt(0);
        string[] split = msgStr.Split('|');
        string msgName = split[0];//第一项为协议名称
        string msgArgs = split[1];
        //监听回调;
        if (listeners.ContainsKey(msgName))
        {
            //调用对应协议的委托函数
            listeners[msgName](msgArgs);
        }
    }
}

5、通信使用类:Main.cs

using System.Collections.Generic;
using UnityEngine;

public class Main : MonoBehaviour
{
    //人物模型预设
    public GameObject humanPrefab;
    //人物列表
    public BaseHuman myHuman;
    public Dictionary<string, BaseHuman> otherHumans;

    void Start()
    {
        otherHumans = new();
        //网络模块,添加协议以及对应委托函数
        NetManager.AddListener("Enter", OnEnter);
        NetManager.AddListener("List", OnList);
        NetManager.AddListener("Move", OnMove);
        NetManager.AddListener("Leave", OnLeave);
        NetManager.AddListener("Attack", OnAttack);
        NetManager.AddListener("Die", OnDie);
        //连接服务器
        NetManager.Connect("127.0.0.1", 8888);
        //添加一个角色(自己)
        GameObject obj = Instantiate(humanPrefab);
        float x = Random.Range(-5, 5);
        float z = Random.Range(-5, 5);
        obj.transform.position = new Vector3(x, 0, z);
        myHuman = obj.AddComponent<CtrlHuman>();//添加CtrlHuman组件
        myHuman.desc = NetManager.GetDesc();

        //发送Enter协议,告诉其他玩家,有新玩家进入游戏
        Vector3 pos = myHuman.transform.position;
        Vector3 eul = myHuman.transform.eulerAngles;
        string sendStr = "*Enter|";
        sendStr += NetManager.GetDesc() + ",";
        sendStr += pos.x + ",";
        sendStr += pos.y + ",";
        sendStr += pos.z + ",";
        sendStr += eul.y;
        NetManager.Send(sendStr);
        //请求玩家列表(其他玩家)
        NetManager.Send("*List|");
    }

    private void Update()
    {
        NetManager.Update();
    }

    void OnEnter(string msgArgs)
    {
        Debug.Log("OnEnter:" + msgArgs);
        //解析参数
        string[] split = msgArgs.Split(',');
        string desc = split[0];
        float x = float.Parse(split[1]);
        float y = float.Parse(split[2]);
        float z = float.Parse(split[3]);
        float eulY = float.Parse(split[4]);
        //是自己
        if (desc == NetManager.GetDesc())
            return;
        //添加一个角色(其他玩家)
        GameObject obj = Instantiate(humanPrefab);
        obj.transform.position = new Vector3(x, y, z);
        obj.transform.eulerAngles = new Vector3(0, eulY, 0);
        BaseHuman h = obj.AddComponent<SyncHuman>();//其他玩家添加SyncHuman组件即可
        h.desc = desc;
        otherHumans.Add(desc, h);
    }

    void OnMove(string msgArgs)
    {
        Debug.Log("OnMove" + msgArgs);
        //解析参数
        string[] split = msgArgs.Split(',');
        string desc = split[0];
        float x = float.Parse(split[1]);
        float y = float.Parse(split[2]);
        float z = float.Parse(split[3]);
        //移动
        if (!otherHumans.ContainsKey(desc))
            return;
        BaseHuman h = otherHumans[desc];
        Vector3 targetPos = new Vector3(x, y, z);
        h.MoveTo(targetPos);
    }

    void OnLeave(string msgArgs)
    {
        Debug.Log("OnLeave" + msgArgs);
        //解析参数
        string[] split = msgArgs.Split(',');
        string desc = split[0];
        //删除
        if (!otherHumans.ContainsKey(desc))
            return;
        BaseHuman h = otherHumans[desc];
        Destroy(h.gameObject);
        otherHumans.Remove(desc);
    }

    void OnList(string msgArgs)
    {
        Debug.Log("OnList:" + msgArgs);
        //解析参数
        string[] split = msgArgs.Split(',');
        int count = (split.Length - 1) / 6;
        for (int i = 0; i < count; i++)
        {
            string desc = split[i * 6 + 0];
            float x = float.Parse(split[i * 6 + 1]);
            float y = float.Parse(split[i * 6 + 2]);
            float z = float.Parse(split[i * 6 + 3]);
            float eulY = float.Parse(split[i * 6 + 4]);
            int hp = int.Parse(split[i * 6 + 5]);
            //是自己
            if (desc == NetManager.GetDesc())
                continue;
            //添加一个角色
            GameObject obj = Instantiate(humanPrefab);
            obj.transform.position = new Vector3(x, y, z);
            obj.transform.eulerAngles = new Vector3(0, eulY, 0);
            BaseHuman h = obj.AddComponent<SyncHuman>();
            h.desc = desc;
            otherHumans.Add(desc, h);
        }
    }

    void OnAttack(string msgArgs)
    {
        Debug.Log("OnAttack" + msgArgs);
        //解析参数
        string[] split = msgArgs.Split(',');
        string desc = split[0];
        float eulY = float.Parse(split[1]);
        //攻击动作
        if (!otherHumans.ContainsKey(desc))
            return;
        SyncHuman h = (SyncHuman)otherHumans[desc];
        h.SyncAttack(eulY);
    }

    void OnDie(string msgArgs)
    {
        Debug.Log("OnDie" + msgArgs);
        //解析参数
        string[] split = msgArgs.Split(',');
        string attDesc = split[0];
        string hitDesc = split[0];
        //自己死了
        if (hitDesc == myHuman.desc)
        {
            Debug.Log("Game Over");
            return;
        }
        //死了
        if (!otherHumans.ContainsKey(hitDesc))
            return;
        SyncHuman h = (SyncHuman)otherHumans[hitDesc];
        h.gameObject.SetActive(false);
    }
}

猜你喜欢

转载自blog.csdn.net/buzhengli/article/details/134962697