Implementación de la cola de mensajes.

【Prefacio】

La lógica principal del juego es generalmente de subproceso único, por lo que es muy simple implementar una cola de mensajes, a diferencia del desarrollo de Internet que involucra múltiples subprocesos y procesos. Puedes leer este artículo primero.

Comprensión de la función de devolución de llamada y el mecanismo de mensaje_Función de devolución de llamada de mensaje_Eternal Star Blog-CSDN Blog

Aquí intente analizar primero algunos elementos y luego escriba el código directamente en función de estos elementos, en lugar de analizar mientras escribe.

[Análisis 1 - Funciones básicas]

Dado que la cola de mensajes debe llamarse entre módulos, debe ser una clase estática que no se puede heredar ni usar en forma de singleton.

Para el remitente del mensaje (Remitente), debe haber un método SendMessage, y el parámetro en el método es el contenido del mensaje enviado.

Para el receptor del mensaje (Receptor), debe haber un método AddListener para escuchar el mensaje, y se requiere un método RemoveListener correspondiente. Obviamente, el receptor del mensaje debe pasar una función de devolución de llamada.

Puede haber mensajes de cualquier forma en la cola de mensajes, por lo que el contenido del mensaje debe estar representado por el tipo de objeto. Esto requiere una encapsulación de cualquier forma de mensaje enviado por el remitente del mensaje.

Para administrar mensajes en la cola de mensajes, se necesita una estructura de datos para almacenar en caché el mensaje, aquí la cola se usa directamente.

La cola de mensajes necesita reenviar el mensaje al receptor, por lo que el receptor debe retenerse, aquí está la función de devolución de llamada del receptor, y debe haber una estructura de datos para almacenar en caché, use directamente la lista.

Debido a que hay varios remitentes y receptores, es necesario distinguirlos. El receptor solo acepta mensajes del tipo especificado, pero debido a que se llama a través de módulos, el tipo de mensaje no se puede predefinir de antemano, por lo que la definición de el tipo de mensaje debe transferirse al mensaje de envío. Al definir el remitente específico, es decir, cuando SendMessage, se debe pasar un campo de tipo de mensaje como parámetro, y este parámetro puede ser un tipo de enumeración.

Después de agregar un parámetro, cuando la cola de mensajes obtiene el mensaje del remitente, además de almacenar en caché el contenido del mensaje, también necesita almacenar en caché el tipo de mensaje. Porque cuando el Tipo es cierto, el Contenido es incierto, es decir, el Remitente puede enviar mensajes del mismo tipo con diferentes contenidos, y se puede usar un diccionario para almacenar en caché ambos, es decir, Diccionario<Tipo, Cola<Contenido>> . Pero hacerlo destruye el principio básico de enviar mensajes en el orden en que llegan. Por lo tanto, para identificar el tipo de Contenido, los dos deben encapsularse en un nuevo tipo de datos.

Al reenviar el mensaje al Receptor, puede encontrar el Receptor correspondiente según el Tipo en el IMessage.Obviamente, el receptor de caché necesita un Diccionario.

En este punto, se ha implementado la cola de mensajes básicos, y el código se puede escribir primero y luego continuar con el análisis. el código se muestra a continuación:

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


public class MessageWrapper
{
    public MessageType type;
    public object content;
}

public enum MessageType
{
    None = 0,
    EnterGame = 1,
    ClickButton = 2,
    Kill = 3,
    //根据使用需要不断往下添加
}

public delegate void MessageCb(object content);
public sealed class MessageQueue 
{
   
    private static Queue<MessageWrapper> messages = new Queue<MessageWrapper>();
    private static Dictionary<MessageType, MessageCb> listeners = new Dictionary<MessageType,MessageCb>();

    public static void SendMessage(MessageType type,object content)
    {
        MessageWrapper messageWrapper = new MessageWrapper();
        messageWrapper.type = type;
        messageWrapper.content = content;
        messages.Enqueue(messageWrapper);
    }

    public static void AddListener(MessageType type, MessageCb cb)
    {
        if(listeners.TryGetValue(type,out var messageCb))
        {
            listeners[type] = messageCb + cb;//委托链
        }
        else
        {
            listeners.Add(type, cb);
        }

    }

    public static void RemoveListener(MessageType type, MessageCb cb)
    {
        if (listeners.TryGetValue(type, out var messageCb))
        {
            if(messageCb != null)
            {
                messageCb -= cb;
                listeners[type] = messageCb;
            }            
        }
    }

    public static void Tick()
    {
        while(messages.Count > 0)
        {
            var message = messages.Dequeue();//做好管理,这里一定不为空
            var listener = listeners[message.type];
            SendMessagerInternal(listener, message.content);
        }
    }

