Unity3D网络游戏实战——实践出真知:大乱斗游戏

前言

这一章是教我们做一个大乱斗游戏。但是书中的代码有些前后不一致导致运行错误,如果你也碰到了这样的情况,可以参考我的代码
我们要完成的主要有以下这些事

  1. 左键操控角色行走
  2. 右键操控角色攻击
  3. 受到攻击掉血,队手hp为0时消失,自己hp为0时失败,掉线同理
  4. 角色类的编写
  5. 介绍协议、消息队列等
  6. 客户端以及服务端代码

角色类BaseHuman

unity相关操作基本都省略了

在这里插入图片描述
BaseHuman为角色基类
CtrlHuman代表操控角色,在BaseHuman基础上多了处理鼠标操控功能
SyncHuman代表同步角色,处理网络同步,由网络数据驱动,由服务端转发角色的状态信息

using System.Collections;
using System.Collections.Generic;
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;

    //攻击动作
    public void Attack()
    {
        isAttacking = true;
        attackTime = Time.time;
        //只要动画不loop,就不会在1.2s内一直攻击
        animator.SetBool("isAttacking", true);
    }

    //攻击update
    public void AttackUpdate()
    {
        if (!isAttacking) return;
        if (Time.time - attackTime < 1.2f) return;
        isAttacking = false;
        animator.SetBool("isAttacking", false);
    }

    //移动到某处
    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);
        }
    }

    protected void Start()
    {
        animator = GetComponent<Animator>();
    }

    protected void Update()
    {
        MoveUpdate();
        AttackUpdate();
    }
}

我们把移动、攻击等两个子类都需要的写在基类中。

3.3.4CtrlHuman

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

public class CtrlHuman : BaseHuman
{
    // Start is called before the first frame update
    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);
            if (hit.collider.tag == "Ground")
            {
                MoveTo(hit.point);

                //发送协议
                string sendStr = "Move|";
                sendStr += NetManager.GetDesc() + ",";
                sendStr += hit.point.x + ",";
                sendStr += hit.point.y + ",";
                sendStr += hit.point.z + ",";
                NetManager.Send(sendStr);
            }
        }

        //attack
        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();


            //发送协议
            string sendStr = "Attack|";
            sendStr += NetManager.GetDesc() + ",";
            sendStr += transform.eulerAngles.y + ",";
            NetManager.Send(sendStr);

            //攻击判定
            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;
                sendStr = "Hit|";
                sendStr += NetManager.GetDesc() + ",";//我们这里把攻击者和被攻击者的ip都发了
                sendStr += h.desc + ",";
                NetManager.Send(sendStr);
            }
        }
    }
}

tips:

  • CtrlHuman的功能有几个:1.鼠标操控人物。2.操控完后发送move和attack协议给服务器。(具体协议后面介绍)
  • new用作修饰符时可以显示隐藏从基类继承的成员。

3.3.5SyncHuman

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

public class SyncHuman : BaseHuman
{
    // Start is called before the first frame update
    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();
    }
}

开放一个同步攻击函数操控自己的转向即可(动画都在基类中转换),大部分操作都由一个Main类来操控,比如设定targetPos,因为都是在Main类里接收并解析服务端转发来的数据

3.4如何使用网络模块

在实际开发中网络模块都是作为底层模块用的,应该和具体的游戏逻辑分开,而不应该把处理逻辑的代码比如给recvStr赋值写到ReceiveCallback里面去,因为它仅需要处理网络数据
所以我们的方法是给网络管理类添加回调,当收到某种消息时自动调用对应的函数,这样就可以将游戏逻辑和底层模块分开。
先介绍委托、协议和消息队列才能更好的写网络管理类

3.4.1委托

NetManager会用委托实现消息分发,可以把委托理解成回调函数的实现方式,比if-else或者switch强!
delegate是C#的一种类型,它能够引用某种类型的方法,相当于C++的函数指针。

  • 委托传递的方法必须有相同的参数和返回值类型
  • 创建委托对象,将要传递的方法作为参数传入
  • 可以通过+=和-=来绑定不同的函数
  • 调用委托时,依次调用对应的所有回调函数

3.4.2通信协议

通信协议是通信双方对数据传送控制的一种约定,通信双方必须共同遵守,要知道对方在说什么和让对方听懂我的话
这里就简单用字符串协议来实现,消息名和消息体用“|”隔开,消息体各个参数用“,”隔开
比如

