{"id":28712377,"url":"https://github.com/adam4lexander/spoke","last_synced_at":"2025-12-27T04:19:13.030Z","repository":{"id":292181387,"uuid":"978674880","full_name":"Adam4lexander/Spoke","owner":"Adam4lexander","description":"Spoke is a tiny declarative reactivity engine for Unity.","archived":false,"fork":false,"pushed_at":"2025-06-08T01:16:00.000Z","size":211,"stargazers_count":21,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-08T02:40:00.245Z","etag":null,"topics":["architecture","csharp","declarative","ecs","game-dev","gameplay","open-source","reactive","reactivity","simulation","state-management","unity","unity3d","vr"],"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/Adam4lexander.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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}},"created_at":"2025-05-06T10:50:09.000Z","updated_at":"2025-06-08T01:16:03.000Z","dependencies_parsed_at":"2025-05-31T07:08:03.184Z","dependency_job_id":"7d4484d8-1749-4809-b83c-d204f03772f7","html_url":"https://github.com/Adam4lexander/Spoke","commit_stats":null,"previous_names":["adam4lexander/spoke"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/Adam4lexander/Spoke","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adam4lexander%2FSpoke","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adam4lexander%2FSpoke/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adam4lexander%2FSpoke/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adam4lexander%2FSpoke/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Adam4lexander","download_url":"https://codeload.github.com/Adam4lexander/Spoke/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adam4lexander%2FSpoke/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259896238,"owners_count":22928331,"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","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":["architecture","csharp","declarative","ecs","game-dev","gameplay","open-source","reactive","reactivity","simulation","state-management","unity","unity3d","vr"],"created_at":"2025-06-14T23:05:43.338Z","updated_at":"2025-12-27T04:19:13.023Z","avatar_url":"https://github.com/Adam4lexander.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🔘 Spoke - _A reactive framework for simulated worlds_\n\n**Spoke** is a tiny reactivity engine for **C#** and **Unity**. \u003cbr\u003e\nIt’s tree-shaped and declarative, with imperative-ordered execution, and built to tame the chaos when many systems interact in dynamic, emergent ways.\n\n- Start and stop behaviours at the right time, keeping them in sync with runtime state.\n- Manage deeply nested logic while keeping it cohesive and clear.\n\nInspired by React, Spoke adds strict guarantees on execution order, making it suitable for game logic. It makes indirect, entangled logic feel imperative, with **automatic lifecycle management** and **self-cleaning reactivity.**\n\nNo flag-checking. No brittle events. No manual cleanup.\u003cbr\u003e\nJust _stateful blocks of logic_, expressed as a tree, that mount and unmount on their own.\n\n- ✨ **Control complexity** — write clear, reactive gameplay logic\n- 🧪 **Use anywhere** — adopt in one script, one system, or your whole project\n\n---\n\n## ⚡ Example\n\n_Spawn a HUD over the nearest enemy_\n\n### 🟧 Vanilla Unity:\n\n```csharp\nGameObject currHUD;\n\nvoid Awake() {\n    OnNearestEnemyChanged.AddEventListener(NearestEnemyChangedHandler);\n}\n\nvoid OnDestroy() {\n    OnNearestEnemyChanged.RemoveEventListener(NearestEnemyChangedHandler);\n    if (currHUD != null) Destroy(currHUD);\n}\n\nvoid NearestEnemyChangedHandler(GameObject enemy) {\n    if (currHUD != null) Destroy(currHUD);\n    if (enemy != null) currHUD = SpawnHUD(enemy);\n}\n```\n\n### 🟦 Spoke:\n\n```csharp\nvoid Init(EffectBuilder s) {\n    if (s.D(NearestEnemy) == null) return;\n    var hud = SpawnHUD(NearestEnemy.Now);\n    s.OnCleanup(() =\u003e Destroy(hud));\n}\n```\n\n👉 In Spoke, the entire behaviour lives in one expressive block. Setup, reaction, and cleanup happen automatically.\n\n---\n\n## 💡 Why Spoke?\n\nUnity’s lifecycle makes it easy for logic to get scattered:\n\n- Systems spread across `Awake`, `OnEnable`, `OnDisable`, `OnDestroy`\n- Polling state in `Update` just to detect changes\n- Brittle event chains with manual subscription cleanup\n- Initialization order bugs between dependent components\n- Scene teardown chaos: accessing destroyed objects\n\nSpoke collapses those problems into **scoped, self-cleaning windows of logic.**\u003cbr\u003e\nYou write: _\"When this state exists — run this behaviour — and clean it up afterward.”_\n\n---\n\n## 🔰 Install\n\nClone this repo or copy **Spoke.Runtime**, **Spoke.Reactive** and **Spoke.Unity** into your project.\u003cbr\u003e\nNo dependencies, no setup.\n\n---\n\n## 🚀 Getting Started\n\nSubclass `SpokeBehaviour` instead of `MonoBehaviour`:\n\n```csharp\nusing Spoke;\n\npublic class MyBehaviour : SpokeBehaviour {\n\n    // Replaces Awake, OnEnable, Start, OnDisable, OnDestroy\n    protected override void Init(EffectBuilder s) {\n\n        // Awake logic here\n\n        s.OnCleanup(() =\u003e {\n            // OnDestroy logic here\n        });\n\n        s.Phase(IsEnabled, s =\u003e {\n            // OnEnable logic here\n            s.OnCleanup(() =\u003e {\n                // OnDisable logic here\n            });\n        });\n\n        s.Phase(IsStarted, s =\u003e {\n            // Start logic here\n        });\n    }\n}\n```\n\n[Or spawn a SpokeTree in your own scripts.](./Docs/Core/07_SpokeTree.md#usage-with-spokebehaviour)\n\n[Read the Quickstart →](./Docs/Core/01_QuickStart.md)\n\n---\n\n## 🧠 Core Concepts\n\nThe reactive model behind Spoke is built around a few simple primitives:\n\n- **Trigger** - fire-and-forget events\n- **State** - reactive container for values\n- **Effect** / **Phase** / **Reaction** - self-cleaning blocks of logic\n- **Memo** - computed reactive value\n- **Dock** - dynamic reactive container\n\n---\n\n## 🤔 \"Spoke-style\" reactivity?\n\nSpoke shares DNA with frameworks like **React** and **SolidJS**\u003cbr\u003e\nInstead of managing a DOM tree, you're sculpting **simulation logic:** behaviour trees, stateful systems, emergent gameplay.\n\nThese frameworks transformed how we write UI.\u003cbr\u003e\nSpoke applies the same principles to **gameplay logic.**\n\n---\n\n## 🎮 Origins\n\nSpoke was born out of necessity while building my VR mech game, **Power Grip Dragoons**. The game has brutal demands for dynamic, event-driven logic on Meta Quest hardware. Over 6 years I refined this architecture until it became the foundation I now use everywhere. Spoke is the result.\n\n---\n\n## 🔍 Real-World Patterns\n\n### Scattered Resource Management\n\nManaging disposables in Unity usually means spreading logic across lifecycle methods.\n\n```cs\n// --- MonoBehaviour\npublic class MyBehaviour : MonoBehaviour {\n\n    IDisposable myResource;\n\n    void OnEnable() {\n        myResource = new SomeCustomResource();\n    }\n\n    void OnDisable() {\n        myResource.Dispose();\n    }\n}\n\n// --- Spoke\npublic class MySpokeBehaviour : SpokeBehaviour {\n\n    protected override void Init(EffectBuilder s) {\n        s.Phase(IsEnabled, s =\u003e {\n            s.Use(new SomeCustomResource());\n        });\n    }\n}\n```\n\nIn Spoke, resource allocation and cleanup collapse into one scoped block. No more lifecycle bugs scattered across methods.\n\n---\n\n### Chained Event Subscriptions\n\nNested event subscriptions (`EnemyDetected → EnemyDestroyed`) get messy fast.\n\n```cs\n// When an enemy is detected on radar, and it becomes destroyed. Then the\n// cockpit voice (BitchinBetty) should speak the phrase: \"Enemy Destroyed\".\n\n// --- MonoBehaviour\npublic class MyBehaviour : MonoBehaviour {\n\n    public UnityEvent\u003cRadarBlip\u003e EnemyDetected;\n    public UnityEvent\u003cRadarBlip\u003e EnemyLost;\n\n    void Awake() {\n        EnemyDetected.AddListener(HandleEnemyDetected);\n        EnemyLost.AddListener(HandleEnemyLost);\n    }\n\n    void OnDestroy() {\n        EnemyDetected.RemoveListener(HandleEnemyDetected);\n        EnemyLost.RemoveListener(HandleEnemyLost);\n    }\n\n    void HandleEnemyDetected(RadarBlip enemy) {\n        enemy.OnDestroyed.AddListener(HandleEnemyDestroyed);\n    }\n\n    void HandleEnemyLost(RadarBlip enemy) {\n        enemy.OnDestroyed.RemoveListener(HandleEnemyDestroyed);\n    }\n\n    void HandleEnemyDestroyed() {\n        BitchinBetty.SpeakEnemyDestroyed();\n    }\n}\n\n// --- Spoke\npublic class MySpokeBehaviour : SpokeBehaviour {\n\n    public UnityEvent\u003cRadarBlip\u003e EnemyDetected;\n    public UnityEvent\u003cRadarBlip\u003e EnemyLost;\n\n    protected override void Init(EffectBuilder s) {\n        var dock = s.Dock();\n        s.Subscribe(EnemyDetected, enemy =\u003e dock.Effect(enemy, s =\u003e {\n            s.Subscribe(enemy.OnDestroyed, BitchinBetty.SpeakEnemyDestroyed);\n        }));\n        s.Subscribe(EnemyLost, enemy =\u003e dock.Drop(enemy));\n    }\n}\n```\n\nIn Spoke, the entire subscription chain lives in one cohesive block. Setup and teardown are automatic. No missed unsubscribes.\n\n---\n\nBoth patterns are really the same thing, they're lifecycle windows. With Spoke, you declare what happens in a window, how windows nest, and how to clean up when they end.\n\n---\n\n## 📘 Documentation\n\n[Read the full documentation →](./Docs/)\n\n---\n\n## 🔬 Performance\n\n[See performance notes →](./Docs/Topics/Performance.md)\n\n---\n\n## 🧰 Requirements\n\n- Unity 2021.3 or later (For Examples)\n- No packages, no dependencies\n\n---\n\n## 📜 License\n\nMIT — free to use in personal or commercial projects.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadam4lexander%2Fspoke","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadam4lexander%2Fspoke","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadam4lexander%2Fspoke/lists"}