【简单3d网络游戏制作】——基于Unity

这篇具有很好参考价值的文章主要介绍了【简单3d网络游戏制作】——基于Unity。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

Demo展示

前期知识点准备

1.delegate委托

2.通信协议

3.List容器

4.dictionary容器

5.MethodInfo类

进入创作

c/s通用通信协议:

客户端 

1.场景搭建

2.BaseHuman刨析(Sync+Ctrl)

        Ctrl脚本

        Sync完整代码 

        BaseHuman完整代码

3.NetManager刨析

4.Main刨析

        Main完整代码

服务端 

MsgHandler刨析 

EventHandler刨析

Program完整代码:

MsgHandler完整代码:


 

Demo展示

简单网络游戏Demo——基于Unity

前期知识点准备

PS:此处不必先看,可以在“进入创作”模块过程中遇到了对应知识点再回头学习理解。

1.delegate委托

关于delegate委托,类似接口,我的理解是一个‘通用入口’,为什么这么说呢,因为它在这的作用就是为同种类型的函数(方法)站岗或者说做兼职。 

条件:

1.引用的方法必须与声明的delegate有相同的返回值和形参
2.创建对象的时候,引用方法必须当作形参输入(本案例用不上,不用管)
3.你以为有第三点?其实没有,这行格子我用来凑数的的哈哈哈!

举例了解:

//声明委托类型

public delegate void DelegateStr(string Str);

//创建需要引用的方法

public static void PrintStr(string S){

        Console.WriteLine("Hello World!"+S);

}

//主函数

public static void Main(string[] args){

        //创建delegate对象

        DelegateStr fun=new DelegateStr(PrintStr);

        //测试看

        fun("I am 胖崽配猪");

}

运行结果:Hello World!I am 胖崽配猪 

 本案例作用:

这里我们用委托的特性,结合后面会提到的Ditionary类,把所有协议方法放入ditionary容器里。

 2.通信协议

为了让所有的客户端顺畅地沟通,我们必须制定相同的协议来规范它们。因此,我们就需要进行“装码”和“解码”这两步骤,而后面的客户端、服务端均会涉及到,那里再进行详细解释和代码展示!

 本案例作用:

方便通信

3.List容器

本案例用到C#的List,它是一种容器,而且是泛型结构,唯一的泛型参数指定列表的数据类型。 

这里给各位刚接触泛型的同学普及一下,大佬自行跳过或纠正:

 泛型就是一种规范类型的抽象结构,它包含泛型类、方法、接口等,它能够实例化各种类型的(类)对象。声明时以抽象的形式,实例化就以一种具体的数据表现出对象,复用广!

想深入理解泛型请click下面的链接跳转,但是记得回来哈!

本案例作用:

在客户端用队列存储消息,形成一个消息队列(排好队,一个一个来!doge)

4.dictionary容器

和队列一样,它也是一种泛型容器,泛型参数有两个:一个是key,一个是value(中文翻译:关键词和对应值)

Key的作用是通过指定类型的具体值作为对应真值的flag;而Value就是真值,Dictionary真正存储的数据。 

本案例作用:

用于存放所有的协议方法 

5.MethodInfo类

MethodInfo类形成的对象能够包含所指代的方法的所有信息,通过此类可以获得方法的名称、参数、返回值等,还能间接调用它。

MethodInfo类下的关键方法为Invoke,通过它可以间接调用指代的方法,达到映射效果。 

~使用该类需要引用System.Reflection.

本案例作用:

用此类运用在服务端上,实现和客户端一样的委托协议映射。


进入创作

        本案例所需的资源如下(github和百度网盘):

                ChannelOne.Github——SimpleNetGame

                ChannelTwo.百度网盘——提取码:PZPZ

c/s通用通信协议:

为了使客户端和服务器规范交流方式,我们规定统一的交流语言。客户端和服务器均涉及到两步骤,我命名为“装码”和“解码” 过程。(以下是手写逻辑图,字丑别喷!)        

【简单3d网络游戏制作】——基于Unity

装码:

                             消息串:    “Enter|127.0.0.1:4564,3,0,5,0,”

 注释:“Enter”是执行协议名;"127.0.0.1:4564"是专属客户端标识;"3"、"0"、"5"、"0"分别对应(x,y,z,旋转角)

        用"|"分割字符串得到两组子串,"Enter"表示协议名,另一个"127.0.0.1:4564,3,0,5,0,"为消息串。

