Unity UGC IDE实现深度解析(五):事件系统与消息传递

Unity UGC IDE实现深度解析(五):事件系统与消息传递

引言

在上一篇中,我们构建了数据的”存储与流动”机制。但仅有数据流还不够——真实的游戏逻辑充满了事件驱动的场景:角色受击、任务完成、UI按钮点击……这些都需要一个强大的事件系统来协调。

本篇将实现一个解耦、可扩展、支持异步的事件系统,让节点图能够响应Unity引擎事件,并支持跨图表通信。

一、事件系统整体架构

1.1 设计思路

事件系统需要解决四个核心问题:

如何封装Unity事件(MonoBehaviour生命周期、物理碰撞等)如何自定义事件(玩家自己定义的游戏事件)如何跨图表通信(子图表触发父图表事件)如何处理异步事件(延迟触发、协程支持)

1.2 核心类设计


// 事件基类(思路:所有事件的抽象)
public abstract class GameEvent
{
    public string EventName { get; protected set; }
    public object Sender { get; set; }           // 事件发送者
    public Dictionary<string, object> Parameters; // 事件参数
    public float Timestamp { get; private set; }  // 触发时间
    
    protected GameEvent(string eventName)
    {
        EventName = eventName;
        Parameters = new Dictionary<string, object>();
        Timestamp = Time.time;
    }
    
    // 克隆事件(用于事件传播)
    public abstract GameEvent Clone();
}

// 具体事件实现(思路:泛型参数化)
public class GameEvent<T> : GameEvent where T : struct
{
    public T Data { get; set; }
    
    public GameEvent(string name, T data) : base(name)
    {
        Data = data;
    }
    
    public override GameEvent Clone()
    {
        return new GameEvent<T>(EventName, Data)
        {
            Sender = Sender,
            Parameters = new Dictionary<string, object>(Parameters)
        };
    }
}

// 事件监听器接口
public interface IEventListener
{
    void OnEventReceived(GameEvent evt);
    int Priority { get; }  // 优先级(数字越小越先执行)
}

二、Unity事件适配器

2.1 生命周期事件封装


// 思路:用中介者模式将MonoBehaviour事件转换为节点事件
public class UnityEventAdapter : MonoBehaviour
{
    private GraphExecutor _executor;
    private Dictionary<string, List<BaseNode>> _eventNodeMap;
    
    public void Initialize(GraphExecutor executor)
    {
        _executor = executor;
        _eventNodeMap = new Dictionary<string, List<BaseNode>>();
        
        // 扫描图表中的所有Unity事件节点
        RegisterUnityEventNodes();
    }
    
    private void RegisterUnityEventNodes()
    {
        foreach (var node in _executor.Graph.Nodes)
        {
            if (node is UnityEventNode eventNode)
            {
                string eventType = eventNode.UnityEventType;
                
                if (!_eventNodeMap.ContainsKey(eventType))
                    _eventNodeMap[eventType] = new List<BaseNode>();
                
                _eventNodeMap[eventType].Add(node);
            }
        }
    }
    
    // Unity生命周期事件转发
    void Start() => TriggerUnityEvent("Start");
    void Update() => TriggerUnityEvent("Update");
    void OnDestroy() => TriggerUnityEvent("OnDestroy");
    
    // 物理事件转发
    void OnCollisionEnter(Collision collision)
    {
        var evt = new GameEvent<CollisionData>("OnCollisionEnter", new CollisionData
        {
            Collider = collision.collider,
            ContactPoint = collision.GetContact(0).point,
            ImpactForce = collision.impulse.magnitude
        });
        
        TriggerUnityEvent("OnCollisionEnter", evt);
    }
    
    private void TriggerUnityEvent(string eventType, GameEvent evt = null)
    {
        if (!_eventNodeMap.TryGetValue(eventType, out var nodes))
            return;
        
        foreach (var node in nodes)
        {
            if (node is UnityEventNode eventNode)
            {
                eventNode.Execute(_executor.Context, evt);
            }
        }
    }
}

// 碰撞数据结构
public struct CollisionData
{
    public Collider Collider;
    public Vector3 ContactPoint;
    public float ImpactForce;
}

2.2 Unity事件节点实现


// Unity事件节点(思路:作为图表执行的入口点)
public class UnityEventNode : BaseNode
{
    [SerializeField] private UnityEventType _eventType;
    public string UnityEventType => _eventType.ToString();
    
    // 输出端口(用于传递事件数据)
    private Port _flowOutput;      // 执行流输出
    private Port _eventDataOutput; // 事件数据输出
    
    protected override void CreatePorts()
    {
        _flowOutput = CreateOutputPort("Execute", PortType.Flow);
        
        // 根据事件类型创建相应的数据端口
        switch (_eventType)
        {
            case UnityEventType.OnCollisionEnter:
                _eventDataOutput = CreateOutputPort("Collision", typeof(CollisionData));
                break;
            case UnityEventType.Update:
                _eventDataOutput = CreateOutputPort("DeltaTime", typeof(float));
                break;
        }
    }
    