    private static void SendMessagerInternal(MessageCb listener, object content)
    {
        if(listener != null)
        {
            listener(content);
        }
    }
}

 [Análisis 2 - otras situaciones]

1. Cuándo enviar: aquí el mensaje se enviará en el siguiente cuadro, si es necesario enviarlo en el cuadro actual. Aunque el valor predeterminado es enviar el siguiente cuadro cuando se usan mensajes, aún debemos agregar algunos controles para esta situación. La variable bool se puede usar aquí, y un bool indica dos posibilidades, que solo distingue la necesidad de enviar inmediatamente y el siguiente cuadro.

2. Receptor repetido: si el mismo receptor llama a AddListener varias veces, cómo tratarlo. En lo que respecta a la implementación actual, no hay forma de distinguir los receptores duplicados. Si necesita distinguir, puede atravesar la cadena de delegación para encontrar Receptores duplicados, o puede eliminar y luego agregar para evitar Receptores duplicados. Mantenga ese principio aquí: para la llamada incorrecta, espero que el error se pueda corregir en lugar de ignorarlo. Así que averigüe qué Receptor está duplicado atravesando la cadena de delegación. Pero el recorrido requiere mucho tiempo.Si un mensaje tiene miles de receptores, llevará mucho tiempo. En el mecanismo de mensajes, el Remitente no puede saber directamente qué Receptores existen, pero la cola de mensajes sí, en este caso, el Receptor puede indicarse a sí mismo directamente en lugar de a través de una función de devolución de llamada. Aquí, permita que Receiver pase un nombre para identificarse cuando AddListener. La cola de mensajes necesita almacenar en caché adicionalmente este identificador.

3. Receptor vacío: AddListener y RemoveListener deben corresponder uno por uno, pero otras personas pueden olvidar escribirlo cuando lo usan, así que imprima el error directamente aquí.

4. Algunos módulos deben tener un procesamiento personalizado antes de enviar o recibir mensajes, y se debe agregar una función de enlace para que estos módulos realicen un procesamiento personalizado.

Algunas otras situaciones se han tratado aquí, y puede haber más situaciones que no se han considerado. Estas situaciones se pueden exponer gradualmente con el uso, y se pueden tratar de acuerdo con estas situaciones. Esto es principalmente la acumulación de experiencia. El código después de considerar otras situaciones es el siguiente:

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


public class MessageWrapper
{
    public MessageType type;
    public object content;
    public bool isSend;
}

public enum MessageType
{
    None = 0,
    EnterGame = 1,
    ClickButton = 2,
    Kill = 3,
    //根据使用需要不断往下添加
}

public delegate void MessageCb(object content);
public delegate void Hook(MessageWrapper message);
public sealed class MessageQueue 
{
   
    private static Queue<MessageWrapper> messages = new Queue<MessageWrapper>();
    private static Dictionary<MessageType, MessageCb> listenerCb = new Dictionary<MessageType,MessageCb>();

    private static Dictionary<MessageType,HashSet<string>> listenerNames = new Dictionary<MessageType,HashSet<string>>();
    private static Dictionary<string,Hook> name2hook = new Dictionary<string,Hook>();
    public static void SendMessage(MessageType type,object content,bool sync = false)
    {
        MessageWrapper messageWrapper = new MessageWrapper();
        messageWrapper.type = type;
        messageWrapper.content = content;
        messageWrapper.isSend = false;
        if(sync)
        {
            SendMessagerInternal(messageWrapper);
        }
        else
        {
            messages.Enqueue(messageWrapper);
        }
        
    }

    public static void AddListener(MessageType type, MessageCb cb,string name)
    {
        if(cb == null)
        {
            Debug.LogError("cb is null");
            return;
        }

        if(string.IsNullOrEmpty(name))
        {
            Debug.LogError("name is null or empty");
        }

        if(listenerCb.TryGetValue(type,out var messageCb))
        {
            if(listenerNames[type].Contains(name))
            {
                Debug.LogError($"receiver is repeated for {type}");
            }
            else
            {
                listenerCb[type] = messageCb + cb;//委托链   
                listenerNames[type].Add(name);
            }
                    
        }
        else
        {
            listenerCb.Add(type, cb);
            listenerNames.Add(type, new HashSet<string> { name });
        }

    }

    public static void RemoveListener(MessageType type, MessageCb cb,string name)
    {
        if (listenerCb.TryGetValue(type, out var messageCb))
        {
            if(listenerNames[type].Contains(name))
            {
                if (messageCb != null)
                {
                    messageCb -= cb;
                    listenerCb[type] = messageCb;
                }
                listenerNames[type].Remove(name);
            }
            else
            {
                Debug.LogWarning($"there is no receiver for {type}");
            }    
        }
    }

