# 观前提醒

项目会在 GitHub 中开源,链接:https://github.com/Maikire/UnityGameDemo/tree/main/A simple web chat room

如果有什么问题或想法,欢迎各位在评论区留言。

# 网络聊天室

本章将制作一个基于 UDP 的网络聊天室,客户端与服务端都用 Unity 来实现(实际的开发中,服务端一定不是用 Unity 做的)

需求:

  • 用户可以自由选择登录服务端或客户端。
  • 客户端:输入名字、服务端的 IP 和端口后即可进行多人聊天。
  • 服务端:输入服务端的 IP 和端口后即可开启服务,无法进行聊天,可以看到谁在线,以及他们的 IP 和端口

# 需求分析

  • 使用 ClientSever 架构
    img
  • 向服务端发送数据时,不能发送自定义的类,只能发送基本的数据类型,例如:bool/int/float/string...
  • 自定义协议:按照指定的格式发送数据和读取数据
    img

具体内容

  1. 发送的消息可能有很多种,使用枚举区分
  2. 将消息的内容封装到一个类中,自定义协议,提供字节数组与对象之间的转换的功能
  3. 制作客户端网络服务类,实现接收和发送消息的功能,将网络部分与其他部分隔离
  4. 实现登录功能,通知服务端上线
  5. 制作客户端的脚本,实现基本的功能
  6. 制作服务端网络服务类,与客户端网络服务类相同
  7. 制作服务端的脚本,实现基本的功能
  8. 仅有三个类是最重要的:消息内容、客户端网络服务类、服务端网络服务类。其他的类都不是重点。

img

# 代码部分

# 消息类型

代码如下:

MessageType
namespace ChatDemo
{
    /// <summary>
    /// 消息类型
    /// </summary>
    public enum MessageType
    {
        /// <summary>
        /// 上线
        /// </summary>
        OnLine,
        /// <summary>
        /// 下线
        /// </summary>
        OffLine,
        /// <summary>
        /// 普通消息
        /// </summary>
        General,
    }
}

# 消息内容

  • 封装聊天信息
  • 字节数组的转换:可以单独处理每个数据类型的转换,但是这样做比较麻烦。所以,本文使用内存流、二进制写入器、二进制读取器

代码如下:

ChatMessage
using System;
using System.IO;
using System.Text;
namespace ChatDemo
{
    /// <summary>
    /// 聊天信息
    /// </summary>
    public class ChatMessage
    {
        /// <summary>
        /// 消息类型
        /// </summary>
        public MessageType Type;
        /// <summary>
        /// 发送者
        /// </summary>
        public string SenderName;
        /// <summary>
        /// 发送内容
        /// </summary>
        public string Content;
        /// <summary>
        /// ChatMessage
        /// </summary>
        public ChatMessage() { }
        /// <summary>
        /// 赋值
        /// </summary>
        /// <param name="messageType">messageType</param>
        /// <param name="senderName">senderName</param>
        /// <param name="content">content</param>
        public ChatMessage(MessageType messageType, string senderName, string content)
        {
            Type = messageType;
            SenderName = senderName;
            Content = content;
        }
        /// <summary>
        /// object -> byte[]
        /// </summary>
        /// <returns></returns>
        public byte[] ObjectToBytes()
        {
            //string/int/bool... -- 二进制写入器 -- 内存流 --> byte []
            // 内存流
            using (MemoryStream stream = new MemoryStream())
            {
                // 二进制写入器
                // 属性 -> 内存流
                BinaryWriter writer = new BinaryWriter(stream);
                WriteString(writer, Type.ToString());
                WriteString(writer, SenderName);
                WriteString(writer, Content);
                // 内存流 -> byte []
                return stream.ToArray();
            }
        }
        /// <summary>
        /// 写入二进制数据
        /// </summary>
        /// <param name="writer">writer</param>
        /// <param name="str">string</param>
        private void WriteString(BinaryWriter writer, string str)
        {
            // 不用这个,因为这个只支持一种编码
            //writer.Write(str);
            // 编码
            byte[] byte_Type = Encoding.UTF8.GetBytes(str);
            // 写入长度
            writer.Write(byte_Type.Length);
            // 写入内容
            writer.Write(byte_Type);
        }
        /// <summary>
        /// byte[] -> object
        /// </summary>
        /// <param name="bytes">bytes</param>
        /// <returns></returns>
        public static ChatMessage BytesToObject(byte[] bytes)
        {
            //byte [] -- 内存流 -- 二进制读取器 --> string/int/bool...
            using (MemoryStream stream = new MemoryStream(bytes))
            {
                BinaryReader reader = new BinaryReader(stream);
                MessageType type = (MessageType)Enum.Parse(typeof(MessageType), ReadString(reader));
                string senderName = ReadString(reader);
                string content = ReadString(reader);
                return new ChatMessage(type, senderName, content);
            }
        }
        /// <summary>
        /// 读取二进制数据
        /// </summary>
        /// <param name="reader">reader</param>
        /// <returns></returns>
        private static string ReadString(BinaryReader reader)
        {
            // 不用这个,因为这个只支持一种编码
            //reader.ReadString();
            // 读 4 个字节:标记了接下来要读取的长度
            int length = reader.ReadInt32();
            // 读指定的长度
            byte[] bytes = reader.ReadBytes(length);
            // 转字符串
            return Encoding.UTF8.GetString(bytes);
        }
    }
}