解码:

                运用C#的字符串函数Split,分割消息串得到所需信息组

 注释:Split函数返回值是字符串组,因此需要创建一个string[]来存放信息组。


客户端 

客户端Unity包含 五个脚本,分别命名为NetManager、BaseHuman、SyncHuman、CtrlHuman和Main这五个脚本。其中BaseHuman是Sync和Ctrl的父类。这五个脚本的大致关系如下图:

【简单3d网络游戏制作】——基于Unity


 场景搭建

 1.首先,从Prefabs文件中将PlaneMax放置到场景中,并给予Tag——Terrain

【简单3d网络游戏制作】——基于Unity

  2.创建NetManager、BaseHuman、SyncHuman、CtrlHuman和Main五个脚本,并将Main脚本挂载在场景的任意一个物体上(我习惯挂主摄像机上)。

【简单3d网络游戏制作】——基于Unity

 (图中多的FollowTarget脚本是我用于摄像机的跟踪角色用的)

3.为了让我们在游戏中有更好的沉浸效果,我们来为游戏添加背景音乐吧!

        选择一个游戏物体,可以选择之前的摄像机,这里我选择了自己导入的小山,添加AudioSource组件,并在Resource文件中将战歌挂载到AudioClip处。【简单3d网络游戏制作】——基于Unity

4.为了让我们的Unity编译器能够直接的进行网络调试,我们必须打开Unity的Server,让云服务有内容即可。


BaseHuman刨析(Sync+Ctrl)

        本脚本实现人物的基本移动和人物动画的控制,它的子类中有CtrlHuman、SyncHuman,Ctrl是负责本地角色的附带脚本,而Sync是其他玩家的附带脚本。

1.Ctrl脚本

        该脚本挺重要的,它负责本机角色的移动并告知服务器本机移动的信息,服务器便将此消息广播给所有的玩家。此处的攻击消息原理同理。

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


public class CtrlHuman : BaseHuman
{
    //使用本继承类的start
    new void Start()
    {
        base.Start();
    }
    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);
                //发送协议
                string sendStr = "Move|";
                sendStr += NetManager.GetDesc() + ",";
                sendStr += hit.point.x + ",";
                sendStr += hit.point.y + ",";
                sendStr += hit.point.z + ",";
                NetManager.Send(sendStr);
            }
        }
       
        if (Input.GetKeyDown(KeyCode.J))
        {
            if (isAttacking) return;
            Attack();
            //发送协议
            string sendStr = "Attack|";
            sendStr += NetManager.GetDesc() + ",";
            NetManager.Send(sendStr);
            //攻击判定

            RaycastHit hit;
            Vector3 lineEnd = transform.position + 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() + ",";//得到"Hit|自己,击打对象,"
                sendStr += h.desc + ",";
                NetManager.Send(sendStr);
            }
           
        }
    }
    
    private void OnDestroy()
    {
       GameObject cube= Instantiate(GameObject.CreatePrimitive(PrimitiveType.Cube));
        cube.transform.position = transform.position;
    }
}

2.Sync完整代码 

         因为Sync是其他同步角色的附带脚本,所以只要能够拥有基础的移动、攻击便可,不需向服务器发送任何消息,只需服从服务器的指令完成动作即可。

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

public class SyncHuman : BaseHuman
{
    
    new void Start()
    {
        base.Start();
    }

   
    new void Update()
    {
        base.Update();
    }
    public void SyncAttack(float euly)
    {
        transform.eulerAngles = new Vector3(0, euly, 0);
        Attack();
    }
}

         运用射线完成鼠标点击移动,并变换动画bool变量实现动画变化。这里我不想细讲了,好累啊,直接看代码吧!

3.BaseHuman完整代码

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

