跳到主要内容

执行顺序与最佳实践

理解GameEvent如何执行回调和管理事件流对于构建可靠、高性能的事件驱动系统至关重要。本指南涵盖执行顺序、常见模式、陷阱和优化策略。


🎯 执行顺序

可视化时间线

当调用myEvent.Raise()时,执行遵循此精确顺序:

myEvent.Raise() 🚀

├── 1️⃣ 基础监听器(FIFO顺序)
│ │
│ ├─► OnUpdate() 📝
│ │ ✓ 已执行
│ │
│ └─► OnRender() 🎨
│ ✓ 已执行

├── 2️⃣ 优先级监听器(高 → 低)
│ │
│ ├─► [优先级 100] Critical() ⚡
│ │ ✓ 首先执行
│ │
│ ├─► [优先级 50] Normal() 📊
│ │ ✓ 其次执行
│ │
│ └─► [优先级 0] LowPriority() 📌
│ ✓ 最后执行

├── 3️⃣ 条件监听器(优先级 + 条件)
│ │
│ └─► [优先级 10] IfHealthLow() 💊
│ │
│ ├─► 条件检查: health < 20?
│ │ ├─► ✅ True → 执行监听器
│ │ └─► ❌ False → 跳过监听器
│ │
│ └─► (检查下一个条件...)

├── 4️⃣ 持久化监听器(跨场景)
│ │
│ └─► GlobalLogger() 📋
│ ✓ 始终执行(DontDestroyOnLoad)

├── 5️⃣ 触发器事件(并行 - 扇出)🌟
│ │
│ ├─────► lightOnEvent.Raise() 💡
│ │ (独立执行)
│ │
│ ├─────► soundEvent.Raise() 🔊
│ │ (独立执行)
│ │
│ └─────► particleEvent.Raise() ✨
│ (独立执行)

│ ⚠️ 如果一个失败,其他仍然执行

└── 6️⃣ 链事件(顺序 - 严格顺序)🔗

└─► fadeOutEvent.Raise() 🌑
✓ 成功

├─► ⏱️ 等待(持续时间/延迟)

└─► loadSceneEvent.Raise() 🗺️
✓ 成功

├─► ⏱️ 等待(持续时间/延迟)

└─► fadeInEvent.Raise() 🌕
✓ 成功

🛑 如果任何步骤失败 → 链停止

执行特性

阶段模式时间失败行为使用场景
基础监听器顺序同一帧,同步继续到下一个标准回调
优先级监听器顺序(排序)同一帧,同步继续到下一个有序处理
条件监听器顺序(过滤)同一帧,同步如果false则跳过,继续状态依赖逻辑
持久化监听器顺序同一帧,同步继续到下一个跨场景系统
触发器事件并行同一帧,独立其他不受影响副作用、通知
链事件顺序多帧,阻塞链停止过场动画、序列

关键差异说明

特性:

  • 在当前帧中同步执行
  • 按定义的顺序一个接一个运行
  • 每个监听器都是独立的
  • 一个监听器的失败不会停止其他监听器

示例:

healthEvent.AddListener(UpdateUI);           // 第1个运行
healthEvent.AddPriorityListener(SaveGame, 100); // 第2个运行(更高优先级)
healthEvent.AddConditionalListener(ShowWarning,
health => health < 20); // 第3个运行(如果条件为true)

healthEvent.Raise(15f);
// 顺序:SaveGame() → UpdateUI() → ShowWarning()(如果health < 20)

时间线:

🖼️ 帧 1024
🚀 healthEvent.Raise(15.0f)

├─► 💾 SaveGame() ⏱️ 0.1ms
├─► 🖥️ UpdateUI() ⏱️ 0.3ms
└─► ⚠️ ShowWarning() ⏱️ 0.2ms

📊 总成本:0.6ms | ⚡ 状态:同步(同一帧)

💡 最佳实践

1. 监听器管理

始终取消订阅

内存泄漏是事件系统的第一大问题。始终清理监听器。

public class PlayerController : MonoBehaviour
{
[GameEventDropdown] public GameEvent onPlayerDeath;

void Start()
{
onPlayerDeath.AddListener(HandleDeath);
}

// 对象被销毁但监听器仍在内存中!
// 这会导致内存泄漏和潜在的崩溃
}

使用OnEnable/OnDisable模式

OnEnable/OnDisable模式是Unity推荐的方法。

public class HealthUI : MonoBehaviour
{
[GameEventDropdown] public GameEvent<float> healthChangedEvent;

void OnEnable()
{
// 活动时订阅
healthChangedEvent.AddListener(OnHealthChanged);
}

void OnDisable()
{
// 非活动时取消订阅
healthChangedEvent.RemoveListener(OnHealthChanged);
}

void OnHealthChanged(float newHealth)
{
// 更新UI
}
}