# 登录信息类

  • 储存登录的信息,例如:IP、端口、名字

代码如下:

LogInContent
namespace ChatDemo
{
    /// <summary>
    /// 登录信息
    /// </summary>
    public class LogInContent
    {
        /// <summary>
        /// Name
        /// </summary>
        public string Name;
        /// <summary>
        /// IP
        /// </summary>
        public string IP;
        /// <summary>
        /// Port
        /// </summary>
        public string Port;
    }
}

# 客户端网络服务类

  • 这个类只需要一个,所以使用单例模式
  • 使用 UDP 协议
  • 创建一个线程,用于接收消息
  • 使用事件处理消息的接收:接收到消息后触发事件并传递参数
  • 辅助线程不能访问 Unity 的 API,需要使用线程交叉访问助手解决这个问题

事件参数类:

ReceiveEventArgs
using System;
using System.Net;
namespace ChatDemo
{
    /// <summary>
    /// 接收事件的信息类
    /// </summary>
    public class ReceiveEventArgs : EventArgs
    {
        /// <summary>
        /// ChatMessage
        /// </summary>
        public ChatMessage Message;
        /// <summary>
        /// IPEndPoint
        /// </summary>
        public IPEndPoint SourceRemote;
        public ReceiveEventArgs() { }
        public ReceiveEventArgs(ChatMessage message, IPEndPoint sourceRemote)
        {
            Message = message;
            SourceRemote = sourceRemote;
        }
    }
}

客户端网络服务类:

ClientUdpNetService
using Common;
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace ChatDemo
{
    /// <summary>
    /// 客户端 UDP 网络服务
    /// </summary>
    public class ClientUdpNetService : MonoSingleton<ClientUdpNetService>
    {
        /// <summary>
        /// 接收到消息后触发
        /// </summary>
        public event EventHandler<ReceiveEventArgs> OnReceive;
        /// <summary>
        /// 登录信息
        /// </summary>
        public LogInContent Content;
        /// <summary>
        /// UdpClient
        /// </summary>
        private UdpClient UDPClient;
        /// <summary>
        /// 客户端线程
        /// </summary>
        private Thread ClientThread;
        private void Start()
        {
            ClientThread = new Thread(ReceiveMessage);
        }
        /// <summary>
        /// 初始化
        /// 由登录窗口传递 IP 和端口
        /// </summary>
        /// <param name="content"> 登录信息 & lt;/param>
        public void Initialize(LogInContent content)
        {
            Content = content;
            // 随机分配端口
            UDPClient = new UdpClient();
            // 与服务端连接(仅仅是配置自身的 Socket)
            // 单播:只能给服务器发数据
            UDPClient.Connect(IPAddress.Parse(Content.IP), int.Parse(Content.Port));
            // 开启线程
            ClientThread.Start();
            NotiyfSever(MessageType.OnLine);
        }
        private void NotiyfSever(MessageType type)
        {
            if (Content == null) return;
            SendMessage(new ChatMessage(type, Content.Name, string.Empty));
        }
        /// <summary>
        /// 发送数据
        /// </summary>
        /// <param name="message"> 信息 & lt;/param>
        public void SendMessage(ChatMessage message)
        {
            byte[] bytes = message.ObjectToBytes();
            // 创建 Socket 对象时建立了链接,所以不能绑定终结点
            UDPClient.Send(bytes, bytes.Length);
        }
        /// <summary>
        /// 接收消息
        /// </summary>
        /// <param name="message"></param>
        private void ReceiveMessage()
        {
            while (true)
            {
                // 任意 IP 地址,任意端口
                IPEndPoint sourceRemote = new IPEndPoint(IPAddress.Any, 0);
                // 接收消息
                byte[] bytes = UDPClient.Receive(ref sourceRemote);
                // 转化消息
                ChatMessage message = ChatMessage.BytesToObject(bytes);
                // 事件参数
                ReceiveEventArgs args = new ReceiveEventArgs(message, sourceRemote);
                // 在主线程中触发事件
                ThreadCrossHelper.Instance.ExecuteOnMainThread(() =>
                {
                    OnReceive?.Invoke(this, args);
                });
            }
        }
        private void OnApplicationQuit()
        {
            NotiyfSever(MessageType.OffLine);
            ClientThread?.Abort();
            UDPClient?.Dispose();
        }
    }
}