public class BaseHuman : MonoBehaviour
{
    //是否移动
    private bool isMoving = false;
    //移动目标点
    private Vector3 targetPosition;
    //移动速度
    public float speed = 1.2f;
    //动画组件
    private Animator animator;
    //是否在攻击
    internal bool isAttacking = false;
    internal float attackingTime = float.MinValue;
    //自身描述
    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);
        }
    }
    //攻击动作
    public void Attack()
    {
        isAttacking = true;
        attackingTime = Time.time;
        animator.SetBool("isAttacking",true);
    }
    //攻击循环
    public void AttackUpdate()
    {
        if (!isAttacking) return;
        if (Time.time - attackingTime < 1f) return;
        isAttacking = false;
        animator.SetBool("isAttacking", false);
    }
    //Update
    protected void Start()
    {
        animator = GetComponent<Animator>();

    }
    //每帧改地址
    protected void Update()
    {
        MoveUpdate();
        AttackUpdate();
    }

}

 (这里的攻击方法可用可不用,我是因为unity有bug所以用不了了,呜呜,我的Demo就只能移动了!)


NetManager刨析

        本脚本包含五个必要变量——socket、readBuff、listeners、msgList、MsgListener
               

                 【简单3d网络游戏制作】——基于Unity

socket:一个客户端只包含一个socket与服务器通信
readBuff:用于暂时接收服务端发来的消息的缓存站
listeners:字典类型,用于存储客户端协议方法的容器
msgList:将接收到的消息存放在队列中
MsgListener:当作AddListener方法的形参,提供给具体的协议方法一个进入listener容器的接口

        作为一个客户端通信站,NetManager要负责socket连接、消息的接收、消息的发送,这里我们对接收和发送均使用异步方式(异步知识点 跳转) 。除此之外,它拥有GetDesc方法——获得本地IP,用于客户端的标志信息、Update方法——解析消息并执行回调。

废话不多说,上完整代码(NetManager):

using System.Collections;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Net;
using UnityEngine;
using System;
public class NetManager : MonoBehaviour
{
    //定义套接字(全局变量)
    static Socket 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);
    }
    public 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 vs)
        {
            Debug.Log("Error" + vs);
        }
    }
    //发送
    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);
    }
    public static void SendCallBack(IAsyncResult ar)
    {
        try                                              // 这一块不会写!!!
        {
            Socket socket = (Socket)ar.AsyncState;
                     
        }
        catch (SocketException vs)
        {
            Debug.Log("Error" + vs);
        }
    }
  //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);
        }

    }



}

Main刨析

一个完整军队需要一位统帅,而Main则在整个客户端中充当这个角色。

        本脚本必要的变量有:【简单3d网络游戏制作】——基于Unity         humanPrefab和enemyPrefab用于区分实例化的角色;字典容量otherHumans用于存放所有在线的角色。

         回到Unity中我们将对应的预制体挂载在脚本对应位置【简单3d网络游戏制作】——基于Unity

         接着,我们先解决本机角色的连接、角色登入创建。但我们自己进入游戏的时候肯定要实例创建一个角色,这里用到之前的变量humanPrefab,然后随机化出生地址,再把我们自己以Sync子类加入到otherHumans容器中,以便协议的统一操作。

【简单3d网络游戏制作】——基于Unity

        然后,我们要在游戏开始阶段前将监听(协议)列表填满,这里运用NetManager的AddListener方法,把我们会涉及到的登入协议、在线协议、移动协议、离开协议等加入Dictionary容器中。

按照通用通信协议,我们把关健词(Key)定义为图中引号所示:

【简单3d网络游戏制作】——基于Unity


 接下来逐一解释Main中每一个协议方法:

1.OnEnter:

        因为我们是网络游戏,所以一定会有玩家的进入。当有玩家进入的时候,我们要得到它所在位置,并且给本地客户端同步一个角色然后将所得位置赋予同步。

【简单3d网络游戏制作】——基于Unity

 代码

public void OnEnter(string msg) //此处进入的是“地址,x,y,z,euly;
    {
        Debug.Log("I just come here!!!"+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]);
        float euly = float.Parse(split[4]);
        if (desc == NetManager.GetDesc()) return;
        //初始化角色同步
        GameObject other = (GameObject)Instantiate(enemyPrefab);
        other.transform.position = new Vector3(x, y, z);
        other.transform.eulerAngles = new Vector3(0, euly, 0);
        BaseHuman h = other.AddComponent<SyncHuman>();                  //此处子类对象赋值给父类,留有悬念!!!看看后期如何处理。
        h.desc = desc;
        otherHumans.Add(desc, h);
    }

 2.OnList:

        为了解决后面进入的玩家能够同步上之前进入的玩家,咱们使用List协议,当玩家一进入游戏,就调用该方法,从服务器获取所有在线玩家的信息并在场景中同步所有角色。在服务端会讲到,该方法接收到服务器的消息用于此处的实参是一条"玩家1+玩家2+玩家3+..."的连串字符串。

