1. 項(xiàng)目概述為什么我們需要EntityX的事件模塊如果你用C寫過游戲或者任何需要處理大量動(dòng)態(tài)交互的復(fù)雜應(yīng)用肯定對(duì)“事件驅(qū)動(dòng)”這個(gè)詞不陌生。想象一下你的游戲里有成百上千個(gè)實(shí)體Entity比如玩家、怪物、子彈、道具。當(dāng)一個(gè)怪物被子彈擊中時(shí)它需要扣血、播放受傷動(dòng)畫、可能還要掉落物品同時(shí)UI可能需要更新連擊數(shù)音效系統(tǒng)要播放“擊中”音效成就系統(tǒng)要檢查是否解鎖了“百發(fā)百中”的成就。如果讓子彈的代碼直接去調(diào)用怪物、UI、音效、成就系統(tǒng)的函數(shù)代碼很快就會(huì)變成一團(tuán)亂麻耦合度高到難以維護(hù)。這就是事件系統(tǒng)要解決的問題解耦。EntityX作為一個(gè)輕量級(jí)的C實(shí)體組件系統(tǒng)ECS框架其事件模塊正是為了優(yōu)雅地處理這種“某事發(fā)生了通知所有關(guān)心此事的對(duì)象”的場(chǎng)景而設(shè)計(jì)的。它不依賴于龐大的游戲引擎你可以把它嵌入到你的C項(xiàng)目中快速構(gòu)建起清晰的事件通信機(jī)制。今天我們就來深入它的內(nèi)部看看這個(gè)事件模塊是如何工作的以及如何在你的項(xiàng)目中高效地使用它。2. EntityX事件模塊核心設(shè)計(jì)解析EntityX的事件系統(tǒng)采用了經(jīng)典的觀察者模式Observer Pattern但在此基礎(chǔ)上做了適合ECS范式的優(yōu)化。其核心思想是事件的發(fā)送者Emitter不需要知道接收者Receiver是誰(shuí)只需要聲明“某某事件發(fā)生了”而接收者則向系統(tǒng)注冊(cè)自己對(duì)某類事件的興趣并提供處理函數(shù)。系統(tǒng)負(fù)責(zé)在事件發(fā)生時(shí)將事件分發(fā)給所有注冊(cè)過的接收者。2.1 核心類與它們的關(guān)系整個(gè)事件模塊圍繞幾個(gè)核心類展開理解它們的關(guān)系是讀懂代碼的關(guān)鍵。EventManager事件系統(tǒng)的中樞和大腦。它負(fù)責(zé)三件事1) 維護(hù)一個(gè)事件類型到接收者列表的映射表2) 提供接口供接收者訂閱subscribe特定事件3) 提供接口供發(fā)送者發(fā)射emit事件并由它負(fù)責(zé)調(diào)用所有訂閱者的處理函數(shù)。每個(gè)EntityX實(shí)例都擁有一個(gè)唯一的EventManager。ReceiverTEvent事件接收者的模板類。這是一個(gè)CRTP奇特的遞歸模板模式基類你需要讓希望接收某類事件的自定義類例如PhysicsSystem,UISystem繼承自Receiver具體事件類型。繼承后你的類必須實(shí)現(xiàn)一個(gè)receive(const 具體事件類型)方法。Receiver內(nèi)部會(huì)持有指向EventManager的指針并在構(gòu)造和析構(gòu)時(shí)自動(dòng)完成訂閱和取消訂閱這是利用RAII資源獲取即初始化原則避免資源泄漏的經(jīng)典做法。事件類型Event Types在EntityX中事件就是普通的C結(jié)構(gòu)體struct或類class。沒有任何基類要求你可以自由定義任何數(shù)據(jù)成員。例如你可以定義一個(gè)CollisionEvent { Entity a, Entity b; }或者一個(gè)KeyPressedEvent { int keyCode; }。這種設(shè)計(jì)極其靈活類型安全由模板系統(tǒng)保證。它們的工作流程可以概括為系統(tǒng)初始化時(shí)各個(gè)系統(tǒng)如RenderSystem,SoundSystem的實(shí)例被創(chuàng)建它們繼承自對(duì)應(yīng)的ReceiverXxxEvent。在系統(tǒng)構(gòu)造函數(shù)中基類ReceiverXxxEvent會(huì)向EventManager注冊(cè)自己。游戲運(yùn)行時(shí)任何代碼通常是某個(gè)系統(tǒng)都可以通過EventManager::emitXxxEvent(event)來發(fā)射一個(gè)事件。EventManager查找所有訂閱了XxxEvent的Receiver并同步地、依次調(diào)用它們的receive方法。當(dāng)系統(tǒng)被銷毀時(shí)Receiver的析構(gòu)函數(shù)會(huì)自動(dòng)從EventManager中注銷自己。2.2 同步 vs 異步事件分發(fā)EntityX 的事件分發(fā)是同步的。這意味著當(dāng)emit被調(diào)用時(shí)所有訂閱者的receive方法會(huì)在當(dāng)前線程中立即被依次調(diào)用直到所有處理函數(shù)都執(zhí)行完畢emit函數(shù)才會(huì)返回。這種設(shè)計(jì)簡(jiǎn)單、直接、可預(yù)測(cè)對(duì)于絕大多數(shù)游戲邏輯事件如碰撞、攻擊、拾取道具來說是完全合適的因?yàn)槟阃ǔOM@些邏輯在同一個(gè)幀內(nèi)被處理完畢。但是同步分發(fā)也帶來一個(gè)潛在問題如果某個(gè)接收者的receive方法執(zhí)行了非常耗時(shí)的操作比如同步加載一個(gè)大資源它會(huì)阻塞所有后續(xù)接收者以及事件發(fā)射者的執(zhí)行。因此在定義事件和處理事件時(shí)一個(gè)重要的經(jīng)驗(yàn)法則是事件處理函數(shù)應(yīng)該盡可能快只做必要的狀態(tài)更新和輕量級(jí)操作將耗時(shí)任務(wù)排隊(duì)到其他線程或系統(tǒng)去處理。如果你確實(shí)需要異步事件EntityX 本身并未直接提供支持但你可以很容易地在它之上構(gòu)建。例如你可以在事件處理函數(shù)中將任務(wù)推入一個(gè)線程池隊(duì)列或者發(fā)射另一個(gè)專門用于異步處理的事件。3. 從零開始使用事件模塊一個(gè)完整示例理論說再多不如看代碼。讓我們通過一個(gè)簡(jiǎn)單的“太空射擊游戲”片段來看看如何定義、發(fā)射和接收事件。3.1 第一步定義你的事件類型首先我們定義游戲中可能需要的幾種事件。這些就是普通的C結(jié)構(gòu)體。// 事件定義 struct CollisionEvent { Entity entityA; Entity entityB; // 可以添加碰撞點(diǎn)、法向量等更多信息 }; struct DamageEvent { Entity target; // 承受傷害的實(shí)體 Entity source; // 傷害來源實(shí)體可能是Entity()表示環(huán)境傷害 int amount; // 傷害值 }; struct EnemyDestroyedEvent { Entity enemy; int scoreValue; // 擊毀該敵人獲得的分?jǐn)?shù) }; struct PlayerHealthChangedEvent { Entity player; int currentHealth; int maxHealth; };3.2 第二步創(chuàng)建接收事件的系統(tǒng)接著我們創(chuàng)建兩個(gè)系統(tǒng)PhysicsSystem負(fù)責(zé)檢測(cè)碰撞并發(fā)射事件CombatSystem和UISystem負(fù)責(zé)接收并處理事件。#include entityx/entityx.h using namespace entityx; // 物理系統(tǒng)檢測(cè)碰撞并發(fā)射 CollisionEvent class PhysicsSystem : public SystemPhysicsSystem { public: void update(EntityManager es, EventManager events, TimeDelta dt) override { // 簡(jiǎn)化的碰撞檢測(cè)偽代碼 es.eachCollisionBox, Position([events](Entity entityA, CollisionBox boxA, Position posA) { es.eachCollisionBox, Position([entityA, boxA, posA, events](Entity entityB, CollisionBox boxB, Position posB) { if (entityA ! entityB checkCollision(boxA, posA, boxB, posB)) { // 關(guān)鍵發(fā)射碰撞事件 events.emitCollisionEvent(CollisionEvent{entityA, entityB}); } }); }); } private: bool checkCollision(const CollisionBox a, const Position pa, const CollisionBox b, const Position pb) { // 實(shí)際的碰撞檢測(cè)邏輯... return false; } }; // 戰(zhàn)斗系統(tǒng)接收碰撞事件判斷傷害并發(fā)射傷害和摧毀事件 class CombatSystem : public SystemCombatSystem, public ReceiverCombatSystem { // 繼承Receiver public: // 必須的配置方法告訴EventManager這個(gè)系統(tǒng)訂閱了哪些事件 void configure(EventManager events) override { events.subscribeCollisionEvent(*this); events.subscribeDamageEvent(*this); } // 接收并處理 CollisionEvent void receive(const CollisionEvent collision) { auto es *entities; // 從System基類獲取EntityManager // 假設(shè)我們有一個(gè)簡(jiǎn)單的規(guī)則如果碰撞雙方都有Health組件則互相造成傷害 if (es.has_componentHealth(collision.entityA) es.has_componentHealth(collision.entityB)) { // 發(fā)射傷害事件 events-emitDamageEvent(DamageEvent{collision.entityB, collision.entityA, 10}); events-emitDamageEvent(DamageEvent{collision.entityA, collision.entityB, 10}); } // 更復(fù)雜的邏輯子彈 vs 敵人玩家 vs 墻壁等... } // 接收并處理 DamageEvent void receive(const DamageEvent damage) { if (auto health entities-componentHealth(damage.target)) { health-current - damage.amount; if (health-current 0) { // 實(shí)體死亡可能發(fā)射摧毀事件 if (damage.target.has_componentEnemy()) { events-emitEnemyDestroyedEvent(EnemyDestroyedEvent{damage.target, 100}); } // 銷毀實(shí)體 damage.target.destroy(); } // 通知UI更新血條 events-emitPlayerHealthChangedEvent( PlayerHealthChangedEvent{damage.target, health-current, health-max} ); } } }; // UI系統(tǒng)接收游戲狀態(tài)事件并更新界面 class UISystem : public SystemUISystem, public ReceiverUISystem { public: void configure(EventManager events) override { events.subscribePlayerHealthChangedEvent(*this); events.subscribeEnemyDestroyedEvent(*this); } void receive(const PlayerHealthChangedEvent healthEvent) { // 更新屏幕上的血條UI std::cout Player Health: healthEvent.currentHealth / healthEvent.maxHealth std::endl; } void receive(const EnemyDestroyedEvent destroyedEvent) { // 更新分?jǐn)?shù)顯示 std::cout Enemy Destroyed! Score destroyedEvent.scoreValue std::endl; } };3.3 第三步組裝系統(tǒng)并運(yùn)行世界最后我們將所有系統(tǒng)組裝起來并運(yùn)行游戲主循環(huán)。int main() { EntityX ex; // 默認(rèn)創(chuàng)建了 EntityManager, EventManager, SystemManager // 獲取系統(tǒng)管理器并添加系統(tǒng) auto systems ex.systems; systems.addPhysicsSystem(); systems.addCombatSystem(); systems.addUISystem(); systems.configure(); // 這會(huì)調(diào)用所有系統(tǒng)的 configure() 方法完成事件訂閱 // 創(chuàng)建一些測(cè)試實(shí)體玩家、敵人... Entity player ex.entities.create(); player.assignHealth(100, 100); player.assignPosition(0, 0); player.assignCollisionBox(10, 10); Entity enemy ex.entities.create(); enemy.assignHealth(50, 50); enemy.assignPosition(5, 5); enemy.assignCollisionBox(8, 8); enemy.assignEnemy(); // 簡(jiǎn)化的游戲主循環(huán) for (int i 0; i 100; i) { // 1. 更新所有系統(tǒng)。PhysicsSystem會(huì)在update中檢測(cè)碰撞并發(fā)射事件。 systems.update_all(1.0f / 60.0f); // 假設(shè)60幀 // 2. EventManager會(huì)在emit時(shí)同步調(diào)用CombatSystem和UISystem的receive方法。 // 3. 事件處理鏈碰撞 - 傷害 - 血條更新/分?jǐn)?shù)更新/實(shí)體銷毀。 } return 0; }通過這個(gè)例子你可以清晰地看到事件如何像鏈條一樣將不同的系統(tǒng)連接起來PhysicsSystem只管碰撞檢測(cè)和發(fā)射事件完全不知道后面誰(shuí)會(huì)處理CombatSystem訂閱碰撞事件處理游戲邏輯并發(fā)射新的事件UISystem訂閱游戲狀態(tài)事件負(fù)責(zé)顯示更新。每個(gè)系統(tǒng)職責(zé)單一耦合度極低。4. 深入源碼事件模塊是如何實(shí)現(xiàn)的理解了如何使用我們?cè)賮砀Q探一下EntityX事件模塊的內(nèi)部實(shí)現(xiàn)這能幫助我們更好地使用它并在遇到問題時(shí)進(jìn)行調(diào)試。我們主要關(guān)注event.h和event.cc這兩個(gè)文件。4.1 EventManager 的內(nèi)部容器EventManager的核心是一個(gè)存儲(chǔ)訂閱關(guān)系的數(shù)據(jù)結(jié)構(gòu)。它使用std::unordered_map將事件類型映射到一個(gè)接收者列表。// 簡(jiǎn)化后的內(nèi)部結(jié)構(gòu)示意 class EventManager { private: // 類型擦除的基類指針用于存儲(chǔ)任意類型的 Receiver 實(shí)例 struct ReceiverBase { virtual ~ReceiverBase() default; }; // 針對(duì)特定事件類型的 Receiver 包裝器 template typename Event struct ReceiverWrapper : ReceiverBase { ReceiverEvent *receiver; explicit ReceiverWrapper(ReceiverEvent *receiver) : receiver(receiver) {} }; // 關(guān)鍵數(shù)據(jù)結(jié)構(gòu)事件類型ID - 該類型事件的接收者列表 std::unordered_mapTypeId, std::vectorstd::unique_ptrReceiverBase receivers_; };這里用到了一個(gè)關(guān)鍵技巧類型擦除Type Erasure。因?yàn)镽eceiverCollisionEvent和ReceiverDamageEvent是不同的類型無(wú)法直接放在同一個(gè)vector里。EntityX 通過一個(gè)非模板的基類ReceiverBase和模板派生類ReceiverWrapperEvent來解決這個(gè)問題。ReceiverWrapper存儲(chǔ)了具體ReceiverEvent的指針而receivers_存儲(chǔ)的是ReceiverBase的智能指針從而實(shí)現(xiàn)了異構(gòu)容器。TypeId是 EntityX 內(nèi)部用于唯一標(biāo)識(shí)類型的一個(gè)整數(shù)值通常通過type_idEvent()函數(shù)獲取這個(gè)函數(shù)會(huì)對(duì)每種類型返回一個(gè)編譯期確定的常量。4.2 訂閱subscribe過程剖析當(dāng)CombatSystem在configure中調(diào)用events.subscribeCollisionEvent(*this)時(shí)發(fā)生了什么template typename Event void EventManager::subscribe(ReceiverEvent receiver) { const auto type_id type_idEvent(); auto receivers receivers_[type_id]; // 獲取或創(chuàng)建該事件類型的接收者列表 // 檢查是否已經(jīng)訂閱過避免重復(fù) auto it std::find_if(receivers.begin(), receivers.end(), [receiver](const std::unique_ptrReceiverBase base) { auto *wrapper static_castReceiverWrapperEvent*(base.get()); return wrapper-receiver receiver; }); if (it receivers.end()) { // 創(chuàng)建包裝器并存入列表 receivers.emplace_back(std::make_uniqueReceiverWrapperEvent(receiver)); } }這個(gè)過程是線程不安全的。EntityX 的事件系統(tǒng)設(shè)計(jì)假設(shè)訂閱發(fā)生在初始化階段configure此時(shí)通常是單線程的。4.3 發(fā)射emit與分發(fā)過程剖析發(fā)射事件的過程是同步遍歷調(diào)用。template typename Event, typename ...Args void EventManager::emit(Args ... args) { const auto type_id type_idEvent(); auto it receivers_.find(type_id); if (it ! receivers_.end()) { // 臨時(shí)創(chuàng)建事件對(duì)象。Args... 允許直接傳遞構(gòu)造參數(shù)給Event。 Event event(std::forwardArgs(args)...); auto receivers it-second; // 遍歷所有訂閱了此事件的接收者 for (auto base : receivers) { // 關(guān)鍵的一步將基類指針安全地向下轉(zhuǎn)型為具體的Wrapper auto *wrapper static_castReceiverWrapperEvent*(base.get()); // 調(diào)用接收者的 receive 方法 wrapper-receiver-receive(event); } } }這里有幾個(gè)值得注意的點(diǎn)事件對(duì)象生命周期事件對(duì)象在emit函數(shù)棧上創(chuàng)建。對(duì)于每個(gè)接收者傳遞的都是這個(gè)對(duì)象的const引用。這意味著所有接收者處理的是同一個(gè)事件對(duì)象。因此絕對(duì)不要在receive方法中修改事件對(duì)象除非你明確知道所有接收者都期望這種修改并且順序是確定的這通常是個(gè)壞主意。異常安全如果某個(gè)接收者的receive方法拋出異常這個(gè)異常會(huì)傳播到emit調(diào)用處并中斷后續(xù)接收者的調(diào)用。你需要確保事件處理函數(shù)是異常安全的或者在外層捕獲異常。性能考量遍歷vector并調(diào)用虛函數(shù)receive是通過Receiver基類接口調(diào)用的是有開銷的。對(duì)于每幀發(fā)射成千上萬(wàn)次的高頻事件比如每個(gè)實(shí)體的PositionUpdatedEvent這種開銷可能成為瓶頸。對(duì)于這種情況更好的模式是使用數(shù)據(jù)組件如Position組件并通過系統(tǒng)查詢來處理而不是事件。4.4 Receiver 的自動(dòng)生命周期管理Receiver類的實(shí)現(xiàn)巧妙地利用了構(gòu)造函數(shù)和析構(gòu)函數(shù)來自動(dòng)管理訂閱關(guān)系。template typename Events class Receiver { public: virtual ~Receiver() { if (event_manager_) { event_manager_-unsubscribeEvents(*this); } } // ... private: EventManager *event_manager_ nullptr; // 友元聲明允許 EventManager 調(diào)用 configure_receiver template typename, typename friend class EventManager; };當(dāng)一個(gè)系統(tǒng)如CombatSystem繼承Receiver時(shí)它通常會(huì)在構(gòu)造函數(shù)中或通過EventManager的configure方法設(shè)置event_manager_指針。當(dāng)系統(tǒng)被銷毀時(shí)Receiver的析構(gòu)函數(shù)會(huì)自動(dòng)調(diào)用unsubscribe將自己從所有事件列表中移除完美避免了“野指針”回調(diào)導(dǎo)致崩潰的問題。這是C RAII理念的絕佳實(shí)踐。5. 高級(jí)用法與性能優(yōu)化實(shí)戰(zhàn)了解了基本原理后我們來看看如何在實(shí)際項(xiàng)目中更高效、更安全地使用EntityX事件模塊。5.1 使用事件類繼承與類型過濾有時(shí)你希望一個(gè)接收者能處理一類相似的事件。EntityX本身不支持基于基類的事件分發(fā)但你可以通過組合方式實(shí)現(xiàn)。// 定義一個(gè)基礎(chǔ)事件 struct BaseGameEvent { Entity sourceEntity; TimePoint timestamp; }; // 派生具體事件 struct DamageEvent : public BaseGameEvent { int amount; DamageType type; }; struct HealEvent : public BaseGameEvent { int amount; }; // 日志系統(tǒng)希望記錄所有游戲事件 class LoggingSystem : public SystemLoggingSystem { public: void configure(EventManager events) { events.subscribeDamageEvent(*this); events.subscribeHealEvent(*this); // ... 訂閱所有BaseGameEvent的派生類 } // 需要為每種事件寫一個(gè)receive內(nèi)部可以調(diào)用一個(gè)公共處理函數(shù) void receive(const DamageEvent e) { logEvent(Damage, e); } void receive(const HealEvent e) { logEvent(Heal, e); } private: void logEvent(const std::string type, const BaseGameEvent e) { std::cout [ e.timestamp ] type from Entity e.sourceEntity.id() std::endl; } };雖然需要為每個(gè)派生事件寫一個(gè)receive轉(zhuǎn)發(fā)函數(shù)但這保證了類型安全并且模式很清晰。切記不要嘗試將EventManager::subscribe與基類類型一起使用因?yàn)槟0鍣C(jī)制會(huì)將其視為完全不同的類型。5.2 高頻事件的優(yōu)化策略事件隊(duì)列與批量處理對(duì)于像PositionChangedEvent這樣的高頻事件每幀為每個(gè)移動(dòng)的實(shí)體都emit一次是不可接受的。解決方案是使用事件隊(duì)列進(jìn)行批處理。// 1. 定義一個(gè)批量事件 struct BatchPositionEvents { std::vectorstd::pairEntity, Vector2 updates; }; // 2. 在移動(dòng)系統(tǒng)中不再立即emit而是收集到臨時(shí)容器 class MovementSystem : public SystemMovementSystem { public: void update(EntityManager es, EventManager events, TimeDelta dt) override { std::vectorstd::pairEntity, Vector2 frameUpdates; es.eachPosition, Velocity([frameUpdates, dt](Entity e, Position pos, Velocity vel) { pos.x vel.dx * dt; pos.y vel.dy * dt; // 收集而不是發(fā)射 frameUpdates.emplace_back(e, Vector2{pos.x, pos.y}); }); // 在update的最后一次性發(fā)射一個(gè)批量事件 if (!frameUpdates.empty()) { events.emitBatchPositionEvents(BatchPositionEvents{std::move(frameUpdates)}); } } }; // 3. 其他系統(tǒng)如渲染插值系統(tǒng)、網(wǎng)絡(luò)同步系統(tǒng)訂閱這個(gè)批量事件 class InterpolationSystem : public SystemInterpolationSystem, public ReceiverInterpolationSystem { public: void configure(EventManager events) override { events.subscribeBatchPositionEvents(*this); } void receive(const BatchPositionEvents batch) { for (const auto [entity, newPos] : batch.updates) { // 平滑插值到新位置 // ... } } };這種方法將 O(N) 次的事件發(fā)射和分發(fā)調(diào)用減少到 O(1) 次極大地提升了性能。代價(jià)是增加了少量?jī)?nèi)存用于收集數(shù)據(jù)并引入了一幀的延遲事件在本幀末收集下一幀初被處理。對(duì)于大多數(shù)情況這是完全可以接受的。5.3 確保事件處理函數(shù)的線程安全EntityX 的事件系統(tǒng)本身不是線程安全的。subscribe,unsubscribe,emit操作如果從多個(gè)線程調(diào)用會(huì)導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)。通常的實(shí)踐是訂閱/取消訂閱只在主線程初始化階段configure或系統(tǒng)創(chuàng)建/銷毀時(shí)進(jìn)行。發(fā)射事件盡量只在主線程的游戲邏輯循環(huán)中發(fā)射。如果其他工作線程如網(wǎng)絡(luò)線程、資源加載線程需要通知主線程應(yīng)該通過線程安全的隊(duì)列將事件對(duì)象傳遞到主線程由主線程在下一幀統(tǒng)一emit。// 一個(gè)簡(jiǎn)單的線程間事件傳遞方案 class ThreadSafeEventQueue { public: template typename Event void pushFromWorkerThread(Event event) { std::lock_guardstd::mutex lock(mutex_); // 需要使用類型擦除來存儲(chǔ)任意事件這里簡(jiǎn)化表示 queue_.push_back(std::make_anyEvent(std::forwardEvent(event))); } void processInMainThread(EventManager mainEventManager) { std::lock_guardstd::mutex lock(mutex_); for (auto eventAny : queue_) { // 這里需要根據(jù) eventAny 中存儲(chǔ)的類型信息調(diào)用對(duì)應(yīng)的 mainEventManager.emit // 實(shí)際實(shí)現(xiàn)需要更復(fù)雜的類型映射此處為概念展示 } queue_.clear(); } private: std::mutex mutex_; std::vectorstd::any queue_; }; // 在主循環(huán)中 int main() { EntityX ex; ThreadSafeEventQueue crossThreadQueue; // ... 初始化系統(tǒng) while (gameRunning) { // 1. 處理從其他線程過來的事件 crossThreadQueue.processInMainThread(ex.events); // 2. 正常更新系統(tǒng)可能會(huì)emit事件 systems.update_all(dt); // ... 其他主循環(huán)邏輯 } }6. 常見陷阱、調(diào)試技巧與最佳實(shí)踐在實(shí)際使用中我踩過不少坑也總結(jié)出一些讓代碼更健壯的經(jīng)驗(yàn)。6.1 陷阱一在事件處理函數(shù)中發(fā)射同一事件這會(huì)導(dǎo)致無(wú)限遞歸和棧溢出。void receive(const SomeEvent e) { // 錯(cuò)誤這會(huì)導(dǎo)致直接或間接的無(wú)限循環(huán)。 events-emitSomeEvent(e); }解決方案仔細(xì)審查事件處理邏輯。如果確實(shí)需要“重新觸發(fā)”或“廣播放大”一個(gè)事件考慮發(fā)射一個(gè)不同但相關(guān)的事件或者使用一個(gè)標(biāo)志位來防止重入。6.2 陷阱二事件處理函數(shù)修改了實(shí)體組件影響了迭代器這是一個(gè)非常隱蔽的錯(cuò)誤。假設(shè)你在一個(gè)es.each循環(huán)中發(fā)射了一個(gè)事件而某個(gè)事件處理函數(shù)銷毀了正在被迭代的實(shí)體或者創(chuàng)建了新的符合迭代條件的實(shí)體這會(huì)導(dǎo)致迭代器失效可能引發(fā)崩潰或未定義行為。// 在 PhysicsSystem 的 update 中 es.eachHealth([events](Entity e, Health h) { if (h.current 0) { events.emitEntityDiedEvent(EntityDiedEvent{e}); // 危險(xiǎn) } }); // 在另一個(gè)系統(tǒng)的 receive 中 void receive(const EntityDiedEvent e) { e.entity.destroy(); // 如果 e.entity 正好是 PhysicsSystem 正在迭代的那個(gè)就出問題了。 }解決方案將“銷毀實(shí)體”這類操作延遲到所有系統(tǒng)update完成之后。常見的模式是發(fā)射一個(gè)EntityDestroyRequestEvent由一個(gè)專門的DestroySystem在所有其他系統(tǒng)更新完畢后統(tǒng)一處理銷毀請(qǐng)求。class DestroySystem : public SystemDestroySystem, public ReceiverDestroySystem { public: void configure(EventManager events) override { events.subscribeEntityDestroyRequestEvent(*this); } void receive(const EntityDestroyRequestEvent e) { pendingDestroys_.push_back(e.entity); } // 在所有其他系統(tǒng)update之后被調(diào)用 void update(EntityManager es, EventManager events, TimeDelta dt) override { for (Entity e : pendingDestroys_) { e.destroy(); } pendingDestroys_.clear(); } private: std::vectorEntity pendingDestroys_; };6.3 調(diào)試技巧可視化事件流當(dāng)事件系統(tǒng)復(fù)雜后調(diào)試“為什么這個(gè)事件沒被處理”或“這個(gè)事件被誰(shuí)處理了”會(huì)很頭疼。可以創(chuàng)建一個(gè)簡(jiǎn)單的EventDebugSystem。class EventDebugSystem : public ReceiverEventDebugSystem { public: // 使用可變參數(shù)模板來訂閱所有事件這是一個(gè)高級(jí)技巧。 template typename Event void subscribeTo(EventManager events) { events.subscribeEvent(*this); } // 一個(gè)通用的 receive 模板捕獲所有事件 template typename Event void receive(const Event e) { std::cout [EventDebug] Type: typeid(Event).name() , Address: e std::endl; // 可以在這里打印事件內(nèi)容或者記錄到文件 } }; // 在配置時(shí)手動(dòng)或通過反射注冊(cè)需要調(diào)試的事件類型 debugSystem.subscribeToCollisionEvent(events); debugSystem.subscribeToDamageEvent(events); // ...6.4 最佳實(shí)踐清單根據(jù)我的項(xiàng)目經(jīng)驗(yàn)遵循以下實(shí)踐能讓基于EntityX事件系統(tǒng)的代碼更清晰、更健壯事件即數(shù)據(jù)保持輕量事件結(jié)構(gòu)體應(yīng)只包含必要的數(shù)據(jù)避免包含智能指針、大型容器或復(fù)雜對(duì)象。優(yōu)先傳遞ID、索引或簡(jiǎn)單值類型。明確命名事件名使用名詞或名詞短語(yǔ)清晰表達(dá)“發(fā)生了什么”如PlayerJumped,InventoryItemAdded,AchievementUnlocked。單一職責(zé)一個(gè)事件應(yīng)只代表一件事。不要?jiǎng)?chuàng)建PlayerActionEvent這種包含enum ActionType的“萬(wàn)能事件”而是拆分成PlayerMovedEvent,PlayerAttackedEvent等。區(qū)分命令與事件RequestPlayerMove命令和PlayerMoved事件是不同的。命令是“希望做某事”事件是“某事已經(jīng)發(fā)生”。避免混淆。文檔化事件契約在事件結(jié)構(gòu)體的定義處用注釋說明誰(shuí)在什么情況下發(fā)射這個(gè)事件事件中的數(shù)據(jù)代表什么含義哪些系統(tǒng)可能會(huì)監(jiān)聽它控制事件粒度不要過度使用事件。對(duì)于每幀都發(fā)生的、數(shù)據(jù)驅(qū)動(dòng)的狀態(tài)同步如位置、旋轉(zhuǎn)使用組件和系統(tǒng)查詢通常比事件更高效。性能熱點(diǎn)監(jiān)控在性能分析工具如tracy、easy_profiler中標(biāo)記emit調(diào)用監(jiān)控高頻事件的性能消耗。