    public static void AddHook(string name, Hook hook)
    {
        if (!name2hook.ContainsKey(name))
        {
            name2hook.Add(name, hook);
        }
        else
        {
            Debug.LogWarning($"hook of {name} is existed");
        }
    }

    public static void RemoveHook(string name)
    {
        if (name2hook.ContainsKey(name))
        {
            name2hook.Remove(name);
        }
        else
        {
            Debug.LogWarning($"hook of {name} is not existed");
        }
    }

    public static void Tick()
    {
        while(messages.Count > 0)
        {
            var message = messages.Dequeue();//做好管理,这里可以省去一些判断           
            SendMessagerInternal(message);
        }
    }
    public static void Dispose()
    {
        messages.Clear();
        listenerCb.Clear();
        listenerNames.Clear();
    }

    private static void SendMessagerInternal(MessageWrapper message)
    {
        foreach (var item in name2hook.Values)
        {
            item.Invoke(message);
        }
        if(listenerCb.TryGetValue(message.type,out var listener))
        {
            if (listener != null && !message.isSend)
            {
                listener(message.content);
                message.isSend = true;
            }
            else
            {
                listenerNames.Remove(message.type);
                Debug.LogError($"listener is null for {message.type}");
            }
        }
        else
        {
            Debug.LogWarning($"there is no receiver for {message.type}");
        }
        
    }

    
}

[Análisis 3 - rendimiento y memoria] 

 1. El mensaje se renueva cada vez, y la cola de mensajes como un módulo básico, la frecuencia de uso será muy alta, la sobrecarga de rendimiento de la renovación cada vez no se puede ignorar, y también causará la fragmentación de la memoria, debe agregar un Pool to Processing, este Pool se coloca directamente sobre sí mismo. La determinación de la capacidad inicial del grupo debe determinarse de acuerdo con la situación de uso real, y la capacidad inicial se obtiene contando el valor máximo del uso real. Sólo da uno aquí.

2. Después de invocar cada mensaje enviado, es posible que el receptor tenga que realizar una gran cantidad de procesamiento. Si hay demasiadas invocaciones en cada marco, puede que este marco tarde demasiado, por lo que puede establecer un límite en los mensajes enviados. en cada fotograma, es decir, procesamiento de fotogramas. El número específico de procesamiento por cuadro debe combinarse con el proyecto real, y aquí hay solo uno.

3. El contenido del mensaje es de tipo objeto. Se pueden producir cajas y desembalajes, lo que da como resultado GC. Debe modificarse a un formulario que no se desembale ni se descomponga. El método consiste en agregar un nuevo campo genérico a la memoria caché. Al mismo tiempo, el MessageWapper se pasa al Receptor, y el Receptor utiliza este campo para obtener el contenido del mensaje.

Teniendo en cuenta el rendimiento y la memoria, la situación es la siguiente:

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


public interface IMessage 
{
    MessageType type { get;set; }
    object content { get; set; }
    
    bool isSend { get; set; }

    void Release();

}
public class MessageWrapper<TContent> : IMessage
{
    private MessageType _type;
    public MessageType type
    {
        get { return _type; }
        set { _type = value; }
    }

    private object _content;
    public object content
    {
        get { return _content; }
        set { _content = value; }
    }

    private bool _isSend;
    public bool isSend
    {
        get { return isSend; }
        set  {isSend = value;}
    }


    private TContent _ncontent;
    public TContent ncontent
    {
        get { return _ncontent; }
        set { _ncontent = value; }
    }

    private void Clear()
    {
        _content =null; 
        _isSend = false;
    }

    public void Release()
    {
        Clear();
        Release(this);
    }


    private static Queue<MessageWrapper<TContent>> messagePool = new Queue<MessageWrapper<TContent>>(Enumerable.Range(0, 10).Select(i => new MessageWrapper<TContent>()));

    public static MessageWrapper<TContent> Allocate()
    {
        MessageWrapper<TContent> result = null;
        if (messagePool.Count > 0)
        {
            result = messagePool.Dequeue();
        }
        else
        {
            result = new MessageWrapper<TContent>();
        }
        return result;
    }
    private static void Release(MessageWrapper<TContent> message)
    {
        if (message == null) return;
        messagePool.Enqueue(message);
    }

}

public enum MessageType
{
    None = 0,
    EnterGame = 1,
    ClickButton = 2,
    Kill = 3,
    //根据使用需要不断往下添加
}