代码 

【简单3d网络游戏制作】——基于Unity


3. OnMove:

        同步移动,当有角色移动时,服务器会通知调用此函数,结合所给信息同步他的移动。

【简单3d网络游戏制作】——基于Unity

 代码 

此处要将所给字符串信息转换成能够使用的float类型。  

【简单3d网络游戏制作】——基于Unity


4.OnLeave:

        当有玩家退出的时候,我们要从场景中销毁对应的角色。 

       

【简单3d网络游戏制作】——基于Unity

这里记得将otherHumans容器内的对应对象给移除。 

【简单3d网络游戏制作】——基于Unity


Main完整代码

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

public class Main : MonoBehaviour
{
    public GameObject humanPrefab;
    public GameObject enemyPrefab;
    public Material enemyMaterial;
     BaseHuman myHuman;
    public Dictionary<string, BaseHuman> otherHumans = new Dictionary<string, BaseHuman>();//此处不初始化就会死人的哈哈哈

    
    private void Start()

    {
        Debug.Log("Test!!!!");
        NetManager.AddListener("Attack", OnAttack);
        NetManager.AddListener("List", OnList);
        NetManager.AddListener("Enter", OnEnter);
        NetManager.AddListener("Move", OnMove);
        NetManager.AddListener("Leave", OnLeave);
        NetManager.Connect("127.0.0.1", 8888);  //此处连接不上就不执行下面的代码,即无法创建角色
        GameObject obj = (GameObject)Instantiate(humanPrefab);
        obj.tag = "Myself";
        obj.name = "Player";
        GameObject[] g= GameObject.FindGameObjectsWithTag("Shoulder");
        Material[] materials = g[0].GetComponent<MeshRenderer>().materials;
        Material[] materials2 = g[1].GetComponent<MeshRenderer>().materials;
        materials[0] = enemyMaterial;
        materials2[0] = enemyMaterial;
        
        float x = Random.Range(-6, 6);
        float z = Random.Range(-6, 6);
        obj.transform.position = new Vector3(x, 0, z);
        myHuman = obj.AddComponent<CtrlHuman>();
        myHuman.desc = NetManager.GetDesc();

        //发送协议
        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|");
    }
    //攻击
    void OnAttack(string msgArgs)
    {
        Debug.Log("I am Fighting!");
        //解析参数
        string[] split = msgArgs.Split(',');
        string des = split[0];
        float euly = float.Parse(split[1]);
        //
        if (!otherHumans.ContainsKey(des)) return;
        SyncHuman h = (SyncHuman)otherHumans[des];
        h.SyncAttack(euly);
    }
    //死亡
    void OnDie(string msg)
    {
        //解析
        string[] split = msg.Split(',');
        string attDec = split[0];
        string beenHitDec = split[1];
        //自己死了
        if (beenHitDec == myHuman.desc) { Debug.Log("Game Over!");
            Destroy(GameObject.FindGameObjectWithTag("Myself"));
        }
        if (!otherHumans.ContainsKey(beenHitDec)) return;
        SyncHuman h = (SyncHuman)otherHumans[beenHitDec];
        h.gameObject.SetActive(false);

    }
    
