《Unity3D网络游戏实战》学习与实践--制作一款大乱斗游戏

角色类

基类Base Human是基础的角色类,它处理"操控角色"和"同步角色"的一些共有功能;CtrlHuman类代表"操控角色"​,它在BaseHuman类的基础上处理鼠标操控功能;SyncHuman类是"同步角色"类,它也继承自BaseHuman,并处理网络同步(如果有必要)​。

BaseHuman

cs 复制代码
        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 = "";

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

            // Use this for initialization
            protected void Start () {
                animator = GetComponent<Animator>();
            }

            // Update is called once per frame
            protected void Update () {
                MoveUpdate();
            }
        }

CtrlHuman

cs 复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CtrlHuman : BaseHuman
{
    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 == "Terrain") {
                MoveTo(hit.point);
            }
        }
    }
}

如何使用网络模块

在实际的网络游戏开发中,网络模块往往是作为一个底层模块用的,它应该和具体的游戏逻辑分开,而不应该把处理逻辑的代码写到 ReceiveCallback 里面去。因为ReceiveCallback应当只处理网络数据,不应该去处理游戏功能

一个可行的做法是,给网络管理类添加回调方法,当收到某种消息时就自动调用某个函数,这样便能够将游戏逻辑和底层模块分开。制作网络管理类前,需要先了解委托、协议和消息队列这三个概念。

通信协议

通信协议是通信双方对数据传送控制的一种约定,通信双方必须共同遵守,方能"知道对方在说什么"和"让对方听懂我的话"​。

使用一种最简单的字符串协议来实现。协议格式如下所示,消息名和消息体用"|"隔开,消息体中各个参数用", "隔开。

消息名|参数1, 参数2, 参数3, ...

Move|127.0.0.1:1234, 10, 0, 8,

处理数据:

cs 复制代码
        string str = "Move|127.0.0.1:1234, 10, 0,8, ";

        string[] args = str.Split('|');
        string msgName = args[0]; //协议名:Move
        string msgBody = args[1]; //协议体:127.0.0.1:1234, 10, 0,8,

        string[] bodyArgs = msgBody.Split(', ');
        string desc = bodyArgs [0];               //玩家描述:127.0.0.1:1234
        float x = float.Parse(bodyArgs [1]);     //x坐标:10
        float y = float.Parse(bodyArgs [2]);     //y坐标:0
        float z = float.Parse(bodyArgs [3]);     //z坐标:8

消息队列

多线程消息处理虽然效率较高,但非主线程不能设置Unity3D组件 ,而且容易造成各种莫名其妙的混乱。由于单线程消息处理足以满足游戏客户端的需要,因此大部分游戏会使用消息队列让主线程去处理异步Socket接收到的消息。

C#的异步通信由线程池实现,不同的BeginReceive不一定在同一线程中执行。创建一个消息列表,每当收到消息便在列表末端添加数据,这个列表由主线程读取,它可以作为主线程和异步接收线程之间的桥梁。由于MonoBehaviour的Update方法在主线程中执行,可让Update方法每次从消息列表中读取几条信息并处理,处理后便在消息列表中删除它们

NetManager类

网络模块中最核心的地方是一个称为NetManager的静态类,这个类对外提供了三个最主要的接口。

  • Connect方法,调用后发起连接;
  • AddListener方法,消息监听。其他模块可以通过AddListener设置某个消息名对应的处理方法,当网络模块接收到这类消息时,就会回调处理方法;
  • Send方法,发送消息给服务端。

无论内部实现有多么复杂,网络模块对外的接口只有图片展示的这几个:

对内部而言,NetManager使用了异步Socket接收消息,每次接收到一条消息后,NetManager会把消息存入消息队列中​。NetManager有一个供外部调用的Update方法,每当调用它时就会处理消息队列里的第一条消息,然后根据协议名将消息分发给对应的回调函数

cs 复制代码
    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);
        //监听列表
        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;
        }

        //获取描述
        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);
                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.Send(sendBytes);
        }

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

漏洞

上述代码没有处理粘包分包、线程冲突等问题

参考书籍:《Unity3D网络游戏实战(第2版)》 (豆瓣) (douban.com)

相关推荐
hakesashou8 分钟前
Python中常用的函数介绍
java·网络·python
C++忠实粉丝19 分钟前
计算机网络socket编程(4)_TCP socket API 详解
网络·数据结构·c++·网络协议·tcp/ip·计算机网络·算法
九州ip动态19 分钟前
做网络推广及游戏注册为什么要换IP
网络·tcp/ip·游戏
Estar.Lee24 分钟前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
蝶开三月25 分钟前
php:使用socket函数创建WebSocket服务
网络·websocket·网络协议·php·socket
G丶AEOM39 分钟前
SSL/TLS,SSL,TLS分别是什么
网络·网络协议·网络安全
儒道易行42 分钟前
【DVWA】RCE远程命令执行实战
网络·安全·网络安全
Koi慢热2 小时前
路由基础(全)
linux·网络·网络协议·安全
hzyyyyyyyu3 小时前
内网安全隧道搭建-ngrok-frp-nps-sapp
服务器·网络·安全