    public void Execute(ExecutionContext context, GameEvent evt)
    {
        // 将事件数据推送到输出端口
        if (evt != null && _eventDataOutput != null)
        {
            if (evt is GameEvent<CollisionData> collisionEvt)
                _eventDataOutput.SetValue(collisionEvt.Data);
            else if (evt is GameEvent<float> floatEvt)
                _eventDataOutput.SetValue(floatEvt.Data);
        }
        
        // 触发执行流
        TriggerConnectedNodes(_flowOutput);
    }
}

// 支持的Unity事件类型
public enum UnityEventType
{
    Start,
    Update,
    FixedUpdate,
    OnDestroy,
    OnCollisionEnter,
    OnCollisionExit,
    OnTriggerEnter,
    OnTriggerExit,
    OnMouseDown,
    // ... 更多事件
}

三、自定义事件总线

3.1 事件总线核心实现


// 思路:单例模式 + 观察者模式
public class EventBus : MonoBehaviour
{
    private static EventBus _instance;
    public static EventBus Instance
    {
        get
        {
            if (_instance == null)
            {
                var go = new GameObject("EventBus");
                _instance = go.AddComponent<EventBus>();
                DontDestroyOnLoad(go);
            }
            return _instance;
        }
    }
    
    // 事件监听器注册表(按事件名分组)
    private Dictionary<string, List<IEventListener>> _listeners 
        = new Dictionary<string, List<IEventListener>>();
    
    // 订阅事件(思路:支持优先级排序)
    public void Subscribe(string eventName, IEventListener listener)
    {
        if (!_listeners.ContainsKey(eventName))
            _listeners[eventName] = new List<IEventListener>();
        
        _listeners[eventName].Add(listener);
        
        // 按优先级排序
        _listeners[eventName].Sort((a, b) => a.Priority.CompareTo(b.Priority));
    }
    
    // 取消订阅
    public void Unsubscribe(string eventName, IEventListener listener)
    {
        if (_listeners.TryGetValue(eventName, out var list))
        {
            list.Remove(listener);
        }
    }
    
    // 发布事件(思路:同步触发所有监听器)
    public void Publish(GameEvent evt)
    {
        if (!_listeners.TryGetValue(evt.EventName, out var list))
            return;
        
        // 创建副本避免迭代时修改集合
        var listenersCopy = new List<IEventListener>(list);
        
        foreach (var listener in listenersCopy)
        {
            try
            {
                listener.OnEventReceived(evt);
            }
            catch (Exception e)
            {
                Debug.LogError($"Event listener error: {e.Message}");
            }
        }
    }
    
    // 延迟发布(思路:下一帧执行)
    public void PublishDelayed(GameEvent evt, float delay = 0f)
    {
        StartCoroutine(PublishDelayedCoroutine(evt, delay));
    }
    
    private IEnumerator PublishDelayedCoroutine(GameEvent evt, float delay)
    {
        if (delay > 0)
            yield return new WaitForSeconds(delay);
        else
            yield return null;
        
        Publish(evt);
    }
}

3.2 事件节点实现


// 发送事件节点(思路:接收输入参数并发布事件)
public class PublishEventNode : BaseNode
{
    [SerializeField] private string _eventName;
    [SerializeField] private bool _delayPublish = false;
    
    private Port _flowInput;
    private Port _flowOutput;
    private Port _parameterInput;  // 事件参数输入
    private Port _delayInput;      // 延迟时间输入
    
    protected override void CreatePorts()
    {
        _flowInput = CreateInputPort("Execute", PortType.Flow);
        _flowOutput = CreateOutputPort("After", PortType.Flow);
        _parameterInput = CreateInputPort("Data", PortType.Dynamic);
        
        if (_delayPublish)
            _delayInput = CreateInputPort("Delay", typeof(float));
    }
    
    protected override void Execute(ExecutionContext context)
    {
        // 获取事件参数
        object eventData = _parameterInput.GetValue<object>();
        
        // 创建事件
        var evt = new GameEvent<object>(_eventName, eventData)
        {
            Sender = this
        };
        
        // 发布事件
        if (_delayPublish)
        {
            float delay = _delayInput.GetValue<float>();
            EventBus.Instance.PublishDelayed(evt, delay);
        }
        else
        {
            EventBus.Instance.Publish(evt);
        }
        
        // 继续执行流
        TriggerConnectedNodes(_flowOutput);
    }
}

// 监听事件节点(思路:实现IEventListener接口)
public class ListenEventNode : BaseNode, IEventListener
{
    [SerializeField] private string _eventName;
    [SerializeField] private int _priority = 0;
    
