Unity遊戲框架設計之訊息管理器

珂霖發表於2024-04-29

Unity遊戲框架設計之訊息管理器

簡單介紹

訊息管理器又可以稱為工作管理員,主要解決延遲執行某些程式碼的問題。比如,我們希望一些程式碼可以延遲指定的時間後才執行,或者我們希望一些程式碼可以在固定的時間執行,又或者我們希望一些程式碼可以每隔一段時間就執行一次。訊息管理器就是為了實現上述功能而開發的。下述的訊息管理器是基於 Unity 的協程進行實現。

程式碼設計

public class MessageManager : SingletonMono<MessageManager>
{
    private readonly Dictionary<string, MessageConfig> _messageSet = new();

    private void CreateMessage<T>(string messageName, float delayTime, float intervalTime, int cycleCount, Action<T> action, T param)
    {
        if (FloatUtils.IsEqualsTo(intervalTime, 0f) && cycleCount <= -1)
        {
            LogManager.Instance.DebugWarning($"無法建立間隔時間為 0 的無限迴圈訊息。messageName={messageName}");
            return;
        }
        IEnumerator task = CycleMessage(messageName, 0f, action, param);
        MessageConfig messageConfig = new MessageConfig(task, delayTime, intervalTime, cycleCount);
        _messageSet.Add(messageName, messageConfig);
    }

    public void PublishDelayMessage<T>(string messageName, float delayTime, Action<T> action, T param)
    {
        CreateMessage(messageName, delayTime, float.MaxValue, 1, action, param);
        PublishMessage(messageName);
    }

    public void PublishFiniteCycleMessage<T>(string messageName, float delayTime, float intervalTime, int cycleCount, Action<T> action, T param)
    {
        CreateMessage(messageName, delayTime, intervalTime, cycleCount, action, param);
        PublishMessage(messageName);
    }

    public void PublishInfiniteCycleMessage<T>(string messageName, float delayTime, float intervalTime, Action<T> action, T param)
    {
        CreateMessage(messageName, delayTime, intervalTime, -1, action, param);
        PublishMessage(messageName);
    }

    public void PublishTimingMessage<T>(string messageName, float fixedTime, Action<T> action, T param)
    {
        CreateMessage(messageName, fixedTime - Time.realtimeSinceStartup, float.MaxValue, 1, action, param);
        PublishMessage(messageName);
    }

    private void PublishMessage(string messageName)
    {
        if (!_messageSet.ContainsKey(messageName))
        {
            LogManager.Instance.DebugWarning($"訊息不存在。messageName={messageName}");
            return;
        }
        MessageConfig messageConfig = _messageSet[messageName];
        StartCoroutine(SleepTime(messageConfig.DelayTime, () => StartCoroutine(messageConfig.Message)));
    }

    public void RemoveMessage(string messageName)
    {
        if (!_messageSet.ContainsKey(messageName))
        {
            LogManager.Instance.DebugWarning($"訊息不存在。messageName={messageName}");
            return;
        }
        MessageConfig messageConfig = _messageSet[messageName];
        StopCoroutine(messageConfig.Message);
        _messageSet.Remove(messageName);
    }

    public void RemoveMessageIfExists(string messageName)
    {
        if (_messageSet.ContainsKey(messageName))
        {
            MessageConfig messageConfig = _messageSet[messageName];
            _messageSet.Remove(messageName);
            StopCoroutine(messageConfig.Message);
        }
    }

    public bool ContainMessage(string messageName)
    {
        return _messageSet.ContainsKey(messageName);
    }

    private IEnumerator CycleMessage<T>(string messageName, float intervalTime, Action<T> action, T param)
    {
        if (!_messageSet.ContainsKey(messageName))
        {
            yield break;
        }
        MessageConfig messageConfig = _messageSet[messageName];
        if (messageConfig.CycleCount >= 1)
        {
            messageConfig.CycleCount--;
        }
        else if (messageConfig.CycleCount == 0)
        {
            _messageSet.Remove(messageName);
            yield break;
        }
        yield return new WaitForSeconds(intervalTime);
        action.Invoke(param);
        if (FloatUtils.IsEqualsTo(intervalTime, 0f) && FloatUtils.IsNotEqualsTo(messageConfig.IntervalTime, 0f))
        {
            messageConfig.Message = CycleMessage(messageName, messageConfig.IntervalTime, action, param);
        }
        else
        {
            messageConfig.Message = CycleMessage(messageName, intervalTime, action, param);
        }
        StartCoroutine(messageConfig.Message);
    }

    private IEnumerator SleepTime(float delayTime, Action callback)
    {
        if (FloatUtils.IsGreaterThan(delayTime, 0f))
        {
            yield return new WaitForSeconds(delayTime);
        }
        callback();
    }

    private class MessageConfig
    {
        public IEnumerator Message;
        public readonly float DelayTime;
        public readonly float IntervalTime;
        public int CycleCount;

        public MessageConfig(IEnumerator message, float delayTime, float intervalTime, int cycleCount)
        {
            Message = message;
            DelayTime = delayTime;
            IntervalTime = intervalTime;
            CycleCount = cycleCount;
        }
    }
}

程式碼說明

(一)實現延遲訊息(倒數計時器)、定時訊息(定時器)、有限迴圈訊息和無限迴圈訊息(迴圈排程器)。

(二)messageName 命名格式推薦 $"{ScriptName}{MethodName}{ID}" 。必須為所有的 messageName 新增唯一的 ID 字尾,防止出現衝突。唯一 ID 可以透過時間戳和遞增的計數器來實現。由於在 Unity 中是單執行緒執行的,因此不必考慮執行緒安全問題,也不必考慮加鎖問題。

(三)在傳遞的 action 引數中,必須先對當前執行環境進行檢查,才能執行訊息處理的程式碼。因為訊息可能是延遲的,而延遲後的執行環境可能並不滿足訊息處理的條件。比如訊息依賴的某些物件被禁用或銷燬,此時應當停止訊息處理,而是退出函式,甚至直接刪除訊息。

後記

由於個人能力有限,文中不免存在疏漏之處,懇求大家斧正,一起交流,共同進步。

相關文章