    public void OnList(string msgArgs)
    {
        Debug.Log("OnList" + msgArgs);
        //解析参数
        string[] split = msgArgs.Split(',');
        for (int i = 0; i < ((split.Length-1)/6); 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 other = (GameObject)Instantiate(enemyPrefab);
            other.transform.position = new Vector3(x, y, z);
            other.transform.eulerAngles = new Vector3(0, euly, 0);
            BaseHuman h = other.AddComponent<SyncHuman>();                  //此处子类对象赋值给父类,留有悬念!!2!看看后期如何处理。
            h.desc = desc;
            otherHumans.Add(desc, h);
        }

    }
    public void OnEnter(string msg) //此处进入的是“地址,x,y,z,euly;
    {
        Debug.Log("I just come here!!!"+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]);
        float euly = float.Parse(split[4]);
        if (desc == NetManager.GetDesc()) return;
        //初始化角色同步
        GameObject other = (GameObject)Instantiate(enemyPrefab);
        other.transform.position = new Vector3(x, y, z);
        other.transform.eulerAngles = new Vector3(0, euly, 0);
        BaseHuman h = other.AddComponent<SyncHuman>();                  //此处子类对象赋值给父类,留有悬念!!!看看后期如何处理。
        h.desc = desc;
        otherHumans.Add(desc, h);
    }
    public void OnMove(string msg)
    {
        Debug.Log("I am moving!!!");
        //解析参数
        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]);
        //移动
        if (!otherHumans.ContainsKey(desc))
        {
            return;
        }
        BaseHuman h = otherHumans[desc];
        Vector3 changePosition = new Vector3(x, y, z);
        h.MoveTo(changePosition);
    }
    public void OnLeave(string msg)
    {
        Debug.Log("I just left!!!");
        //解析参数
        string[] split = msg.Split(',');
        string des = split[0];
        //删除对应角色
        if (!otherHumans.ContainsKey(des)) return;
        BaseHuman h = otherHumans[des];
        Destroy(h.gameObject);
        otherHumans.Remove(des);
    }
    private void Update()
    {
        NetManager.Update();
    }
}

服务端 

本服务器运用VS编译,涉及三个 .cs文件,分别为Program、MsgHandler、EventHandler。与客户端类似,三个文件分别担任统帅、士兵1、士兵2的责任。

【简单3d网络游戏制作】——基于Unity

整个服务器任然运用我之前博客的异步服务器框架(该博客地址指引 ),只不过存在部分的调整和添加,接下来由我来细说。

变量的添加:客户端状态类中,我们添加了客户端角色的五个基本属性血量、坐标(x,y,z)和旋转角。

【简单3d网络游戏制作】——基于Unity

       如图, 添加Select方法,该方法能够检测所给列表内可读的套接字(客户端),能够自动将非可读的套接字给踢出,留下能够联系的客户端。

【简单3d网络游戏制作】——基于Unity

        如图, 运用Method方法。假设所有的消息处理方法都定义在MsgHandler类中,且都是静态方法,通过typeof(MsgHandler).GetMethod(funName)便能够获取MsgHandler类中名为funName的静态方法。

         mi.Invoke(null,o)代表调用mi所包含的方法,第一个参数null为this指针,由于消息处理方法都是静态方法,因此填null。第二个参数o代表参数列表。这里消息处理函数都有两个参数,第一个是客户端状态state,第二个是消息的内容msgArgs。

 【简单3d网络游戏制作】——基于Unity


MsgHandler刨析 

        上面提到了消息处理函数,那么消息处理函数到底是谁呢?它在哪呢?(卖关子好爽啊!)实际上,消息处理函数都封装在MsgHandler里,和客户端一样,它拥有登入处理、移动处理、在线列表处理、攻击处理(我的有bug就不作解释了)。

  

1.MsgEnter: 

         和客户端不同的是,服务器的消息处理需要多一个参数——ClientState,用于了解对应客户端的行为。得到对应的客户端状态属性值后,向所有玩家广播该客户端的登入。

【简单3d网络游戏制作】——基于Unity

2. MsgList:

        当客户端进入游戏,他需要获取之前的玩家的登入信息,因此他会向服务器发送请求,然后服务器就通过该函数反馈给对应玩家一个所有在线玩家的连串字符串。

该连串字符串:“List|192.168.100:8080,1,2,3,0,192.168.100:8080,2,3,4,0,......”

【简单3d网络游戏制作】——基于Unity

3.MsgMove:

        在得知玩家移动后,服务器更新对应玩家状态的基本属性,并广播告知所有玩家,让他们同步该玩家移动。

【简单3d网络游戏制作】——基于Unity


 EventHandler刨析

        其实这里只处理玩家的退出消息,挺简单的。

【简单3d网络游戏制作】——基于Unity


 Program完整代码:

using System;
using System.Collections.Generic;
using System.Reflection;  //使用映射空间doge
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;



namespace Socket_1_Server
{
    //客户端状态体,携带任何客户端的字段
    /// <summary>
    /// //客户端状态体,携带任何客户端的字段
    /// </summary>
    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;
    }
    class Program
    {
        public static Socket server;
        public static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();
        //public static Socket acceptSocket;
        static void Main(string[] args)
        {

            Console.WriteLine("Welcome to Server!!!");
            //Socket
             server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPAddress local_Ip = IPAddress.Parse("127.0.0.1");
             IPEndPoint iPEndPoint = new IPEndPoint(local_Ip, 8888);
            //绑定
                server.Bind(iPEndPoint);
            //Listen
            server.Listen(0);
            Console.WriteLine("[服务器]启动成功!");

            //checkRead
            List<Socket> checkRead = new List<Socket>();
            //主循环
            while (true)
            {
                //填充cheakRead列表
                checkRead.Clear();
                checkRead.Add(server);
                foreach(var s in clients.Values)
                {
                    checkRead.Add(s.socket);
                }
                //select
                Socket.Select(checkRead, null, null, 1000);

                //检查可读对象
                foreach (var item in checkRead)
                {
                    if (item == server)
                    {
                        ReadLIstenfd(item);

                    }
                    else
                    {
                        ReadClientfd(item);
                    }
                }

            }
            
        }
        /// <summary>
        /// 监听客户端
        /// </summary>
        /// <param name="listenfd"></param>
        public static void ReadLIstenfd(Socket listenfd)
        {
            Console.WriteLine("Accept");
            Socket clientfd = listenfd.Accept();
            ClientState state = new ClientState();
            state.socket = clientfd;
            clients.Add(clientfd, state);
        }
        //读取Clientfd
        public static bool ReadClientfd(Socket clientfd)
        {
            ClientState state = clients[clientfd];
            //接收
            int count = 0;
            try
            {
                count = clientfd.Receive(state.readBuff);
            }
            catch (SocketException ex)
            {
                MethodInfo mei = typeof(EventHandler).GetMethod("OnDisconnect");    //使用Method方法
                object[] ob = { state };
                mei.Invoke(null, ob);

                clientfd.Close();
                clients.Remove(clientfd);

                Console.WriteLine("Error:" + ex.ToString());
                return false;
            }
            if (count <= 0)
            {
                MethodInfo mei = typeof(EventHandler).GetMethod("OnDisconnect");    //使用Method方法
                object[] ob = { state };
                mei.Invoke(null, ob);

                clientfd.Close();
                clients.Remove(clientfd);
                Console.WriteLine("出现错误!!!");
                return false;

            }
            //广播(消息处理)
            string recvStr = System.Text.Encoding.Default.GetString(state.readBuff, 0,count );
            
            string[] split = recvStr.Split('|');        //自动解析消息
            Console.WriteLine("massage is:" + recvStr);
            string msgName = split[0];
            string msgArgs = split[1];
            string funName = "Msg" + msgName;
            MethodInfo mi = typeof(MsgHandler).GetMethod(funName);      //使用Method方法
            Object[] o = { state, msgArgs };
            mi.Invoke(null, o);


           
            return true;
        }
        /// <summary>
        /// Accept的回调函数
        /// </summary>
        /// <param name="ar"></param>
        public static void Acceptcallback(IAsyncResult ar)
        {
            try
            {
                Console.WriteLine("客户端接入");
                Socket socket = (Socket)ar.AsyncState;
                Socket chatSocket = socket.EndAccept(ar);
                //列表导入
                ClientState state = new ClientState();
                state.socket = chatSocket;
                clients.Add(chatSocket, state);
                //接收数据
                chatSocket.BeginReceive(state.readBuff, 0, 1024, 0,  Receivecallback, state);
                socket.BeginAccept(Acceptcallback, socket);
            }
            catch(SocketException ex)
            {
                Console.WriteLine("Error:" + ex.ToString());
            }
        }
        /// <summary>
        /// Receive的回调函数
        /// </summary>
        /// <param name="ar"></param>
        public static void Receivecallback(IAsyncResult ar)
        {
            try
            {
                ClientState clientState = (ClientState)ar.AsyncState;
                Socket clientfd = clientState.socket;
                int count = clientfd.EndReceive(ar);
                //判断客户端的断开
                if (count == 0)
                {
                    clientfd.Close();
                    clients.Remove(clientfd);
                    Console.WriteLine("有客户端退出!");
                    return;
                }
                string recvStr = System.Text.Encoding.Default.GetString(clientState.readBuff, 0, count);
                //将信息广播
                string time = System.DateTime.Now.ToString();
                byte[] vs = System.Text.Encoding.Default.GetBytes("[消息]:" + recvStr);
                foreach (ClientState item in clients.Values)  //此处将clients中的所有clientstate遍历出来;
                {
                    if(item.socket!=clientfd)
                    item.socket.Send(vs);
                }
                clientfd.Send(vs);
                clientfd.BeginReceive(clientState.readBuff, 0, 1024, 0, Receivecallback, clientState);
            }
            catch (SocketException ex)
            {
                Console.WriteLine("error:" + ex.ToString());
            }
            
        }
        /// <summary>
        /// 消息发送封装,避免字节串的复用
        /// </summary>
        /// <param name="cs"></param>
        /// <param name="sendStr"></param>
        public static void Send(ClientState cs, string sendStr)
        {
            byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
            cs.socket.Send(sendBytes);
        }
    }
    
}

MsgHandler完整代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Socket_1_Server
{
    class MsgHandler :Program
    {
        /// <summary>
        /// 网络角色进入
        /// </summary>
        /// <param name="c"></param>
        /// <param name="msgArgs"></param>
        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 item in Program.clients.Values)
            {
                Program.Send(item, sendStr);
            }
        } 
        
        public static void MsgList(ClientState c,string msgArgs)
        {
            Console.WriteLine("MsgList" + msgArgs);
            string sendStr = "List|";
            foreach (var item in Program.clients.Values)
            {
                sendStr += item.socket.RemoteEndPoint.ToString() + ",";
                sendStr += item.x.ToString() + ",";
                sendStr += item.y.ToString() + ",";
                sendStr += item.z.ToString() + ",";
                sendStr += item.euly.ToString() + ",";
                sendStr += item.hp.ToString() + ",";
            }
            Program.Send(c, sendStr);
        }
        public  static void MsgHit(ClientState c,string msgArgs)
        {
            string[] split = msgArgs.Split(',');
            string attDec = split[0];
            string beenHitDec = split[1];
            //找出被打的
            ClientState beenHitCS = null;
            foreach (var item in Program.clients.Values)
            {
                if (item.socket.RemoteEndPoint.ToString() == beenHitDec)
                    beenHitCS = item;
            }
            if (beenHitCS == null)
            {
                return;
            }
            //扣血行动
            beenHitCS.hp -= 25;
            //死亡判断
            if (beenHitCS.hp <= 0)
            {
                string sendStr = "Die|" + beenHitCS.socket.RemoteEndPoint.ToString();       //终结点转换成string类型
                foreach (var item in Program.clients.Values)
                {
                    Program.Send(item, sendStr);
                }
            }
        }
        public static void MsgAttack(ClientState c,string msgArgs)
        {
            //广播
            String sendStr = "Attack|" + msgArgs;
            foreach (var item in Program.clients.Values)
            {
                Program.Send(item, 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 item in Program.clients.Values) 
            {

                Program.Send(item, sendStr);
            }
        }
    }
}

        完成所有的创作后,就可以打包测试了!

        ¥最后希望各位帅哥、美女点个赞吧,码2w字我真的头疼¥文章来源地址https://www.toymoban.com/news/detail-449306.html