# 登录功能

  • 登录界面分两部分制作,点击按钮切换
    img
    img
    img
  • 使用变换组件助手类寻找物体
  • 注册按钮事件,切换场景
  • 保存用户输入的信息
  • 会涉及到服务端网络服务类的代码,它与客户端网络服务类的代码相似,具体内容会在下文中讲到

登录信息输入,分别挂载到物体 “ClientContent” 和 “SeverContent” 上

LogInInput
using Common;
using TMPro;
using UnityEngine;
namespace ChatDemo
{
    /// <summary>
    /// 登录信息输入
    /// </summary>
    public class LogInInput : MonoBehaviour
    {
        /// <summary>
        /// Input_Name
        /// </summary>
        private TMP_InputField Input_Name;
        /// <summary>
        /// Input_IP
        /// </summary>
        private TMP_InputField Input_IP;
        /// <summary>
        /// Input_Port
        /// </summary>
        private TMP_InputField Input_Port;
        /// <summary>
        /// 登录信息
        /// </summary>
        public LogInContent Content
        {
            get
            {
                return new LogInContent()
                {
                    Name = Input_Name?.text,
                    IP = Input_IP.text,
                    Port = Input_Port.text,
                };
            }
        }
        private void Awake()
        {
            Input_Name = this.transform.FindChildByName("Input_Name")?.GetComponent<TMP_InputField>();
            Input_IP = this.transform.FindChildByName("Input_IP").GetComponent<TMP_InputField>();
            Input_Port = this.transform.FindChildByName("Input_Port").GetComponent<TMP_InputField>();
        }
    }
}

登录按钮的处理,挂载到物体 “Canvas” 上

LogInButtons
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
namespace ChatDemo
{
    /// <summary>
    /// LogInButton
    /// </summary>
    public class LogInButtons : MonoBehaviour
    {
        /// <summary>
        /// LogInType
        /// </summary>
        private enum LogInType
        {
            /// <summary>
            /// 客户端
            /// </summary>
            Client,
            /// <summary>
            /// 服务端
            /// </summary>
            Sever,
        }
        [Header("Client")]
        [Tooltip("客户端登录")]
        public Button Client;
        [Tooltip("ClientContent")]
        public GameObject ClientContent;
        [Tooltip("ClientImage")]
        private Image ClientImage;
        [Header("Sever")]
        [Tooltip("服务端登录")]
        public Button Sever;
        [Tooltip("SeverContent")]
        public GameObject SeverContent;
        [Tooltip("SeverImage")]
        private Image SeverImage;
        [Header("Enter")]
        [Tooltip("登录按钮")]
        public Button Enter;
        /// <summary>
        /// LogInType
        /// </summary>
        private LogInType LogType;
        private void Awake()
        {
            // 考虑到遍历物体的性能开销,所以不用代码找物体
            ClientImage = Client.GetComponent<Image>();
            SeverImage = Sever.GetComponent<Image>();
        }
        private void Start()
        {
            LogType = LogInType.Client;
            Client.onClick.AddListener(OnClientButtonClick);
            Sever.onClick.AddListener(OnServerButtonClick);
            Enter.onClick.AddListener(OnEnterButtonClick);
        }
        /// <summary>
        /// OnClientButtonClick
        /// </summary>
        private void OnClientButtonClick()
        {
            ClientImage.color = Color.red;
            ClientContent.SetActive(true);
            SeverImage.color = Color.white;
            SeverContent.SetActive(false);
            LogType = LogInType.Client;
        }
        /// <summary>
        /// OnServerButtonClick
        /// </summary>
        private void OnServerButtonClick()
        {
            ClientImage.color = Color.white;
            ClientContent.SetActive(false);
            SeverImage.color = Color.red;
            SeverContent.SetActive(true);
            LogType = LogInType.Sever;
        }
        /// <summary>
        /// OnEnterButtonClick
        /// </summary>
        private void OnEnterButtonClick()
        {
            switch (LogType)
            {
                case LogInType.Client:
                    GoClient();
                    break;
                case LogInType.Sever:
                    GoServer();
                    break;
                default:
                    break;
            }
        }
        /// <summary>
        /// GoClient
        /// </summary>
        private void GoClient()
        {
            LogInInput logInInput = ClientContent.GetComponent<LogInInput>();
            ClientUdpNetService.Instance.Initialize(logInInput.Content);
            SceneManager.LoadScene("ChatDemoClient", LoadSceneMode.Single);
        }
        /// <summary>
        /// GoServer
        /// </summary>
        private void GoServer()
        {
            LogInInput logInInput = SeverContent.GetComponent<LogInInput>();
            SeverUdpNetService.Instance.Initialize(logInInput.Content);
            SceneManager.LoadScene("ChatDemoSever", LoadSceneMode.Single);
        }
    }
}