    private Port _flowOutput;
    private Port _eventDataOutput;
    
    public int Priority => _priority;
    
    protected override void CreatePorts()
    {
        _flowOutput = CreateOutputPort("On Event", PortType.Flow);
        _eventDataOutput = CreateOutputPort("Data", PortType.Dynamic);
    }
    
    public override void OnEnable()
    {
        base.OnEnable();
        // 注册到事件总线
        EventBus.Instance.Subscribe(_eventName, this);
    }
    
    public override void OnDisable()
    {
        base.OnDisable();
        EventBus.Instance.Unsubscribe(_eventName, this);
    }
    
    public void OnEventReceived(GameEvent evt)
    {
        // 将事件数据推送到输出端口
        if (evt is GameEvent<object> genericEvt)
        {
            _eventDataOutput.SetValue(genericEvt.Data);
        }
        
        // 触发执行流
        TriggerConnectedNodes(_flowOutput);
    }
}

四、跨图表消息路由

4.1 图表层级关系

实现思路:


// 图表执行器(添加父子关系管理)
public class GraphExecutor : MonoBehaviour
{
    public GraphAsset Graph { get; private set; }
    public GraphExecutor ParentExecutor { get; set; }  // 父图表引用
    
    private List<GraphExecutor> _childExecutors = new List<GraphExecutor>();
    
    // 创建子图表实例
    public GraphExecutor CreateChildGraph(GraphAsset childGraph)
    {
        var childGO = new GameObject($"SubGraph_{childGraph.name}");
        childGO.transform.SetParent(transform);
        
        var childExecutor = childGO.AddComponent<GraphExecutor>();
        childExecutor.Initialize(childGraph);
        childExecutor.ParentExecutor = this;
        
        _childExecutors.Add(childExecutor);
        
        return childExecutor;
    }
    
    // 向上传播事件(冒泡)
    public void BubbleEvent(GameEvent evt)
    {
        // 先在当前图表处理
        EventBus.Instance.Publish(evt);
        
        // 向父图表传播
        if (ParentExecutor !=null)
        {
            ParentExecutor.BubbleEvent(evt);
        }
    }
    
    // 向下广播事件(捕获)
    public void BroadcastEvent(GameEvent evt)
    {
        // 当前图表处理
        EventBus.Instance.Publish(evt);
        
        // 向所有子图表广播
        foreach (var child in _childExecutors)
        {
            child.BroadcastEvent(evt);
        }
    }
}

4.2 子图表节点实现


// 子图表节点(思路:封装整个图表作为可复用单元)
public class SubGraphNode : BaseNode
{
    [SerializeField] private GraphAsset _subGraphAsset;
    
    private GraphExecutor _subGraphExecutor;
    private Dictionary<string, Port> _inputPorts = new Dictionary<string, Port>();
    private Dictionary<string, Port> _outputPorts = new Dictionary<string, Port>();
    
    protected override void CreatePorts()
    {
        // 根据子图表的输入/输出定义创建端口
        if (_subGraphAsset != null)
        {
            foreach (var input in _subGraphAsset.InputDefinitions)
            {
                var port = CreateInputPort(input.Name, input.Type);
                _inputPorts[input.Name] = port;
            }
            
            foreach (var output in _subGraphAsset.OutputDefinitions)
            {
                var port = CreateOutputPort(output.Name, output.Type);
                _outputPorts[output.Name] = port;
            }
        }
    }
    
    protected override void Execute(ExecutionContext context)
    {
        // 创建子图表执行器
        if (_subGraphExecutor == null)
        {
            _subGraphExecutor = context.CurrentExecutor.CreateChildGraph(_subGraphAsset);
        }
        
        // 传递输入参数到子图表
        foreach (var kvp in _inputPorts)
        {
            object value = kvp.Value.GetValue<object>();
            _subGraphExecutor.Context.SetVariable(kvp.Key, value);
        }
        
        // 执行子图表
        _subGraphExecutor.Execute();
        
        // 获取子图表输出
        foreach (var kvp in _outputPorts)
        {
            object value = _subGraphExecutor.Context.GetVariable(kvp.Key);
            kvp.Value.SetValue(value);
        }
    }
}

五、异步事件队列

5.1 事件队列实现


// 思路:用队列缓冲事件,避免同一帧内递归触发
public class AsyncEventQueue
{
    private Queue<GameEvent> _eventQueue = new Queue<GameEvent>();
    private HashSet<string> _processingEvents = new HashSet<string>();
    
    public void Enqueue(GameEvent evt)
    {
        _eventQueue.Enqueue(evt);
    }
    