Move|127.0.0.1:1234,10,0,8,
消息名,ip,端口号,移动坐标

用Split(’|’)和Split(’,’)可将协议中各个参数解析出来。
解析后,通过委托将不同的消息交给不同的方法处理。Move协议就用OnMove方法处理…

3.4.3消息队列

多线程消息处理虽然效率高,但非主线程不能设置Unity3D组件,所以聊天室我们就用了变量recvStr作为主线程和callback线程的桥梁,容易造成混乱。
因为单线程消息可以满足游戏客户端的需要,所以大部分游戏会使用消息队列让主线程取处理异步Socket收到的消息。

C#的异步通信由线程池实现,不同的BeginReceive不一定在同一线程执行**(这里有个坑!Send也要改成异步的,不然如果是send的同步实现形式,消息不是一条一条过去的,而是同时收到的,无法正确解析)**
创建一个消息列表,每次收到消息就在末端添加数据,列表由主线程读取,它作为主线程和异步线程的桥梁。mono的update在主线程执行,所以每次可以从消息列表读取几条信息处理,处理后在列表中删除
用List实现

3.4.4类

Show me code

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

public static class NetManager
{
    //套接字
    static Socket socket;
    //接收缓冲区
    static byte[] readBuff = new byte[1024];
    //委托类型
    public delegate void MsgListener(string str);
    //监听列表
    //指明各个消息名所对应的处理方法,外部可通过addlistener添加消息名对应的处理函数
    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)
    {
        //消息对应一个委托,委托即回调。收到此消息直接通过dic触发委托即可
        listeners[msgName] = listener;
    }

    //获取描述
    public static string GetDesc()
    {
        if (socket == null) return "";
        if (!socket.Connected) return "";
        return socket.LocalEndPoint.ToString();//ip
    }

    //连接
    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);
            msgList.Add(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.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);
    }

    //Send回调
    private static void SendCallback(IAsyncResult ar)
    {
        try
        {
            Socket socket = (Socket)ar.AsyncState;
        }
        catch(SocketException ex)
        {
            Debug.Log("Socket Send fail" + ex.ToString());
        }
    }

    //Update,收到消息后让外界调用的,通过消息列表执行对应的回调。
    public static void Update()
    {
        if (msgList.Count <= 0)
            return;
        string msgStr = msgList[0];
        msgList.RemoveAt(0);
        string[] split = msgStr.Split('|');
        string msgName = split[0];
        string msgArgs = split[1];
        //监听回调
        if (listeners.ContainsKey(msgName))
        {
            listeners[msgName](msgArgs);
        }
    }
}

主要作用:

  • Connect
  • AddListener,消息监听,其他模块可以通过此方法设置某个消息名对应的处理模块,我们在Main的Start中绑定。
  • Send,发送消息给服务端
    使用异步Socket接收消息,每次接收到一条消息存入消息队列。再编写一个供外部调用的Update方法,每当调用它就处理消息队列里第一条消息,然后根据协议名将消息分发给对应的回调函数。

注意注意:
Send要用异步来实现!!

3.5进入游戏:Enter协议

客户端进入游戏后发送一条Enter协议个服务端,包含对玩家的描述、位置等,服务端将Enter协议广播出去,其他客户端收到Enter协议后,创建一个同步角色(SyncHuman)

此处把代码全贴了,后序在讲到不同的协议就单独拿出来说

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

public class Main : MonoBehaviour
{
    public GameObject humanPrefab;
    //任务列表
    public BaseHuman myHuman;
    //不new会怎么样?
    public Dictionary<string, BaseHuman> otherHumans = new Dictionary<string, BaseHuman>();

    // Start is called before the first frame update
    void Start()
    {
        //net模块
        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 = (GameObject)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>();
        myHuman.desc = NetManager.GetDesc();
        //发送enter协议,调用start的时候会发送enter加入待处理列表
        Vector3 pos = myHuman.transform.position;
        Vector3 eul = myHuman.transform.eulerAngles;
        string sendStr = "Enter|";
        sendStr += NetManager.GetDesc() + ",";
        sendStr += pos.x + "," + pos.y + "," + pos.z + "," + eul.y+",";
        NetManager.Send(sendStr);

        //请求玩家列表
        //感觉这个被一级加到enter协议里面了
        NetManager.Send("List|");
    }

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

