• 按我的理解,抛开一些绕口的概念描述,事件系统可以描述为一种客户端代码模块内部的通讯机制。
  • 当然,也可以用来双端联动。

游戏逻辑监听、抛出事件的机制。

Game Framework 中的很多模块在完成操作后都会抛出内置事件,监听这些事件将大大解除游戏逻辑之间的耦合。

除了 Game Framework 内置事件外,使用者也可以定义自己的游戏逻辑事件,游戏中所有事件均派生自 GameEventArgs 类,事件对象使用了引用池技术,以避免使用事件过程中频繁的内存分配。

E神原话

下面放出简化的代码结构--伪代码

EventManager

EventManager用来管理EventPool

public class EventManager : IEventManager
{
    //事件池
    private readonly EventPool<GameEventArgs> m_EventPool;

    /// <summary>
    /// 订阅事件处理函数。
    /// </summary>
    /// <param name="id">事件类型编号。</param>
    /// <param name="handler">要订阅的事件处理函数。</param>
    public void Subscribe(int id, EventHandler<GameEventArgs> handler);

    /// <summary>
    /// 取消订阅事件处理函数。
    /// </summary>
    /// <param name="id">事件类型编号。</param>
    /// <param name="handler">要取消订阅的事件处理函数。</param>
    public void Unsubscribe(int id, EventHandler<GameEventArgs> handler);

    /// <summary>
    /// 设置默认事件处理函数。
    /// </summary>
    /// <param name="handler">要设置的默认事件处理函数。</param>
    public void SetDefaultHandler(EventHandler<GameEventArgs> handler);

    /// <summary>
    /// 抛出事件,这个操作是线程安全的,即使不在主线程中抛出,也可保证在主线程中回调事件处理函数,但事件会在抛出后的下一帧分发。
    /// </summary>
    /// <param name="sender">事件源。</param>
    /// <param name="e">事件参数。</param>
    public void Fire(object sender, GameEventArgs e);

    /// <summary>
    /// 抛出事件立即模式,这个操作不是线程安全的,事件会立刻分发。
    /// </summary>
    /// <param name="sender">事件源。</param>
    /// <param name="e">事件参数。</param>
    public void FireNow(object sender, GameEventArgs e);
}

EventPool

EventPool用一个多值字典存储了事件,一个key可以对应多个事件

用队列缓存了事件参数,事件参数队列在引用池管理体系中,避免了反复生成事件参数实例带来的GC消耗

这个多值字典比较有意思,我们在下面看下具体实现

public class EventPool<T> where T : BaseEventArgs
{
    //多值字典 存放了所有已经注册的事件
    private readonly GameFrameworkMultiDictionary<int, EventHandler<T>> m_EventHandlers;
    //事件参数队列 用来缓存EventArgs
    private readonly Queue<Event> m_Events;
    //事件池模式
    private readonly EventPoolMode m_EventPoolMode;
    //默认事件
    private EventHandler<T> m_DefaultHandler;

    private sealed class Event : IReference 
    {
        public object Sender;
        public T EventArgs;   
    }

    ///.....EventManager中各种函数的具体实现逻辑

}

GF多值字典

下面一直到GF特色链表部分的简化代码特意留下了GF多值字典的运转的关键步骤

可以看到GF多值字典主体是:一个GF链表,一个字典

字典的value值用来存放GF链表中的两个节点,以此让key对应链表中的多个值(这些值在这两个节点之间)

下面给出逻辑构图

key和链表的对应关系

这是逻辑上的元素位置和对应关系,并不是物理空间上的元素位置关系,实际GF链表的各个节点可能是分散的存于内存中的。

同时E神也添加了GF多值字典和GF链表的foreach使用支持(这部分代码比较占地方就不展示了)

public class GameFrameworkMultiDictionary<TKey, TValue> : IEnumerable<KeyValuePair<TKey, GameFrameworkLinkedListRange<TValue>>>, IEnumerable
{
    //GF特色链表
    private readonly GameFrameworkLinkedList<TValue> m_LinkedList;
    private readonly Dictionary<TKey, GameFrameworkLinkedListRange<TValue>> m_Dictionary;

