游戲隨筆之事件系統的設計


  新的一年,新的開始,祝願大家新春快樂!轉載請標明出處:http://www.cnblogs.com/zblade/

  今天,開始寫新的一年的博客了,我的博客更新的比較隨意,也比較緩慢,哈哈。前段時間一直在看一些D3D11的博客,自己也在摸索各個API的使用,不過今天要寫的是一篇游戲中的事件系統的設計,具體的事件系統的設計模式(觀察者模式),我就不再贅述了,網上有很多相關資料,可以自行查閱學習了解一下相關的原理,比較簡單易懂。

  在游戲開發中,我們經常會和服務器進行信息的交互,比如玩家點擊了商城里面的領取獎勵的按鈕,此時客戶端會發送一個領取的協議給服務器,服務器校驗通過后,會下發給客戶端,客戶端在收到消息后,會重新刷新一下相關的數據,那么這些數據的刷新怎么表現在UI上面呢,就需要我們通過事件系統來通知UI層進行相關的刷新操作了。

  最常見的事件系統,就是我上面說到的,將數據層和UI層進行分離,通過事件系統的調用來實現耦合的拆分。這在游戲中有較多的應用,那么,我們今天先說一下這種事件系統的設計模式吧。

一、常見事件系統的設計

  要設計一個事件系統,一般會暴露最常見的三個接口:Add/Remove/Trigger,分別負責事件的添加,移除和觸發。依然用Lua來實現這樣的一個接口,我們可以用幾個table來實現一個基本的事件系統的三個接口:

local events = {} local eventHandleId = 0
local eventHandles = {} --設置兩個內部函數操作增加和刪除
local function AddListener(name, listener) local listeners = events[name] if not listeners then listeners = {} events[name] = listeners end

    local handleId = listeners[listener] if not handleId then handleId = eventHandleId + 1 eventHandleId = handleId --此處插入,用function來用作key值
        listeners[listener] = handleId eventHandles[handleId] = { name, listener } end

    return handleId end

local function RemoveListener(name, listener, handles) local listeners = events[name] if listeners then
        if not listener then
            --該name下對應的所有都清空
            for _, handleId in pairs(listeners) do eventHandles[handleId] = nil
                if handles then handles[handleId] = nil
                end
            end events[name] = nil
        else
            --特定對應的某個listner刪除
            local handleId = listeners[listener] if handleId then listeners[listener] = nil eventHandles[handleId] = nil
                if handles then handles[handleId] = nil
                end
            end
        end
    end
end
--指定刪除某個handleId對應的事件
local function RemoveHandle(handleId) local entry = eventHandles[handleId] if entry then RemoveListener(entry[1], entry[2]) end
end

-- 
local EventManager = {} --增加
function EventManager.Add(name, listener) AddListener(name, listener) end
--刪除
function EventManager.Remove(name, listener) RemoveListener(name, listener) end
--清空
function EventManager.Clear() events = {} eventHandles = {} end
--觸發
function EventManager.Dispatch(name, ...) local listeners = events[name] if listeners then
        for listener, _ in pairs(listeners) do
            --基於key值(實質為函數)來執行觸發操作
 listener(name, ...) end
    end
end

  巧用lua中的table,我們可以實現一個最基本的事件系統,具體的原理可以參看lua代碼來理解,不是很難。這是最基本的事件系統設計,在此基礎上,我們可以進一步的優化我們的事件系統。我們在進行事件系統的注冊、刪除和觸發的時候,並沒有考慮到並發性。比如同時有多個消息過來,要求我們對同一個事件進行處理,這時候事件系統就需要考慮並發性的設計了。

二、處理並發性的事件系統設計

    對於並發性的處理,許多常見的思路都給出了不同的處理辦法,比如加鎖就是一個比較好的處理辦法。在執行觸發的操作的時候,這是就對添加的函數進行滯后處理,這樣可以避免在執行觸發操作的時候,又塞入一個新的監聽,造成觸發隱藏問題。可以這樣處理:

local events = {}
local eventHandleId = 0
local eventHandles = {}

local function AddListener(name, listener)
    local listeners = events[name]
    if not listeners then
        --listeners不再是一個簡單的table,通過多個標識符來標識
        listeners = { insert = {}, dirty = false, executing = false, destroyed = false }
        events[name] = listeners
    end

    local handleId = listeners[listener] or listeners.insert[listener]
    if not handleId then
        handleId = eventHandleId + 1
        eventHandleId = handleId
        --如果在塞入的時候,該listeners正在被觸發,則不執行立即塞入的操作,等下一個觸發到來的時候執行
        if listeners.executing then
            listeners.insert[listener] = handleId
            listeners.dirty = true
        else
            listeners[listener] = handleId
        end
        eventHandles[handleId] = { name, listener }
    end

    return handleId
end

