{"id":30754422,"url":"https://github.com/alaxxxx/echo","last_synced_at":"2025-09-04T09:10:17.648Z","repository":{"id":312589590,"uuid":"1020845602","full_name":"Alaxxxx/Echo","owner":"Alaxxxx","description":"Unity EventBus system","archived":false,"fork":false,"pushed_at":"2025-08-31T17:22:41.000Z","size":86,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-08-31T19:19:19.206Z","etag":null,"topics":["eventbus","events","unity"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Alaxxxx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-07-16T13:35:12.000Z","updated_at":"2025-08-31T17:22:44.000Z","dependencies_parsed_at":"2025-08-31T19:19:24.854Z","dependency_job_id":"bccf2e91-0b46-4a2f-8e8d-3a8decd121ca","html_url":"https://github.com/Alaxxxx/Echo","commit_stats":null,"previous_names":["alaxxxx/echo"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/Alaxxxx/Echo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Alaxxxx%2FEcho","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Alaxxxx%2FEcho/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Alaxxxx%2FEcho/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Alaxxxx%2FEcho/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Alaxxxx","download_url":"https://codeload.github.com/Alaxxxx/Echo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Alaxxxx%2FEcho/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273581326,"owners_count":25131393,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-04T02:00:08.968Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["eventbus","events","unity"],"created_at":"2025-09-04T09:09:30.949Z","updated_at":"2025-09-04T09:10:17.093Z","avatar_url":"https://github.com/Alaxxxx.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Echo: High-Performance Event Bus for Unity\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/Alaxxxx/Echo/stargazers\"\u003e\u003cimg src=\"https://img.shields.io/github/stars/Alaxxxx/Echo?style=flat-square\u0026logo=github\u0026color=FFC107\" alt=\"GitHub Stars\"\u003e\u003c/a\u003e\n  \u0026nbsp;\n  \u003ca href=\"https://github.com/Alaxxxx?tab=followers\"\u003e\u003cimg src=\"https://img.shields.io/github/followers/Alaxxxx?style=flat-square\u0026logo=github\u0026label=Followers\u0026color=282c34\" alt=\"GitHub Followers\"\u003e\u003c/a\u003e\n  \u0026nbsp;\n  \u003ca href=\"https://github.com/Alaxxxx/Echo/commits/main\"\u003e\u003cimg src=\"https://img.shields.io/github/last-commit/Alaxxxx/Echo?style=flat-square\u0026logo=github\u0026color=blueviolet\" alt=\"Last Commit\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/Alaxxxx/Echo/releases\"\u003e\u003cimg src=\"https://img.shields.io/github/v/release/Alaxxxx/Echo?style=flat-square\" alt=\"Release\"\u003e\u003c/a\u003e\n  \u0026nbsp;\n  \u003ca href=\"https://unity.com/\"\u003e\u003cimg src=\"https://img.shields.io/badge/Unity-2021.3+-2296F3.svg?style=flat-square\u0026logo=unity\" alt=\"Unity Version\"\u003e\u003c/a\u003e\n  \u0026nbsp;\n  \u003ca href=\"https://github.com/Alaxxxx/Echo/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/github/license/Alaxxxx/Echo?style=flat-square\" alt=\"License\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n**Echo** is a static, high-performance, and type-safe event bus system designed for Unity. It leverages `struct`-based events to achieve **zero-allocation publishing**, eliminating garbage collector spikes and ensuring smooth performance, even in high-frequency scenarios.\n\nWith a fluent filtering API, automatic subscription management, and seamless integration with GameObjects, Echo provides a powerful yet simple solution for creating decoupled and maintainable code architecture in your projects.\n\n\u003cbr\u003e\n\n## ✨ Features\n\n- **🚀 Zero-Allocation Publishing**: Utilizes `structs` for events to prevent heap allocations and GC pressure.\n- **🔒 Type-Safe by Design**: Generic, interface-constrained API prevents runtime errors by ensuring event correctness at compile time.\n- **🎯 Advanced Filtering API**: A fluent, chainable interface (`Where(...).And(...).Or(...)`) to subscribe to events that meet complex conditions.\n- **♻️ Automatic Subscription Management**: Scoped subscriptions (`IDisposable`) handle cleanup automatically, preventing common memory leaks.\n- **📦 Event Batching \u0026 Aggregation**: Collect high-frequency events and publish them in batches to optimize performance.\n- **🎮 Unity Integration**: Helper extensions for GameObjects allow for easy source/target event tracking.\n\n\u003cbr\u003e\n\n## 🚀 Getting Started\n\n### Installation\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e1. Install via Git URL (Recommended)\u003c/strong\u003e\u003c/summary\u003e\n\u003cbr\u003e\nThis method installs the package directly from the GitHub repository and allows you to easily update to the latest version.\n\n1.  In Unity, open the **Package Manager** (`Window \u003e Package Management \u003e Package Manager`).\n2.  Click the **+** button in the top-left corner and select **\"Add package from git URL...\"**.\n3.  Enter the following URL and click \"Install\":\n    ```\n    https://github.com/Alaxxxx/Echo.git\n    ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e2. Install via .unitypackage\u003c/strong\u003e\u003c/summary\u003e\n\u003cbr\u003e\nThis method is great if you prefer a specific, stable version of the asset.\n\n1.  Go to the [**Releases**](https://github.com/Alaxxxx/Echo/releases) page.\n2.  Download the `.unitypackage` file from the latest release.\n3.  In your Unity project, go to **`Assets \u003e Import Package \u003e Custom Package...`** and select the downloaded file.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003e3. Manual Installation (from .zip)\u003c/strong\u003e\u003c/summary\u003e\n\u003cbr\u003e\n\n1.  Download this repository as a ZIP file by clicking **`Code \u003e Download ZIP`** on the main repository page.\n2.  Unzip the downloaded file.\n3.  Drag and drop the main asset folder (the one containing all the scripts and resources) into the `Assets` folder of your Unity project.\n\u003c/details\u003e\n\n**Requirements:**\n- Unity 2021.3 or higher\n- .NET Standard 2.1 or higher\n\n\u003cbr\u003e\n\n## The Basics\n\nUsing Echo involves three simple steps: defining, publishing, and listening to events.\n\n### Understanding the Event System\n\nEcho's event bus is built around **`struct`**-based events to ensure **zero-allocation** publishing and maximum performance. \n\u003e [!NOTE]\n\u003e It is essential to understand that the system is **synchronous by default**: an event is published and fully handled within the same frame, providing predictable code flow. For asynchronous needs, such as delays, specific extension methods are available.\n\n\u003cbr\u003e\n\n### Event Types\n\n**IEvent - Basic Events**\n```csharp\nusing Echo.Interface;\nusing UnityEngine;\n\n// An event carrying data about a player's death\npublic struct PlayerDiedEvent : IEvent\n{\n    public int PlayerId;\n    public Vector3 Position;\n}\n```\n\n\u003cbr\u003e\n\n**ITrackedEvent - Source/Target Events**\n```csharp\nusing Echo.Interface;\n\npublic struct DamageEvent : ITrackedEvent\n{\n    public int SourceId { get; set; }  // Required by interface\n    public int TargetId { get; set; }  // Required by interface\n    public float Damage;\n    public DamageType Type;\n}\n```\n\n\u003cbr\u003e\n\n**Why Structs?**\n- **Zero allocations**: No garbage collection pressure during event publishing\n- **Memory efficiency**: Events are copied by value, no heap allocations\n- **Performance**: Direct memory access with no indirection\n\n**IEvent vs ITrackedEvent:**\n- `IEvent`: Use for general game events (UI updates, state changes, notifications)\n- `ITrackedEvent`: Use when you need to track relationships between entities (combat, interactions, AI communication)\n\n\u003cbr\u003e\n\n### 1. Publishing Events\n\n```csharp\nusing Echo.Core.Extensions;\nusing UnityEngine;\n\npublic class PlayerHealth : MonoBehaviour\n{\n    public int playerId = 1;\n\n    public void Die()\n    {\n        // Method 1: Using extensions (recommended)\n        new PlayerDiedEvent\n        {\n            PlayerId = this.playerId,\n            Position = transform.position\n        }.Fire(); // The Fire() extension publishes the event\n        \n        // Method 2: Direct publishing\n        EventBus.Publish(new PlayerDiedEvent \n        { \n            PlayerId = this.playerId, \n            Position = transform.position \n        });\n    }\n}\n```\n\n### 2. Subscribing to Events\n\nThe standard pattern in Unity is to subscribe in `OnEnable()` and always unsubscribe in `OnDisable()` to prevent memory leaks.\n\n```csharp\nusing Echo.Core;\nusing UnityEngine;\n\npublic class GameManager : MonoBehaviour\n{\n    private void OnEnable()\n    {\n        // Subscribe to the event and specify the handler method\n        EventBus.Subscribe\u003cPlayerDiedEvent\u003e(OnPlayerDied);\n    }\n\n    private void OnDisable()\n    {\n        // VERY IMPORTANT: Unsubscribe to prevent memory leaks and errors\n        EventBus.Unsubscribe\u003cPlayerDiedEvent\u003e(OnPlayerDied);\n    }\n\n    private void OnPlayerDied(PlayerDiedEvent eventData)\n    {\n        Debug.Log($\"Player {eventData.PlayerId} died at {eventData.Position}!\");\n        // ... logic to handle the player's death (e.g., respawn, update UI) ...\n    }\n}\n```\n\n### 3. Event Markers - Zero-Data Events\n\nEvent markers are structs with no data, perfect for simple notifications:\n\n```csharp\npublic struct GameStartedEvent : IEvent { }\npublic struct LevelCompletedEvent : IEvent { }\npublic struct PauseRequestedEvent : IEvent { }\n\n// Usage\nnew GameStartedEvent().Fire();\n\n// Subscribe\nEventBus.Subscribe\u003cGameStartedEvent\u003e(OnGameStarted);\n\nvoid OnGameStarted(GameStartedEvent evt)\n{\n    // evt parameter exists but contains no data\n    InitializeGame();\n}\n```\n\n**Why use Event Markers?**\n- **Decoupling**: Systems don't need direct references to each other\n- **Flexibility**: Easy to add new listeners without modifying existing code\n- **Debugging**: Clear event flow in your game's architecture\n- **Zero cost**: No memory overhead, just a type signature\n\n\u003cbr\u003e\n\n## ♻️ Automatic Cleanup with Scoped Subscriptions\n\nForgetting to unsubscribe from an event in `OnDisable` is one of the most common sources of memory leaks and bugs in Unity. To make this process more robust, Echo offers a safer pattern based on the `IDisposable` interface.\n\nThe key insight is that this makes unsubscribing simpler and harder to get wrong. Instead of needing to manually call `Unsubscribe` with the exact same method reference, you just hold onto the subscription object and call its `Dispose()` method.\n\n\u003cbr\u003e\n\n### 1. Use Case 1: Subscriptions Tied to a MonoBehaviour's Lifecycle\n\nThis is the most frequent scenario: a component needs to listen for an event as long as it's active (`OnEnable`) and must stop listening when it's disabled `OnDisable`).\n\nThe advantage here is that you no longer need to worry about which handler method you passed to `Subscribe`. Just call `.Dispose()` on the subscription object, and it cleans itself up.\n\n```csharp\nusing Echo.Core;\nusing System;\nusing UnityEngine;\n\npublic class UINotificationManager : MonoBehaviour\n{\n    // Store the subscription object, which is \"Disposable\"\n    private IDisposable _gameOverSubscription;\n\n    void OnEnable()\n    {\n        // Subscribe to the event and keep the returned IDisposable object.\n        _gameOverSubscription = EventBus.SubscribeScoped\u003cGameOverEvent\u003e(ShowGameOverScreen);\n    }\n\n    void OnDisable()\n    {\n        // By calling Dispose(), the object handles   \n        // its own unsubscription from the EventBus.\n        _gameOverSubscription?.Dispose();\n    }\n\n    private void ShowGameOverScreen(GameOverEvent evt)\n    {\n        // ... logic to show the Game Over screen ...\n    }\n}\n```\n\n\u003cbr\u003e\n\n### 2. Use Case 2: Temporary Subscriptions with `using` (Truly Automatic Cleanup)\n\nThere are times when you only need to listen for an event within a specific scope, like a single method or a coroutine. This is where the `IDisposable` pattern becomes incredibly powerful with C#'s `using` statement, which provides fully guaranteed and automatic cleanup.\n\nAs soon as the code execution leaves the **using** block—whether normally, through a `return`, or via an exception—the `Dispose()` method is called automatically.\n\nImagine a tutorial that waits for the player to perform a \"jump\" action, but only for a few seconds.\n\n```csharp\nusing Echo.Core;\nusing System;\nusing UnityEngine;\n\npublic class TutorialManager : MonoBehaviour\n{\n    // A coroutine that waits for a specific action\n    public void PromptForJump()\n    {\n        StartCoroutine(WaitForJumpAction());\n    }\n\n    private System.Collections.IEnumerator WaitForJumpAction()\n    {\n        Debug.Log(\"Tutorial: Please jump now!\");\n\n        // We subscribe to 'PlayerJumpedEvent' only within this 'using' block.\n        using var jumpSubscription = EventBus.SubscribeScoped\u003cPlayerJumpedEvent\u003e(OnPlayerJumped);\n\n        // Wait for 5 seconds. The subscription is active during this time.\n        yield return new WaitForSeconds(5f);\n\n        // At the end of this yield, the method continues and the 'using' block ends.\n        // 'jumpSubscription.Dispose()' is now called automatically,\n        // which cleans up the subscription. If the player hasn't jumped in 5 seconds,\n        // we stop listening.\n    }\n\n    private void OnPlayerJumped(PlayerJumpedEvent evt)\n    {\n        Debug.Log(\"Great! You jumped. Tutorial step complete.\");\n        // We can now stop the coroutine since the goal was achieved.\n        StopCoroutine(nameof(WaitForJumpAction));\n    }\n}\n\n// A simple event marker for this action\npublic struct PlayerJumpedEvent : IEvent { }\n```\n\n\u003cbr\u003e\n\n## 🎯 Tracked Events: Source \u0026 Target\n\nFor events where you need to know \"who did what to whom\" (e.g., combat, interactions), use `ITrackedEvent`. This interface adds `SourceId` and `TargetId` properties to your event.\nIt extends `IEvent` by adding two properties: `SourceId` and `TargetId`.\n\n### 1. Define a Tracked Event\n```csharp\nusing Echo.Interface;\n\npublic struct DamageDealtEvent : ITrackedEvent\n{\n    // Required by ITrackedEvent\n    public int SourceId { get; set; }\n    public int TargetId { get; set; }\n\n    // Custom data\n    public float DamageAmount;\n}\n```\n\n\u003cbr\u003e\n\n### 2. Publish with Source and Target\nUse the special extension methods to automatically populate the IDs from GameObjects.\n\n```csharp\nusing Echo.Core.Extensions;\nusing UnityEngine;\n\npublic class Weapon : MonoBehaviour\n{\n    public float damage = 25f;\n\n    public void Attack(GameObject target)\n    {\n        new DamageDealtEvent { DamageAmount = damage }\n            .FireFromTo(gameObject, target); // Sets SourceId and TargetId from GameObjects\n    }\n}\n```\n\n### 3. Subscribe with GameObject Helpers\nYou can easily subscribe to events that are sent from or to a specific GameObject.\n\n```csharp\nusing Echo.Core.Extensions;\nusing UnityEngine;\n\npublic class PlayerHealth : MonoBehaviour\n{\n    void OnEnable()\n    {\n        // Subscribe to any damage event where this GameObject is the target\n        gameObject.SubscribeToThis\u003cDamageDealtEvent\u003e(OnDamageReceived);\n    }\n\n    void OnDisable()\n    {\n        // Remember to unsubscribe to prevent memory leaks\n        // Note: GameObject extensions don't have automatic cleanup\n        EventBus.Unsubscribe\u003cDamageDealtEvent\u003e(OnDamageReceived);\n    }\n\n    private void OnDamageReceived(DamageDealtEvent evt)\n    {\n        Debug.Log($\"Took {evt.DamageAmount} damage from entity {evt.SourceId}\");\n        // ... apply damage ...\n    }\n}\n```\n\n\u003cbr\u003e\n\n### How IDs Work: `GameObject.GetInstanceID()`\n\nBy default, Echo's helper extensions use Unity's built-in `GameObject.GetInstanceID()` method to populate the `SourceId` and `TargetId`.\n\n`GetInstanceID()` returns a **unique integer** for every object that inherits from `UnityEngine.Object` (like GameObjects, Components, and Materials). This ID is guaranteed to be unique for the entire session your application is running, making it a fast and convenient way to reference specific object instances without passing direct object references.\n\n\u003cbr\u003e\n\n\u003e [!WARNING]\n\u003e A common source of errors is confusing the ID of a `GameObject` with the ID of one of its `Component`s. When your script inherits from `MonoBehaviour`, `this.GetInstanceID()` returns the unique ID of the **script component instance**, while `this.gameObject.GetInstanceID()` returns the unique ID of the **GameObject** it is attached to. These two IDs will **not** be the same. Be sure to use the correct one for your logic (Echo's helpers typically expect the GameObject's ID).\n\n\u003cbr\u003e\n\n### Managing Complexity: A Note on ID Management\n\nWhile using `GetInstanceID()` is efficient, it has a limitation: the ID is an arbitrary integer. A log stating that \"Entity 1738 dealt damage to Entity 9254\" is not very descriptive for debugging.\n\nFor more complex projects, you will likely want to implement your own system to map these instance IDs to more meaningful entities. Echo intentionally leaves this implementation to you, as every project's needs are different.\n\nA common pattern is to create a central `EntityManager` or `Registry`:\n\n1.  When an important entity (like a player, enemy, or interactive object) is created (`Awake` or `OnEnable`), it registers itself with the manager.\n2.  The manager stores it in a `Dictionary\u003cint, IGameEntity\u003e`, using its `GetInstanceID()` as the key.\n3.  When you receive a tracked event, you can pass the `SourceId` or `TargetId` to your manager to retrieve the actual `GameObject` or a custom entity class.\n\n\u003cbr\u003e\n\n## 🔥 Advanced Subscriptions: Fluent Filtering\n\nCreate highly specific subscriptions with the fluent `Where\u003cT\u003e()` API. Chain conditions with `And()` and `Or()` to build complex logic without cluttering your handler methods.\n\n```csharp\nusing Echo.Core;\nusing Echo.Core.Extensions; // Required for filter extensions\nusing UnityEngine;\n\npublic class SpecialEffectsManager : MonoBehaviour\n{\n    [SerializeField] private GameObject _player;\n\n    void Start()\n    {\n        // Example 1: A complex chain combining AND/OR logic.\n        // Listen for events where (the value is 100 AND the flag is true) OR (the value is over 200).\n        EventBus.Where\u003cGenericEventA\u003e()\n            .And(evt =\u003e evt.SomeValue == 100 \u0026\u0026 evt.SomeFlag == true)\n            .Or(evt =\u003e evt.SomeValue \u003e 200)\n            .Subscribe(HandleComplexCondition);\n\n        // Example 2: Filtering by source/target and specific values.\n        // Listen for events sent FROM the player TO the enemy, where a specific tag matches.\n        EventBus.Where\u003cGenericTrackedEventB\u003e()\n            .FromSource(_player)\n            .ToTarget(_enemy)\n            .WithValue(evt =\u003e evt.Tag, \"Interaction\")\n            .Subscribe(HandlePlayerToEnemyInteraction);\n\n        // Example 3: Using a range and multiple source/target checks.\n        // Listen for events where the source is the player OR another object,\n        // the target is NOT the player, and a float value is within a specific range.\n        EventBus.Where\u003cGenericTrackedEventB\u003e()\n            .And(evt =\u003e evt.SourceId == _player.GetInstanceID() || evt.SourceId == _someOtherObject.GetInstanceID())\n            .And(evt =\u003e evt.TargetId != _player.GetInstanceID())\n            .WithRange(evt =\u003e evt.Amount, 10.5f, 50.0f)\n            .Subscribe(HandleRangedEventFromMultipleSources);\n\n        // Example 4: A temporary subscription with the 'using' block for automatic cleanup.\n        // This listener is active only for the duration of this method. It filters events\n        // that are between two specific objects or have a specific ID.\n        using var tempSubscription = EventBus.Where\u003cGenericTrackedEventB\u003e()\n            .Between(_player, _someOtherObject) // Helper for Source AND Target\n            .Or(evt =\u003e evt.TargetId == _entityId)\n            .SubscribeScoped(HandleTemporaryEvent);\n        \n        Debug.Log(\"Listeners configured. The temporary listener will now be disposed.\");\n    }\n\n    ...\n}\n```\n\n\u003cbr\u003e\n\n## 📦 Performance Tuning: Event Aggregation\n\nFor high-frequency events (like analytics or continuous damage), publishing every single event can be inefficient in some cases. The `EventAggregator` lets you collect events and publish them as a single batch.\n\n### Collecting and Flushing Events\n\nUse the `.Collect()` extension to add an event to a temporary buffer. Then, call `EventAggregator\u003cT\u003e.Flush()` to publish all collected events at once.\n\n```csharp\nusing Echo.Core.Data;\nusing Echo.Core.Extensions;\n\npublic class AnalyticsManager : MonoBehaviour\n{\n    public void TrackPlayerAction(Vector3 position, string action)\n    {\n        // This event is not published immediately. It is collected.\n        new PlayerActionEvent { Position = position, ActionName = action }.Collect();\n    }\n\n    // Call this periodically, or when the scene changes\n    public void SendAnalyticsBatch()\n    {\n        // Publishes all collected PlayerActionEvent instances in one go\n        EventAggregator\u003cPlayerActionEvent\u003e.Flush();\n    }\n}\n```\n\n\u003cbr\u003e\n\nYou can also use `.CollectAndFlush(flushThreshold)` to automatically publish when the buffer reaches a certain size:\n\n```csharp\npublic void TrackHighFrequencyEvent()\n{\n    // Auto-flush when 50 events are collected\n    new AnalyticsEvent().CollectAndFlush(50);\n}\n```\n\n\u003cbr\u003e\n\n\u003e [!NOTE]\n\u003e The **EventAggregator** is a performance tool for **specific scenarios** and should not be treated as a default optimization. Firing an event directly with `.Fire()` is already extremely fast due to the zero-allocation nature of the system.\n\u003e\n\u003e The aggregator introduces its own small overhead by buffering events. This cost is only justified in very high-frequency situations (e.g., hundreds of events per frame from analytics, particle collisions, etc.). In these specific cases, the cost of making many individual calls to the event publishing system can become greater than the cost of buffering.\n\u003e\n\u003e **As a rule of thumb:** use direct publishing (`.Fire()`). Only consider using the aggregator if you have profiled your application and identified a clear bottleneck caused by an exceptionally high volume of events.\n\n\u003cbr\u003e\n\n## 📖 More Features \u0026 API Highlights\n\n### Event Extensions\n\nA rich set of extension methods makes publishing expressive and powerful:\n\n```csharp\n// Fire only if a condition is met\nnew GameOverEvent().FireIf(currentHealth \u003c= 0);\n\n// Schedule an event for the next frame (requires a MonoBehaviour to start the coroutine)\nStartCoroutine(new UiRefreshEvent().FireNextFrame());\n\n// Fire after a 2-second delay\nStartCoroutine(new BombExplodedEvent().FireDelayed(2.0f));\n\n// Transform an event into another type before publishing\nplayerEvent.FireAs(evt =\u003e new UiUpdateEvent { PlayerId = evt.Id });\n\n// Publish an entire array or list of events at once\nDamageEvent[] damageBatch = GetDamageEvents();\ndamageBatch.FireBatch();\n```\n\n\u003cbr\u003e\n\n### Memory Management\n\nThe `EventAggregator` provides tools to manage memory for event buffers:\n\n```csharp\n// Pre-allocate buffer space if you know many events are coming\nEventAggregator\u003cMyEvent\u003e.Reserve(1000);\n\n// Free up unused memory after a batch is flushed\nEventAggregator\u003cMyEvent\u003e.TrimExcess();\n\n// Check the state of the aggregator\nint pending = EventAggregator\u003cMyEvent\u003e.PendingCount;\nint capacity = EventAggregator\u003cMyEvent\u003e.Capacity;\n```\n\n\u003cbr\u003e\n\n## 💡 Best Practices\n\n### Event Design\n\n```csharp\n// ✅ Good: Struct with clear purpose\npublic struct PlayerLevelUpEvent : IEvent\n{\n    public int PlayerId;\n    public int NewLevel;\n    public int OldLevel;\n}\n\n// ❌ Avoid: Reference types\npublic struct PlayerEvent : IEvent\n{\n    public string PlayerName;\n}\n```\n\n\u003cbr\u003e\n\n\u003e [!NOTE]\n\u003e The **memory layout** and **size** of your event `struct` are critical for performance. The layout refers to how a struct's fields are arranged in memory by the C# compiler. By default, the compiler may add \"padding\" bytes between fields to ensure they align with the CPU's natural word size (e.g., aligning a 4-byte `int` on a 4-byte boundary). This can make a struct larger than the sum of its parts.\n\u003e\n\u003e **Why does this matter?** Because events are `struct`s, they are copied by value every time they are published. A larger struct means more data is copied to the stack, which consumes more CPU cycles. In high-frequency scenarios, this can become a noticeable overhead.\n\u003e\n\u003e **Recommendations:**\n\u003e * **Keep structs small and focused.** An event should carry only the essential data required by its listeners.\n\u003e * **Aim for a size under 64 bytes.** This is a common CPU cache line size. Keeping your struct within this limit can improve memory access patterns. You can check a struct's size with `sizeof(MyEventStruct)`.\n\u003e * **Order fields wisely.** For advanced optimization, you can use the `[StructLayout(LayoutKind.Explicit)]` attribute to control the exact memory layout and eliminate padding, but this is often unnecessary. A simpler trick is to declare fields from largest to smallest (e.g., `long`, `int`, `short`, `bool`) to help the compiler minimize padding naturally.\n\n\u003cbr\u003e\n\n### Subscription Management\n\n```csharp\npublic class GameSystem : MonoBehaviour\n{\n    private IDisposable _subscription;\n    \n    void OnEnable()\n    {\n        // ✅ Good: Use scoped subscriptions when possible\n        _subscription = EventBus.SubscribeScoped\u003cGameEvent\u003e(HandleGameEvent);\n    }\n    \n    void OnDisable()\n    {\n        // ✅ Good: Always clean up\n        _subscription?.Dispose();\n    }\n}\n```\n\n\u003cbr\u003e\n\n### Performance Optimization\n\n```csharp\n// ✅ Good: Batch operations when possible\nvar events = new DamageEvent[100];\n// ... populate events ...\nevents.FireBatch();\n\n// ✅ Good: Use aggregation for high-frequency events\nhighFrequencyEvent.CollectAndFlush(50);\n\n// ✅ Good: Reserve capacity for known workloads\nEventAggregator\u003cDamageEvent\u003e.Reserve(1000);\n```\n\n\u003cbr\u003e\n\n## 🤝 Contributing \u0026 Supporting\n\nThis project is open-source under the **MIT License**, and any form of contribution is welcome and greatly appreciated!\n\nIf **Echo** helps you build a cleaner, more performant architecture in your projects, the best way to show your support is by **giving it a star ⭐️ on GitHub!** It helps a lot with visibility and motivates me to continue its development.\n\nHere are other ways you can get involved:\n\n* **💡 Share Ideas \u0026 Report Bugs:** Have a great idea for a new feature or found a potential performance issue? [Open an issue](https://github.com/Alaxxxx/Echo/issues) to share the details.\n* **🔌 Contribute Code:** Feel free to fork the repository and submit a pull request for bug fixes or new features.\n* **🗣️ Spread the Word:** Know other developers passionate about clean code and performance? Let them know about Echo!\n\nEvery contribution is incredibly valuable. Thank you for your support!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falaxxxx%2Fecho","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falaxxxx%2Fecho","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falaxxxx%2Fecho/lists"}