# 客户端的脚本

  • 制作 UI 界面
    img
    img
  • 接收消息、发送消息、生成消息(预制件)

消息的拥有者

MessageOwer
namespace ChatDemo
{
    /// <summary>
    /// 消息的拥有者
    /// </summary>
    public enum MessageOwer
    {
        My,
        Other
    }
}

挂载到预制件上

ClientMessageManger
using Common;
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace ChatDemo
{
    /// <summary>
    /// 客户端信息管理
    /// </summary>
    public class ClientMessageManger : MonoBehaviour
    {
        /// <summary>
        /// 设置信息
        /// </summary>
        /// <param name="message"> 信息 & lt;/param>
        /// <param name="name"> 发送者 & lt;/param>
        public void SetMessage(string message, string name)
        {
            this.transform.FindChildByName("Message").GetComponent<TextMeshProUGUI>().text = message;
            this.transform.FindChildByName("Name").GetComponent<TextMeshProUGUI>().text = name;
            this.transform.FindChildByName("Time").GetComponent<TextMeshProUGUI>().text = DateTime.Now.ToString();
        }
    }
}

挂载到 “Content” 上

CreateClientMessage
using UnityEngine;
namespace ChatDemo
{
    /// <summary>
    /// 创建客户端消息
    /// </summary>
    public class CreateClientMessage : MonoBehaviour
    {
        [Tooltip("我的消息预制件")]
        public GameObject MessagePrefab_My;
        [Tooltip("其他人的消息预制件")]
        public GameObject MessagePrefab_Other;
        /// <summary>
        /// 增加新消息
        /// </summary>
        /// <param name="ower"> 消息属于谁 & lt;/param>
        /// <param name="chatMessage"> 消息 & lt;/param>
        public void AddMessage(MessageOwer ower, ChatMessage chatMessage)
        {
            switch (ower)
            {
                case MessageOwer.My:
                    Create(MessagePrefab_My, chatMessage);
                    break;
                case MessageOwer.Other:
                    Create(MessagePrefab_Other, chatMessage);
                    break;
                default:
                    break;
            }
        }
        /// <summary>
        /// 创建物体
        /// </summary>
        /// <param name="prefab"> 预制件 & lt;/param>
        /// <param name="chatMessage"> 信息 & lt;/param>
        private void Create(GameObject prefab, ChatMessage chatMessage)
        {
            GameObject obj = Instantiate(prefab, this.transform);
            obj.GetComponent<ClientMessageManger>().SetMessage(chatMessage.Content, chatMessage.SenderName);
        }
    }
}

挂载到 “Canvas” 上