    /// <summary>
    /// 向指定的主键增加指定的值。
    /// </summary>
    /// <param name="key">指定的主键。</param>
    /// <param name="value">指定的值。</param>
    public void Add(TKey key, TValue value)
    {
        GameFrameworkLinkedListRange<TValue> range = default(GameFrameworkLinkedListRange<TValue>);
        if (m_Dictionary.TryGetValue(key, out range))
        {
            m_LinkedList.AddBefore(range.Terminal, value);
        }
        else
        {
            LinkedListNode<TValue> first = m_LinkedList.AddLast(value);
            LinkedListNode<TValue> terminal = m_LinkedList.AddLast(default(TValue));
            m_Dictionary.Add(key, new GameFrameworkLinkedListRange<TValue>(first, terminal));
        }
    }
}
public struct GameFrameworkLinkedListRange<T> : IEnumerable<T>, IEnumerable
{
    private readonly LinkedListNode<T> m_First;
    private readonly LinkedListNode<T> m_Terminal;
}

GF特色链表

GF链表在C#自带的链表上包装了一层内存管理机制,减少了链表在释放和增加节点的时候可能会带来的GC

public class GameFrameworkLinkedList<T> : ICollection<T>, IEnumerable<T>, ICollection, IEnumerable
{
    private readonly LinkedList<T> m_LinkedList;
    private readonly Queue<LinkedListNode<T>> m_CachedNodes;

    /// <summary>
    /// 在链表中指定的现有结点前添加包含指定值的新结点。
    /// </summary>
    /// <param name="node">指定的现有结点。</param>
    /// <param name="value">指定值。</param>
    /// <returns>包含指定值的新结点。</returns>
    public LinkedListNode<T> AddBefore(LinkedListNode<T> node, T value)
    {
        LinkedListNode<T> newNode = AcquireNode(value);
        m_LinkedList.AddBefore(node, newNode);
        return newNode;
    }

    /// <summary>
    /// 如果有回收的缓存节点空间 则利用缓存空间 没有则申请新的空间
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    private LinkedListNode<T> AcquireNode(T value)
    {
        LinkedListNode<T> node = null;
        if (m_CachedNodes.Count > 0)
        {
            node = m_CachedNodes.Dequeue();
            node.Value = value;
        }
        else
        {
            node = new LinkedListNode<T>(value);
        }

        return node;
    }

    /// <summary>
    /// 从链表中移除指定的结点。
    /// </summary>
    /// <param name="node">指定的结点。</param>
    public void Remove(LinkedListNode<T> node)
    {
        m_LinkedList.Remove(node);
        ReleaseNode(node);
    }

    private void ReleaseNode(LinkedListNode<T> node)
    {
        node.Value = default(T);
        m_CachedNodes.Enqueue(node);
    }
}

(UGF)EventComponent

在游戏开发中使用GF的事件系统,几乎可以完全不依赖于unity,但是应该是出于统一标准的考量E神还是写了EventComponent,EventComponent几乎就是Eventmanager的函数搬运工这里就不贴代码了。

小结

GF的事件系统使用起来可能不会特别方便,但是在性能和内存的控制上已经做到了非常优秀的程度。

使用实例

public class TestEventArgs : GameEventArgs
{
    /// <summary>
    /// 事件编号。
    /// </summary>
    public static readonly int EventId = typeof(TestEventArgs).GetHashCode();

    /// <summary>
    /// 获取事件编号
    /// </summary>
    public override int Id
    {
        get
        {
            return EventId;
        }
    }
    /// <summary>
    /// 输出字段
    /// </summary>
    public string logString
    {
        get;
        private set;
    }

    /// <summary>
    /// 创建事件
    /// </summary>
    /// <param name="e">内部事件。</param>
    /// <returns>创建的事件。</returns>
    public static TestEventArgs Create(string logString)
    {
        // 使用引用池技术,避免频繁内存分配
        TestEventArgs e = ReferencePool.Acquire<TestEventArgs>();
        e.logString = logString;
        return e;
    }

    public override void Clear()
    {
        logString = null;
    }
}
namespace StarForce
{
    public class TestNode : MonoBehaviour
    {
        EventHandler<GameEventArgs> Logstring;

        private void Awake()
        {
            Logstring += LogString;
        }

        private void Start()
        {
            ///订阅事件
            GameEntry.Event.Subscribe(TestEventArgs.EventId, Logstring);
            ///发送事件
            GameEntry.Event.Fire(this, TestEventArgs.Create("猫刀刀"));
        }

        private void LogString(object sender, GameEventArgs e)
        {
            TestEventArgs ne = (TestEventArgs)e;

            Debug.LogError(ne.logString);
        }
   
    }
}
运行结果