【简单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通用通信协议:

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

装码:

                             消息串:    “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的父类。这五个脚本的大致关系如下图:


 场景搭建

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

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

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

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

        选择一个游戏物体,可以选择之前的摄像机,这里我选择了自己导入的小山,添加AudioSource组件,并在Resource文件中将战歌挂载到AudioClip处。

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
               

                 

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则在整个客户端中充当这个角色。

        本脚本必要的变量有:         humanPrefab和enemyPrefab用于区分实例化的角色;字典容量otherHumans用于存放所有在线的角色。

         回到Unity中我们将对应的预制体挂载在脚本对应位置

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

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

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


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

1.OnEnter:

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

 代码

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+..."的连串字符串。

代码 


3. OnMove:

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

 代码 

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


4.OnLeave:

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

       

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


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的责任。

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

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

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

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

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

 


MsgHandler刨析 

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

  

1.MsgEnter: 

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

2. MsgList:

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

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

3.MsgMove:

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


 EventHandler刨析

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


 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字我真的头疼¥

猜你喜欢

转载自blog.csdn.net/m0_64810555/article/details/128068730