ChatClient
using Common;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace ChatDemo
{
    /// <summary>
    /// 客户端逻辑
    /// </summary>
    public class ChatClient : MonoBehaviour
    {
        [Tooltip("增加消息")]
        public CreateClientMessage Create;
        /// <summary>
        /// SendButton
        /// </summary>
        private Button SendButton;
        /// <summary>
        /// 输入的信息
        /// </summary>
        private TMP_InputField InputMessage;
        private void Awake()
        {
            SendButton = this.transform.FindChildByName("SendButton").GetComponent<Button>();
            InputMessage = this.transform.FindChildByName("InputMessage").GetComponent<TMP_InputField>();
        }
        private void Update()
        {
            if (Input.GetKey(KeyCode.LeftControl) && Input.GetKey(KeyCode.Return))
            {
                OnSendMessage();
            }
        }
        private void OnEnable()
        {
            SendButton.onClick.AddListener(OnSendMessage);
            ClientUdpNetService.Instance.OnReceive += OnReceiveMessage;
        }
        private void OnDisable()
        {
            SendButton.onClick.RemoveListener(OnSendMessage);
            ClientUdpNetService.Instance.OnReceive -= OnReceiveMessage;
        }
        /// <summary>
        /// 按下发送消息按钮
        /// </summary>
        private void OnSendMessage()
        {
            if (InputMessage.text == string.Empty) return;
            ChatMessage message = new ChatMessage()
            {
                Type = MessageType.General,
                SenderName = ClientUdpNetService.Instance.Content.Name,
                Content = InputMessage.text
            };
            Create.AddMessage(MessageOwer.My, message);
            ClientUdpNetService.Instance.SendMessage(message);
            InputMessage.text = string.Empty;
        }
        /// <summary>
        /// 接收消息
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="receive"></param>
        private void OnReceiveMessage(object sender, ReceiveEventArgs receive)
        {
            if (receive.Message.SenderName != ClientUdpNetService.Instance.Content.Name)
            {
                Create.AddMessage(MessageOwer.Other, receive.Message);
            }
        }
    }
}

# 服务端网络服务类

  • 需要分配 IP 和端口
  • 使用列表储存所有已经上线的客户端
  • 其他部分与客户端网络服务类类似

代码如下:

SeverUdpNetService
using Common;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace ChatDemo
{
    /// <summary>
    /// 服务端 UDP 网络服务
    /// </summary>
    public class SeverUdpNetService : MonoSingleton<SeverUdpNetService>
    {
        /// <summary>
        /// 接收到消息后触发
        /// </summary>
        public event EventHandler<ReceiveEventArgs> OnReceive;
        /// <summary>
        /// 登录信息
        /// </summary>
        public LogInContent Content;
        /// <summary>
        /// UdpClient
        /// </summary>
        private UdpClient UDPClient;
        /// <summary>
        /// 服务端线程
        /// </summary>
        private Thread SeverThread;
        /// <summary>
        /// 客户端列表
        /// </summary>
        public List<IPEndPoint> ClientRemotes { get; private set; }
        private void Start()
        {
            ClientRemotes = new List<IPEndPoint>();
            SeverThread = new Thread(ReceiveMessage);
        }
        /// <summary>
        /// 初始化
        /// 由登录窗口传递 IP 和端口
        /// </summary>
        /// <param name="content"> 登录信息 & lt;/param>
        public void Initialize(LogInContent content)
        {
            Content = content;
            // 分配端口
            IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Parse(Content.IP), int.Parse(Content.Port));
            UDPClient = new UdpClient(iPEndPoint);
            // 开启线程
            SeverThread.Start();
        }
        /// <summary>
        /// 发送数据
        /// </summary>
        /// <param name="message"> 信息 & lt;/param>
        /// <param name="remote"> 目标终结点 & lt;/param>
        public void SendMessage(ChatMessage message, IPEndPoint remote)
        {
            byte[] bytes = message.ObjectToBytes();
            UDPClient.Send(bytes, bytes.Length, remote);
        }
        /// <summary>
        /// 接收消息
        /// </summary>
        private void ReceiveMessage()
        {
            while (true)
            {
                // 任意 IP 地址,任意端口
                IPEndPoint sourceRemote = new IPEndPoint(IPAddress.Any, 0);
                // 接收消息
                byte[] bytes = UDPClient.Receive(ref sourceRemote);
                // 转化消息
                ChatMessage message = ChatMessage.BytesToObject(bytes);
                // 根据消息类型执行对应的逻辑
                MessageReceived(message, sourceRemote);
                // 事件参数
                ReceiveEventArgs args = new ReceiveEventArgs(message, sourceRemote);
                // 在主线程中触发事件
                ThreadCrossHelper.Instance.ExecuteOnMainThread(() =>
                {
                    OnReceive?.Invoke(this, args);
                });
            }
        }
        /// <summary>
        /// 根据消息类型执行对应的逻辑
        /// </summary>
        /// <param name="message"></param>
        /// <param name="remote"></param>
        private void MessageReceived(ChatMessage message, IPEndPoint remote)
        {
            switch (message.Type)
            {
                case MessageType.OnLine:
                    ClientRemotes.Add(remote);
                    break;
                case MessageType.OffLine:
                    ClientRemotes.Remove(remote);
                    break;
                case MessageType.General:
                    ClientRemotes.ForEach((item) => { SendMessage(message, item); });
                    break;
                default:
                    break;
            }
        }
        private void OnApplicationQuit()
        {
            SeverThread?.Abort();
            UDPClient?.Dispose();
        }
    }
}