    void OnEnter(string msgArgs)
    {
        Debug.Log("OnEnter " + msgArgs);

        //解析参数,netManager那里也要解析,不过是为了绑定回调
        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]);
        //是自己,netManager只为自己这个客户端服务
        if(desc == NetManager.GetDesc())
        {
            return;
        }
        //在收到别的客户端进入游戏发送的enter协议时,在本地生成一个同步角色
        GameObject obj = (GameObject)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 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]);
            float hp = int.Parse(split[i * 6 + 5]);
            //self
            if (desc == NetManager.GetDesc())
            {
                continue;
            }
            //添加角色,那enter还需要生成吗
            //没有关系,list协议是用于生成在你之前进入游戏的玩家的,在你进入的时候调用一次,之后再有玩家进入就通过enter生成同步对象
            GameObject obj = (GameObject)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 OnMove(string msg)
    {
        Debug.Log("OnMove" + msg);
        //解析参数
        string[] split = msg.Split(',');
        string desc = split[0];
        float x = float.Parse(split[1]);
        float y = float.Parse(split[2]);
        float z = float.Parse(split[3]);
        //move
        if (!otherHumans.ContainsKey(desc)) return;
        BaseHuman h = otherHumans[desc];//这相当于直接修改本体
        Vector3 targetPos = new Vector3(x, y, z);
        h.MoveTo(targetPos);
    }

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

    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);//应该是直接删掉
    }
}
  • 在Start中绑定委托
  • 创建角色
  • 在Update中调用NetManger的Update!让其在有消息的情况下不停解析
  • 程序按需给角色添加CtrlHUuman或SyncHUman,不用再prefab上加

Enter协议:

Enter|127.0.0.1:4564,3,0,5,0,

客户端发送和服务端转发的形式一样

3.5.2接收Enter协议

也就是上面OnEnter
客户端收到服务端转发的Enter协议后,解析Enter协议的各个参数,用于生成同步角色并加入到otherHuman列表中(后序可以直接通过ip来操控同步角色)

问题

后进入的客户端看不到前面的玩家
因为后进入的收不到前一个玩家的Enter协议,后序用List协议解决

3.6服务端如何处理消息

客户端用AddListener把网络协议和具体的处理函数对应。服务端就用反射机制把底层网络模块和具体的消息处理函数分开

3.6.1反射机制

using System.Reflection;
using System.Linq;

ReadClientfd(Socket clientfd){
		string[] split = recvStr.Split('|');
        Console.WriteLine("Receive" + recvStr);
        string msgName = split[0];
        string msgArgs = split[1];
        string funName = "Msg" + msgName;
        //MethodInfo类的对象mi包含它所指代的方法的所有信息
        //通过这个类可以得到方法的名称、参数、返回值,并且可以调用它
        //我们会将所有消息处理方法定义在MsgHandler中,且为static方法。所以可以用下面这条语句获取handler类中名为funName的static func
        MethodInfo mi = typeof(MsgHandler).GetMethod(funName);
        object[] o = { state, msgArgs };
        //调用mi所包含的方法,arg0代表this指针,因为为staic方法所以填null,arg1是参数列表,里面包含消息处理需要的两个参数,客户端state和消息内容msgArgs
        mi.Invoke(null, o);
}

我们需要的就是在解析协议名后,自动调用名为“Msg+协议名”的方法
上面的反射代码就可以帮我们实现
MethodInfo类的对象mi包含它所指代的方法的所有信息,通过这个类可以得到方法的名称、参数、返回值,并且可以调用它,我们会将所有消息处理方法定义在MsgHandler中,且为static方法。所以可以用下面这条语句获取handler类中名为funName的static func
调用mi所包含的方法,arg0代表this指针,因为为staic方法所以填null,arg1是参数列表,里面包含消息处理需要的两个参数,客户端state和消息内容msgArgs

3.6.2消息处理

using System;
using System.Collections.Generic;