public delegate void MessageCb(IMessage message);
public delegate void Hook(IMessage message);
public sealed class MessageQueue 
{
    private static readonly int MAX_COUNT_MESSAGE = 20;
    private static Queue<IMessage> messages = new Queue<IMessage>();
    private static Dictionary<MessageType, MessageCb> listenerCb = new Dictionary<MessageType,MessageCb>();

    private static Dictionary<MessageType,HashSet<string>> listenerNames = new Dictionary<MessageType,HashSet<string>>();
    private static Dictionary<string,Hook> name2hook = new Dictionary<string,Hook>();
    public static void SendMessage(MessageType type,object content = null,bool sync = false)
    {
        MessageWrapper<object> messageWrapper = MessageWrapper<object>.Allocate();
        messageWrapper.type = type;
        messageWrapper.content = content;
        messageWrapper.isSend = false;
        if(sync)
        {
            SendMessagerInternal(messageWrapper);
            curCount++;
        }
        else
        {
            messages.Enqueue(messageWrapper);
        }
        
    }

    public static void SendMessage<TContent>(MessageType type, TContent content = default, bool sync = false)
    {
        MessageWrapper<TContent> messageWrapper = MessageWrapper<TContent>.Allocate();
        messageWrapper.type = type;
        messageWrapper.ncontent = content;
        messageWrapper.isSend = false;
        if (sync)
        {
            SendMessagerInternal(messageWrapper);
            curCount++;
        }
        else
        {
            messages.Enqueue(messageWrapper);
        }

    }

    public static void AddListener(MessageType type, MessageCb cb,string name)
    {
        if(cb == null)
        {
            Debug.LogError("cb is null");
            return;
        }

        if(string.IsNullOrEmpty(name))
        {
            Debug.LogError("name is null or empty");
        }

        if(listenerCb.TryGetValue(type,out var messageCb))
        {
            if(listenerNames[type].Contains(name))
            {
                Debug.LogError($"receiver is repeated for {type}");
            }
            else
            {
                listenerCb[type] = messageCb + cb;//委托链   
                listenerNames[type].Add(name);
            }
                    
        }
        else
        {
            listenerCb.Add(type, cb);
            listenerNames.Add(type, new HashSet<string> { name });
        }

    }

    public static void RemoveListener(MessageType type, MessageCb cb,string name)
    {
        if (listenerCb.TryGetValue(type, out var messageCb))
        {
            if(listenerNames[type].Contains(name))
            {
                if (messageCb != null)
                {
                    messageCb -= cb;
                    listenerCb[type] = messageCb;
                }
                listenerNames[type].Remove(name);
            }
            else
            {
                Debug.LogWarning($"there is no receiver for {type}");
            }    
        }
    }

    public static void AddHook(string name, Hook hook)
    {
        if (!name2hook.ContainsKey(name))
        {
            name2hook.Add(name, hook);
        }
        else
        {
            Debug.LogWarning($"hook of {name} is existed");
        }
    }

    public static void RemoveHook(string name)
    {
        if (name2hook.ContainsKey(name))
        {
            name2hook.Remove(name);
        }
        else
        {
            Debug.LogWarning($"hook of {name} is not existed");
        }
    }

    private static int curCount = 0;
    public static void Tick()
    {
        while(messages.Count > 0)
        {
            curCount++;
            if (curCount > MAX_COUNT_MESSAGE)
            {
                curCount = 0;
                break;
            }
            var message = messages.Dequeue();//做好管理,这里可以省去一些判断           
            SendMessagerInternal(message);
            
        }
    }
    public static void Dispose()
    {
        foreach (var item in messages)
        {
            item.Release();
        }
        messages.Clear();
        listenerCb.Clear();
        listenerNames.Clear();
    }

    private static void SendMessagerInternal(IMessage message)
    {
        foreach (var item in name2hook.Values)
        {
            item.Invoke(message);
        }
        if(listenerCb.TryGetValue(message.type,out var listener))
        {
            if (listener != null && !message.isSend)
            {
                message.isSend = true;
                listener(message);
            }
            else
            {
                listenerNames.Remove(message.type);
                Debug.LogError($"listener is null for {message.type}");
            }
        }
        else
        {
            Debug.LogWarning($"there is no receiver for {message.type}");
        }
        message.Release();
    }

    
}

【Resumir】

 Para implementar una cola de mensajes, primero implemente sus funciones básicas, luego considere varias situaciones de uso, realice modificaciones adicionales sobre la base de las funciones básicas y optimice el rendimiento y la memoria.

Supongo que te gusta

Origin blog.csdn.net/enternalstar/article/details/129935351
Recomendado
Clasificación