# 服务端的脚本

  • 制作 UI
    img
    img
  • 接收消息、生成消息(预制件)
  • 消息的预制件会频繁地创建和销毁,可以使用对象池

挂载到预制件上

SeverMessageManger
using Common;
using System.Net;
using TMPro;
using UnityEngine;
namespace ChatDemo
{
    /// <summary>
    /// 服务端信息管理
    /// </summary>
    public class SeverMessageManger : MonoBehaviour
    {
        /// <summary>
        /// 设置信息
        /// </summary>
        /// <param name="sourceRemote">source remote</param>
        public void SetMessage(IPEndPoint sourceRemote)
        {
            this.transform.FindChildByName("SourceIPInformation").GetComponent<TextMeshProUGUI>().text = sourceRemote.Address.ToString();
            this.transform.FindChildByName("SourcePortInformation").GetComponent<TextMeshProUGUI>().text = sourceRemote.Port.ToString();
        }
    }
}

挂载到 “Content” 上

CreateSeverMessage
using Common;
using System.Net;
using UnityEngine;
namespace ChatDemo
{
    /// <summary>
    /// 创建服务端消息
    /// </summary>
    public class CreateSeverMessage : MonoBehaviour
    {
        [Tooltip("服务端消息预制件")]
        public GameObject SeverMessage;
        public void AddMessage(IPEndPoint sourceRemote)
        {
            for (int i = 0; i < this.transform.childCount; i++)
            {
                ObjectPool.Instance.RecoverGameObject(this.transform.GetChild(i).gameObject);
            }
            for (int i = 0; i < SeverUdpNetService.Instance.ClientRemotes.Count; i++)
            {
                GameObject obj = ObjectPool.Instance.GetGameObject("SeverMessage", SeverMessage, this.transform);
                obj.GetComponent<SeverMessageManger>().SetMessage(sourceRemote);
            }
        }
    }
}

挂载到 “SeverWindow” 上

ChatSever
using UnityEngine;
namespace ChatDemo
{
    /// <summary>
    /// 服务端逻辑
    /// </summary>
    public class ChatSever : MonoBehaviour
    {
        [Tooltip("增加消息")]
        public CreateSeverMessage Create;
        private void OnEnable()
        {
            SeverUdpNetService.Instance.OnReceive += OnReceiveMessage;
        }
        private void OnDisable()
        {
            SeverUdpNetService.Instance.OnReceive -= OnReceiveMessage;
        }
        /// <summary>
        /// 接收消息
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="receive"></param>
        private void OnReceiveMessage(object sender, ReceiveEventArgs receive)
        {
            if (receive.Message.Type != MessageType.General)
            {
                Create.AddMessage(receive.SourceRemote);
            }
        }
    }
}

# 切换场景不销毁物体

  • 客户端网络服务类、服务端网络服务类、线程交叉访问助手类都挂载到同一个物体上
  • 在切换场景后还需要使用这个物体,所以不能销毁

img

代码如下:

KeepObject
using UnityEngine;
namespace ChatDemo
{
    /// <summary>
    /// 切换场景保留物体
    /// </summary>
    public class KeepObject : MonoBehaviour
    {
        private void Start()
        {
            DontDestroyOnLoad(this.gameObject);
        }
    }
}

# 类图

img
img

# 测试

img
img