    public void ProcessQueue()
    {
        int maxIterations = 100; // 防止无限循环
        int iterations = 0;
        
        while (_eventQueue.Count > 0 && iterations < maxIterations)
        {
            var evt = _eventQueue.Dequeue();
            
            // 检测循环依赖
            if (_processingEvents.Contains(evt.EventName))
            {
                Debug.LogWarning($"Circular event detected: {evt.EventName}");
                continue;
            }
            
            _processingEvents.Add(evt.EventName);
            EventBus.Instance.Publish(evt);
            _processingEvents.Remove(evt.EventName);
            
            iterations++;
        }
    }
}

// 在EventBus中集成队列
public class EventBus : MonoBehaviour
{
    private AsyncEventQueue _asyncQueue = new AsyncEventQueue();
    
    void LateUpdate()
    {
        // 每帧末尾处理异步事件
        _asyncQueue.ProcessQueue();
    }
    
    public void PublishAsync(GameEvent evt)
    {
        _asyncQueue.Enqueue(evt);
    }
}

5.2 协程支持


// 协程事件节点
public class WaitForEventNode : BaseNode
{
    [SerializeField] private string _eventName;
    [SerializeField] private float _timeout = 0f;
    
    private Port _flowInput;
    private Port _flowOutput;
    private Port _timeoutOutput;
    
    protected override void Execute(ExecutionContext context)
    {
        context.CurrentExecutor.StartCoroutine(WaitForEventCoroutine(context));
    }
    
    private IEnumerator WaitForEventCoroutine(ExecutionContext context)
    {
        bool eventReceived = false;
        GameEvent receivedEvent = null;
        
        // 临时监听器
        IEventListener tempListener = new DelegateEventListener(evt =>
        {
            eventReceived = true;
            receivedEvent = evt;
        });
        
        EventBus.Instance.Subscribe(_eventName, tempListener);
        
        float elapsedTime = 0f;
        while (!eventReceived)
        {
            if (_timeout > 0 && elapsedTime >= _timeout)
            {
                EventBus.Instance.Unsubscribe(_eventName, tempListener);
                TriggerConnectedNodes(_timeoutOutput);
                yield break;
            }
            
            elapsedTime += Time.deltaTime;
            yield return null;
        }
        
        EventBus.Instance.Unsubscribe(_eventName, tempListener);
        TriggerConnectedNodes(_flowOutput);
    }
}

// 委托事件监听器(辅助类)
public class DelegateEventListener : IEventListener
{
    private Action<GameEvent> _callback;
    public int Priority => 0;
    
    public DelegateEventListener(Action<GameEvent> callback)
    {
        _callback = callback;
    }
    
    public void OnEventReceived(GameEvent evt)
    {
        _callback?.Invoke(evt);
    }
}

六、完整实战案例:技能系统

6.1 需求设计

实现一个技能触发流程:

玩家按下技能键检查CD和消耗播放动画延迟0.5秒后造成伤害触发UI更新

6.2 事件定义


// 自定义技能事件
public struct SkillCastEvent
{
    public int SkillID;
    public Vector3 CastPosition;
    public GameObject Caster;
}

public struct DamageEvent
{
    public GameObject Target;
    public float Damage;
    public DamageType Type;
}

6.3 节点图流程

伪代码实现:


// 技能触发节点
public class CastSkillNode : BaseNode
{
    [SerializeField] private int _skillID;
    
    protected override void Execute(ExecutionContext context)
    {
        var evt = new GameEvent<SkillCastEvent>("OnSkillCast", new SkillCastEvent
        {
            SkillID = _skillID,
            CastPosition = transform.position,
            Caster = gameObject
        });
        
        EventBus.Instance.Publish(evt);
    }
}

// 伤害节点(监听伤害事件并处理)
public class DamageHandlerNode : BaseNode, IEventListener
{
    public int Priority => 10;
    
    void OnEnable() => EventBus.Instance.Subscribe("OnDamage", this);
    void OnDisable() => EventBus.Instance.Unsubscribe("OnDamage", this);
    
    public void OnEventReceived(GameEvent evt)
    {
        if (evt is GameEvent<DamageEvent> damageEvt)
        {
            var data = damageEvt.Data;
            
            // 获取当前HP
            int currentHP = GlobalVariableManager.Instance.GetValue<int>("PlayerHP");
            int newHP = Mathf.Max(0, currentHP - (int)data.Damage);
            
            // 更新HP
            GlobalVariableManager.Instance.SetValue("PlayerHP", newHP);
            
            // 发布HP变化事件
            EventBus.Instance.Publish(new GameEvent<int>("OnHPChanged", newHP));
        }
    }
}

总结

本篇实现了完整的事件系统,核心要点:

Unity事件适配:将引擎事件转换为节点事件自定义事件总线:支持优先级、异步队列跨图表通信:事件冒泡和捕获机制协程支持:异步等待事件解耦设计:发布-订阅模式

© 版权声明

相关文章

暂无评论

none
暂无评论...