local function RemoveListener(name, listener, handles)
    local listeners = events[name]
    if listeners then
        if not listener then
            for _, handleId in pairs(listeners) do
                eventHandles[handleId] = nil
                if handles then
                    handles[handleId] = nil
                end
            end
            --標記其已經destroyed
            listeners.destroyed = true
            events[name] = nil
        else
            local handleId = listeners[listener] or listeners.insert[listener]
            if handleId then
                listeners[listener] = nil
                listeners.insert[listener] = nil
                eventHandles[handleId] = nil
                if handles then
                    handles[handleId] = nil
                end
            end
        end
    end
end

local function RemoveHandle(handleId)
    local entry = eventHandles[handleId]
    if entry then
        RemoveListener(entry[1], entry[2])
    end
end

local EventManager = {}

function EventManager.Add(name, listener)
    AddListener(name, listener)
end

function EventManager.Remove(name, listener)
    RemoveListener(name, listener)
end

function EventManager.Clear()
    events = {}
    eventHandles = {}
end

function EventManager.Dispatch(name, ...)
    local listeners = events[name]
    if listeners then
        listeners.executing = true
        for listener, _ in pairs(listeners) do
            if type(listener) == "function" then
                listener(name, ...)
            end
            --可能在執行listener的過程中回調執行了remove,所以需要檢測一次是否退出
            if listeners.destroyed then
                return
            end
        end
        --觸發完后,再執行緩存的塞入檢測
        if listeners.dirty then
            for listener, handleId in pairs(listeners.insert) do
                listeners[listener] = handleId
                listeners.insert[listener] = nil
            end
            listeners.dirty = false
        end
        listeners.executing = false
    end
end

   如果不用lua,改用c#來實現,則需要巧妙的運用c#中的鏈表來實現對應的操作,這兒我也給出一份c#鏈表的相關實現吧:)

using System;
using System.Collections.Generic;

public class EventListener
{
    public string    name;
    public Delegate  action;
}

public static class EventManger
{
    private static Dictionary<string, List<EventListener>> listenList = new Dictionary<string, List<EventListener>>();
    private static Dictionary<string, int> listenerStatus = new Dictionary<string, int>();
    private static Dictionary<string, List<EventListener>> addList = new Dictionary<string, List<EventListener>>();
    private static Dictionary<string, List<EventListener>> removeList = new Dictionary<string, List<EventListener>>();

    //查找監聽
    public static EventListener GetListener(string name, Delegate action)
    {
        if (action == null) return null;
        List<EventListener> listEvent = null;
        if (!listenList.TryGetValue(name, out listEvent) || listEvent.Count < 1)
            return null;
        var ls = listEvent.Find(l => l.action.Method == action.Method);
        return ls;
    }

    //添加事件監聽
    public static EventListener AddEventListener(string name, Delegate action)
    {
        if (action == null) return null;
        //判斷是否已經有
        var listener = GetListener(name, action);
        if (listener != null)
            return listener;
        //new 可以用資源池代替
        listener = new EventListener();
        listener.name = name;
        listener.action = action;
        //第一次創建,則建立對應的dic
        if(!listenList.ContainsKey(name))
        {
            listenList.Add(name, new List<EventListener>());
            addList.Add(name, new List<EventListener>());
            removeList.Add(name, new List<EventListener>());
            listenerStatus.Add(name, 0);
        }
        //處於事件觸發,則滯后處理
        if (listenerStatus[name] > 0)
            addList[name].Add(listener);
        else
            listenList[name].Add(listener);

        return listener;
    }

    //刪除事件監聽
    public static void RemoveListener(string name, EventListener listener)
    {
        List<EventListener> deleteList = null;
        if(listenList.TryGetValue(name, out deleteList))
        {
            //首先刪除addList中的監聽
            addList[name].Remove(listener);
            if(deleteList.Contains(listener))
            {
                //如果正在觸發,則滯后
                if (listenerStatus[name] > 0)
                    removeList[name].Add(listener);
                else
                {
                    deleteList.Remove(listener);
                    //歸還到資源池去........
                }
            }
        }
    }

    //事件觸發
    public static void DispatchEvent(string name)
    {
        //...............
        List<EventListener> ls = null;
        if(listenList.TryGetValue(name, out ls))
        {
            //標記正在觸發
            listenerStatus[name]++;
            //
            foreach(var listener in ls)
            {
                listener.action.DynamicInvoke(null);
            }

            var count = listenerStatus[name] - 1;
            listenerStatus[name] = count;
            //此時執行滯后的增刪操作
            if(count < 1)
            {
                var list = addList[name];
                ls.AddRange(list);
                list.Clear();
                list = removeList[name];
                foreach (var listener in list)
                    ls.Remove(listener);
                list.Clear();
            }
            //.......
        }
    }



}

  用listenerStatus來標志是否處於事件觸發的狀態, 可以較為巧妙的避開同時對鏈表的操作,當然實際的應用在還會添加一些限定條件,判定條件,避免某些不符合常規的操作帶來的風險,具體需要結合項目來進行相關的實現即可。好了,今天的文章就寫到這兒,后續再繼續更新 :D

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM