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事件适配:将引擎事件转换为节点事件自定义事件总线:支持优先级、异步队列跨图表通信:事件冒泡和捕获机制协程支持:异步等待事件解耦设计:发布-订阅模式
© 版权声明
文章版权归作者所有,未经允许请勿转载。
相关文章
暂无评论...