好处:

  • 对象禁用/销毁时自动清理
  • 监听器仅在需要时活动
  • 防止重复订阅
  • 适用于对象池

2. 调度管理

存储句柄以便取消

如果以后需要取消,始终存储ScheduleHandle

public class PoisonEffect : MonoBehaviour
{
void ApplyPoison()
{
// 以后无法取消这个!
poisonEvent.RaiseRepeating(damagePerTick, 1f, repeatCount: 10);
}

void CurePoison()
{
// 无法停止毒药!
// 它将继续执行所有10次
}
}

多个调度模式

管理多个调度时,使用集合。

public class BuffManager : MonoBehaviour
{
[GameEventDropdown] public GameEvent<string> buffTickEvent;

private Dictionary<string, ScheduleHandle> _activeBuffs = new();

public void ApplyBuff(string buffName, float interval, int duration)
{
// 如果有,取消现有buff
if (_activeBuffs.TryGetValue(buffName, out var existingHandle))
{
buffTickEvent.CancelRepeating(existingHandle);
}

// 应用新buff
var handle = buffTickEvent.RaiseRepeating(
buffName,
interval,
repeatCount: duration
);

_activeBuffs[buffName] = handle;
}

public void RemoveBuff(string buffName)
{
if (_activeBuffs.TryGetValue(buffName, out var handle))
{
buffTickEvent.CancelRepeating(handle);
_activeBuffs.Remove(buffName);
}
}

void OnDisable()
{
// 取消所有buff
foreach (var handle in _activeBuffs.Values)
{
buffTickEvent.CancelRepeating(handle);
}
_activeBuffs.Clear();
}
}

3. 触发器和链管理

使用句柄以安全移除

始终使用句柄以避免删除其他系统的触发器/链。

public class DoorSystem : MonoBehaviour
{
void SetupDoor()
{
doorOpenEvent.AddTriggerEvent(lightOnEvent);
}

void Cleanup()
{
// 危险:删除所有到lightOnEvent的触发器
// 甚至是其他系统注册的!
doorOpenEvent.RemoveTriggerEvent(lightOnEvent);
}
}

组织多个触发器/链

对复杂系统使用结构化方法。

public class CutsceneManager : MonoBehaviour
{
// 存储所有句柄以便清理
private readonly List<ChainHandle> _cutsceneChains = new();
private readonly List<TriggerHandle> _cutsceneTriggers = new();

void SetupCutscene()
{
// 构建过场动画序列
var chain1 = startEvent.AddChainEvent(fadeOutEvent, duration: 1f);
var chain2 = startEvent.AddChainEvent(playVideoEvent, duration: 5f);
var chain3 = startEvent.AddChainEvent(fadeInEvent, duration: 1f);

_cutsceneChains.Add(chain1);
_cutsceneChains.Add(chain2);
_cutsceneChains.Add(chain3);

// 为效果添加并行触发器
var trigger1 = startEvent.AddTriggerEvent(stopGameplayMusicEvent);
var trigger2 = startEvent.AddTriggerEvent(hideCrosshairEvent);

_cutsceneTriggers.Add(trigger1);
_cutsceneTriggers.Add(trigger2);
}

void SkipCutscene()
{
// 清理所有链
foreach (var chain in _cutsceneChains)
{
startEvent.RemoveChainEvent(chain);
}
_cutsceneChains.Clear();

// 清理所有触发器
foreach (var trigger in _cutsceneTriggers)
{
startEvent.RemoveTriggerEvent(trigger);
}
_cutsceneTriggers.Clear();
}
}

4. 优先级使用

优先级值指南

在项目中使用一致的优先级刻度。

// 定义优先级常量
public static class EventPriority
{
public const int CRITICAL = 1000; // 绝对必须首先运行
public const int HIGH = 100; // 重要系统
public const int NORMAL = 0; // 默认优先级
public const int LOW = -100; // 可以稍后运行
public const int CLEANUP = -1000; // 最终清理任务
}

// 用法
healthEvent.AddPriorityListener(SavePlayerData, EventPriority.CRITICAL);
healthEvent.AddPriorityListener(UpdateHealthBar, EventPriority.HIGH);
healthEvent.AddPriorityListener(PlayDamageSound, EventPriority.NORMAL);
healthEvent.AddPriorityListener(UpdateStatistics, EventPriority.LOW);

优先级反模式

// 不要使用随机或不一致的优先级
healthEvent.AddPriorityListener(SystemA, 523);
healthEvent.AddPriorityListener(SystemB, 891);
healthEvent.AddPriorityListener(SystemC, 7);