到了这里,关于【简单3d网络游戏制作】——基于Unity的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Unity与C++网络游戏开发实战:基于VR、AI与分布式架构 【1.6】

    3.8 Unity中使用协程         协程是在Unity中经常使用的一种辅助处理模式。比如,我们需要设计一个人一边走动一边去观察周围的情况,走动和观察这两种运动同时进行。这时我们可以使用多线程来处理这个问题,但是多线程在内存和CPU的调度时间上具有一些风险。此时在

    2024年04月10日
    浏览(47)
  • Unity游戏开发客户端面经——网络(初级)

    前言:记录了总6w字的面经知识点,文章中的知识点若想深入了解,可以点击链接学习。由于文本太多,按类型分开。这一篇是 网络 常问问题总结,有帮助的可以收藏。 区别 UDP TCP 是否连接 不连接 面向连接 是否可靠 不可靠 可靠传输(传输过程中会丢失,但会重发)使用

    2024年02月01日
    浏览(44)
  • Unity之NetCode多人网络游戏联机对战教程(1)

    官网链接:https://docs-multiplayer.unity3d.com/netcode/current/about/ Netcode for GameObjects(NGO)是专为Unity构建的高级网络库。它能够在网络会话中将 GameObject 和 世界数据 同时发送给多名玩家。使用NGO不必关心低级协议和网络框架。 打开一个unity项目,在菜单栏中选择 Window Package Manager

    2024年02月07日
    浏览(59)
  • Unity之NetCode多人网络游戏联机对战教程(4)--连接申请ConnectionApproval

    没看过前面的教程请先阅读前面的教程,本期将会讲到Netcode联机的申请,当一个Client想连接进来,应向Server发送申请联机的信息,然后由服务端向客户端下发数据,所有数据应该在服务端,而不是在客户端。 举几个常见的例子需要用到 ConnectionApproval 的场景 使用密码加入房

    2024年02月03日
    浏览(61)
  • Unity 3D游戏开发+脚本编程完整指南:制作第一个游戏:3D滚球跑酷

    教程相关资源 Unity 3D游戏开发+脚本编程完整指南(工程文件+PPT).zip 本节利用前面的知识来实现第一个较为完整的小游戏,如 图 1-21 所示。 图1-21 3D滚球跑酷游戏完成效果 1. 功能点分析 游戏中的小球会以恒定速度向前移动,而玩家控制着小球 左右移动来躲避跑道中的黄色障

    2024年02月21日
    浏览(51)
  • 【unity】快速了解游戏制作流程-制作九宫格简单游戏demo

            hi~大家好呀!欢迎来到我的unity学习笔记系列~,本篇我会简单的记录一下游戏流程并且简单上手一个通过九宫格移动到指定位置的小游戏,话不多说,我们直接开始吧~                  本篇源自我看B站一位up主的视频所做的笔记,感兴趣的可以去看原视频哦

    2023年04月08日
    浏览(57)
  • 【Unity】3D贪吃蛇游戏制作/WebGL本地测试及项目部署

    本文是Unity3D贪吃蛇游戏从制作到部署的相关细节 项目开源代码:https://github.com/zstar1003/3D_Snake 试玩链接:http://xdxsb.top/Snake_Game_3D 效果预览: 试玩链接中的内容会和该效果图略有不同,后面会详细说明。 经典贪吃蛇游戏:蛇身随着吃食物的增加不断变长,通过A/D或方向键←→

    2024年02月07日
    浏览(52)
  • 使用Unity3D制作2D游戏的重点做法

    官网上有提供一个 2D游戏的教学范例 ,这个游戏只不过把镜头摆放在横向位置跟随角色移动,游戏内物件运动时固定一个轴不动,使他像横轴式的2D游戏来玩,本质上其实还是个3D游戏,所以如果没有3D建模的话不就没办法做2D游戏了?其实这些并没有定论,就看制作者如何运

    2024年02月11日
    浏览(65)
  • Unity3D项目之游戏场景小地图制作

    创建一个场景资源,可在asset store资源商店下载,选择心仪场景。 链接:https://assetstore.unity.com/?locale=zh-CN 添加一个对象GameObject,命名为player,子物体包括:主摄像机,角色,后面步骤会添加一个plane。 添加一个地图摄像机,命名为MapCamera。修改属性projection为OrthoGraphic;Size为

    2024年02月11日
    浏览(79)
  • 【Unity】教你如何使用Unity制作一个简单的跑酷游戏

    其实用Unity制作游戏并不难,如果你想学习,那我就建议你想从制作一个简单的跑酷游戏来找到兴趣,因为如果你一开始就一直学习一些没什么必要的语法,这样就会让你一开始就失去了信心,失去了学习Unity的动力,所以如果你先学习如何制作一个简单的跑酷地图,然后你就

    2024年02月21日
    浏览(57)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包