public class MsgHandler
{
    //这是收到服务端收到消息调用的对应函数
    //arg0表示消息从哪个客户端发来,arg1代表消息内容
    //如果同时发送了list|,把list作为参数一起穿进来了
	public static void MsgEnter(ClientState c,string msgArgs)
    {
        Console.WriteLine("MsgEnter" + 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);
        }
    }

    public static void MsgList(ClientState c,string msgArgs)
    {
        string sendStr = "List|";
        //组装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("MsgList" + msgArgs);
        MainClass.Send(c, sendStr);
    }

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

    public static void MsgAttack(ClientState c,string msgArgs)
    {
        //只需转发attack协议即可
        string sendStr = "Attack|" + msgArgs;
        foreach (ClientState cs in MainClass.clients.Values)
        {
            MainClass.Send(cs, sendStr);
        }
    }

    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)
        {
            string sendStr = "Die|" + hitCS.socket.RemoteEndPoint.ToString();
            foreach (ClientState cs in MainClass.clients.Values)
            {
                MainClass.Send(cs, sendStr);
            }
        }
    }
}

一般都是转发给客户端,enter、move这些就需要记录下位置等,hit就要判断是否死亡,死亡就发送die协议。

3.6.3事件处理

using System;

//处理上下线事件,和处理消息的class类似
public class EventHandler
{
    //Clientstate类要设为public,不然方法和参数访问性不一致要报错
    //角色下线,删除客户端内的同步角色
    public static void OnDisconnect(ClientState c)
    {
        string desc = c.socket.RemoteEndPoint.ToString();
        string sendStr = "Leave|" + desc + ",";
        foreach (ClientState cs in MainClass.clients.Values)
        {
            MainClass.Send(cs, sendStr);
        }
    }
}

3.7玩家列表:List协议

玩家进入场景后,调用NetManger.Send发送List协议。
此处发送和转发的协议格式不同

  • 客户端发送的请求
List|
  • 服务端转发的回应
List|127.0.0.1:4564,3,0,5,0,100,127.0.0.1:4578,4,0,9,0,100,

有几个人后面就有几组
在start中发送请求

3.7.1客户端处理

	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]);
            float hp = int.Parse(split[i * 6 + 5]);
            //self
            if (desc == NetManager.GetDesc())
            {
                continue;
            }
            //添加角色,那enter还需要生成吗
            //没有关系,list协议是用于生成在你之前进入游戏的玩家的,在你进入的时候调用一次,之后再有玩家进入就通过enter生成同步对象
            GameObject obj = (GameObject)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);
        }
    }

每次有新的客户端进入,都通过for循环生成前面所有记录下的玩家。

3.7.2服务端处理

组装记录下的所有客户端信息后转发。(这就是我们为什么要在服务端记录人物pos、hp等,可以用于list协议给后面的玩家看)

	public static void MsgList(ClientState c,string msgArgs)
    {
        string sendStr = "List|";
        //组装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("MsgList" + msgArgs);
        MainClass.Send(c, sendStr);
    }

3.8移动同步:Move协议

点击场景后,将targetPos发送给服务端,服务端一边记录位置信息以便广播给其他客户端。其余客户端收到协议后,解析目的地位置信息,用otherHuman和ip控制SyncHuman走到对应位置

3.8.1客户端处理

点击后将targetPos发送给服务端

if (Input.GetMouseButtonDown(0))
{
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    RaycastHit hit;
    Physics.Raycast(ray, out hit);
    if (hit.collider.tag == "Ground")
    {
        MoveTo(hit.point);

        //发送协议
        string sendStr = "Move|";
        sendStr += NetManager.GetDesc() + ",";
        sendStr += hit.point.x + ",";
        sendStr += hit.point.y + ",";
        sendStr += hit.point.z + ",";
        NetManager.Send(sendStr);
    }
}

操控对应的同步角色行走

void OnMove(string msg)
{
    Debug.Log("OnMove" + msg);
    //解析参数
    string[] split = msg.Split(',');
    string desc = split[0];
    float x = float.Parse(split[1]);
    float y = float.Parse(split[2]);
    float z = float.Parse(split[3]);
    //move
    if (!otherHumans.ContainsKey(desc)) return;
    BaseHuman h = otherHumans[desc];//这相当于直接修改本体
    Vector3 targetPos = new Vector3(x, y, z);
    h.MoveTo(targetPos);
}

3.8.2服务端处理

解析参数
记录信息
广播

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

3.8.3完善

由于网络延迟等问题,这种同步方式可能有问题,几个客户端的表现不会完全一致。现在的网络游戏也只能保证误差在可接受范围内。

后续的几个协议都大差不差,就不写了!

最后

在测试中可能会断线、收不到协议等。因为TCP协议是基于数据流的协议,并不保证每次接受的数据都完整,商业级游戏必须解决各种隐患。

  • 3
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值