// 当顺序不重要时不要过度使用优先级
uiClickEvent.AddPriorityListener(PlaySound, 50);
uiClickEvent.AddPriorityListener(PlayParticle, 49);
// 这些不需要优先级,使用基础监听器!

5. 条件监听器

有效的条件设计

保持条件简单快速。

// 不要在条件中做昂贵的操作
enemySpawnEvent.AddConditionalListener(
SpawnBoss,
() => {
// 不好:条件中的复杂计算
var enemies = FindObjectsOfType<Enemy>();
var totalHealth = enemies.Sum(e => e.Health);
var averageLevel = enemies.Average(e => e.Level);
return totalHealth < 100 && averageLevel > 5;
}
);

⚠️ 常见陷阱

1. 内存泄漏

问题: 对象销毁时不取消订阅监听器。

症状:

  • 随时间增加的内存使用
  • 关于销毁对象的错误
  • 回调在空引用上执行

解决方案:

// 始终使用OnEnable/OnDisable模式
void OnEnable() => myEvent.AddListener(OnCallback);
void OnDisable() => myEvent.RemoveListener(OnCallback);

2. 丢失调度句柄

问题: 创建调度而不存储句柄。

症状:

  • 无法取消重复事件
  • 对象销毁后事件继续
  • 不需要的执行造成资源浪费

解决方案:

private ScheduleHandle _handle;

void StartTimer()
{
_handle = timerEvent.RaiseRepeating(1f);
}

void StopTimer()
{
timerEvent.CancelRepeating(_handle);
}

3. 广泛移除影响

问题: 使用基于目标的移除而不是基于句柄的移除。

症状:

  • 其他系统的触发器/链意外被删除
  • 难以调试的问题,事件停止触发
  • 跨系统耦合和脆弱性

解决方案:

// 存储句柄,精确移除
private TriggerHandle _myTrigger;

void Setup()
{
_myTrigger = eventA.AddTriggerEvent(eventB);
}

void Cleanup()
{
eventA.RemoveTriggerEvent(_myTrigger); // 安全!
}

4. 递归事件触发

问题: 事件监听器触发相同的事件,导致无限循环。

症状:

  • 堆栈溢出异常
  • Unity冻结
  • 指数执行增长

示例:

// ❌ 危险:无限递归!
void Setup()
{
healthEvent.AddListener(OnHealthChanged);
}

void OnHealthChanged(float health)
{
// 这再次触发OnHealthChanged!
healthEvent.Raise(health - 1); // ← 无限循环
}

解决方案:

// ✅ 使用标志防止递归
private bool _isProcessingHealthChange = false;

void OnHealthChanged(float health)
{
if (_isProcessingHealthChange) return; // 防止递归

_isProcessingHealthChange = true;

// 现在这里触发是安全的
if (health <= 0)
{
deathEvent.Raise();
}

_isProcessingHealthChange = false;
}

🚀 性能优化

1. 最小化监听器数量

即使代码已经高度优化,每个监听器仍会有一些开销。尽可能合并。

// 相关操作的多个监听器
healthEvent.AddListener(UpdateHealthBar);
healthEvent.AddListener(UpdateHealthText);
healthEvent.AddListener(UpdateHealthIcon);
healthEvent.AddListener(UpdateHealthColor);

2. 避免监听器中的重操作

保持监听器轻量级。将重工作移到协程/异步。

void OnDataLoaded(string data)
{
// 不好:阻塞所有后续监听器的执行
var parsed = JsonUtility.FromJson<LargeData>(data);
ProcessComplexData(parsed); // 需要50ms
SaveToDatabase(parsed); // 需要100ms
}

3. 缓存委托分配

避免每帧创建新的委托分配。

void OnEnable()
{
// 每次都创建新的委托分配
updateEvent.AddListener(() => UpdateHealth());
}

📊 总结检查清单

使用此检查清单处理GameEvent

监听器管理

  • 始终在OnDisable中取消订阅
  • 使用OnEnable/OnDisable模式
  • 尽可能缓存委托引用
  • 保持监听器轻量级

调度管理

  • 需要取消时存储ScheduleHandle
  • 在OnDisable中取消调度
  • 使用集合管理多个调度
  • 对象销毁时清理

触发器/链管理

  • 使用句柄以安全移除
  • 在集合中存储句柄以便清理
  • 为并行选择触发器,为顺序选择链
  • 记得为链调用ExecuteChainEvents()

性能

  • 合并相关监听器
  • 将重工作移到协程/异步
  • 使用简单、快速的条件
  • 避免递归事件触发

优先级与条件

  • 使用一致的优先级刻度
  • 仅在顺序重要时使用优先级
  • 保持条件简单且缓存
  • 记录优先级依赖关系