{"id":19512523,"url":"https://github.com/vberlier/goomy","last_synced_at":"2025-04-26T04:31:36.458Z","repository":{"id":89109913,"uuid":"182013354","full_name":"vberlier/goomy","owner":"vberlier","description":"A tiny, experimental ECS framework.","archived":false,"fork":false,"pushed_at":"2019-04-28T03:29:09.000Z","size":265,"stargazers_count":12,"open_issues_count":0,"forks_count":0,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-04T08:01:42.212Z","etag":null,"topics":["cpp17","ecs-framework","experimental","template-metaprogramming"],"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/vberlier.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}},"created_at":"2019-04-18T03:59:27.000Z","updated_at":"2024-09-12T07:06:06.000Z","dependencies_parsed_at":"2023-06-13T23:30:58.282Z","dependency_job_id":null,"html_url":"https://github.com/vberlier/goomy","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vberlier%2Fgoomy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vberlier%2Fgoomy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vberlier%2Fgoomy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vberlier%2Fgoomy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vberlier","download_url":"https://codeload.github.com/vberlier/goomy/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250931026,"owners_count":21509803,"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":["cpp17","ecs-framework","experimental","template-metaprogramming"],"created_at":"2024-11-10T23:26:30.063Z","updated_at":"2025-04-26T04:31:35.818Z","avatar_url":"https://github.com/vberlier.png","language":"C++","readme":"\u003c!-- Go to https://github.com/vberlier/goomy to read this file with formatting --\u003e\n\n# goomy\n\n\u003e A tiny, experimental [ECS framework](https://en.wikipedia.org/wiki/Entity_component_system).\n\n**⚠️ Disclaimer ⚠️**\n\nThis project is a proof-of-concept and is mostly an excuse for me to get familiar with modern C++ and a few experimental features. Don't use this code. _(yet?)_\n\n## Introduction\n\nThe framework is heavily template-based, and effectively almost disappears at compile-time. Components of the same type are stored contiguously in memory, and entities only hold indices linking to their associated components. Entities are stored contiguously in memory as well, but all these lower-level details are packaged behind a lightweight wrapper API.\n\nThe framework also comes with a signaling/event system that makes use of the [detection idiom](https://en.cppreference.com/w/cpp/experimental/is_detected) to facilitate dependency injection.\n\n## What it looks like\n\nThe following example is extremely simple and really only scratches the surface of what's possible, but it should give you a basic idea of how the framework works.\n\n```cpp\n#include \u003cgoomy.hpp\u003e\n\nclass Lifetime;\nclass TestSystem;\n\nusing Engine =\n    goomy::Engine\u003cgoomy::Mount\u003cTestSystem\u003e, goomy::Register\u003cLifetime\u003e\u003e;\n\nclass Lifetime {\n  public:\n    int lifetime;\n    Lifetime(int lifetime) : lifetime(lifetime) {}\n};\n\nclass TestSystem {\n  public:\n    void onInit(Engine \u0026engine) {\n        for (int i = 0; i \u003c 1000; ++i) {\n            engine.entity().with\u003cLifetime\u003e(i);\n        }\n    }\n    void onUpdate(Engine \u0026engine) {\n        if (engine.getEntityCount() == 0) {\n            engine.shutdown();\n        }\n    }\n    void onUpdate(goomy::Component\u003cEngine, Lifetime\u003e component) {\n        if (--component.data().lifetime \u003c= 0) {\n            component.entity().destroy();\n        }\n    }\n};\n\nint main() {\n    Engine().loop();\n    return 0;\n}\n```\n\n\u003e You can find this example in the [`examples/basic`](https://github.com/vberlier/goomy/tree/master/examples/basic) directory.\n\n## Running the examples\n\nThe project has only been tested on Linux but doesn't rely on any platform-specific library so you should be good as long as you have a compliant C++17 compiler on hand. One of the examples demonstrates how the framework could be used in combination with [SFML](https://www.sfml-dev.org/), so you'll need to make sure that the latest stable version is available on your system.\n\n1. Clone this repository\n\n    ``` sh\n    $ git clone https://github.com/vberlier/goomy.git\n    $ cd goomy\n    ```\n\n2. Build the project\n\n    ``` sh\n    $ mkdir cmake-build-release\n    $ cd cmake-build-release\n    $ cmake -DCMAKE_BUILD_TYPE=Release ..\n    $ make\n    ```\n\n3. Run the examples\n\n    ``` sh\n    $ ./examples/basic/example_basic\n    $ ./examples/sfml/example_sfml\n    ```\n\n### The SFML example\n\n\u003cp align=\"center\"\u003e\n  \u003cimg width=\"49%\" src=\"https://raw.githubusercontent.com/vberlier/goomy/master/examples/sfml/screenshots/1.png\"\u003e\n  \u003cimg width=\"49%\" src=\"https://raw.githubusercontent.com/vberlier/goomy/master/examples/sfml/screenshots/2.png\"\u003e\n\u003c/p\u003e\n\nThis example demonstrates how the framework could be used in combination with [SFML](https://www.sfml-dev.org/). The executable creates a window in which you can spawn particles in a few different ways:\n\n- Clicking and dragging the left mouse button anywhere in the window creates a stream of particles (a particle spawns every frame)\n- Dragging the right mouse button and releasing it lets you shoot a single particle in a particular direction\n- Pressing the left mouse button while aiming with the right mouse button reactivates the particle stream\n\n\u003e The code for this example is in the [`examples/sfml`](https://github.com/vberlier/goomy/tree/master/examples/sfml) directory.\n\n## Usage\n\nGetting started with `goomy` is fairly straightforward. You pretty much only need to instantiate the `goomy::Engine` template and declare your systems and components.\n\n```cpp\nusing Engine = goomy::Engine\u003cgoomy::Mount\u003cSystem1, System2, ...\u003e,\n                             goomy::Register\u003cComponent1, Component2, ...\u003e\u003e;\n```\n\nInstantiating the template effectively generates an entity type, creates registries for your different components and wires all the systems and signals together. Everything then happens through your specialized `Engine` type.\n\n\u003e You will probably need to forward-declare your system classes because you'll often want to reference your custom `Engine` in their declarations.\n\nAfter declaring your custom engine type, the only thing you need to do is to instantiate it and call the `loop()` member function.\n\n```cpp\nint main() {\n    Engine().loop();\n    return 0;\n}\n```\n\n### Systems\n\nThe framework lets you mount any class as a system, there are no requirements, no specific base class to inherit from. For instance, this makes it possible to mount an [SFML window](https://www.sfml-dev.org/tutorials/2.5/window-window.php) to your engine (see: [`examples/sfml`](https://github.com/vberlier/goomy/tree/master/examples/sfml)). There are really no limitations to what can be a system.\n\n```cpp\nclass TestSystem {};\n```\n\nAfter mounting the system to the engine, the framework will take care of instantiating it.\n\n```cpp\ngoomy::Mount\u003cTestSystem\u003e\n```\n\n### Signals\n\nThe distinctive capability of classes that have been mounted to the engine as systems is that they can respond to signals. Dispatching a signal will call the associated member function on all the systems where it is defined.\n\n```cpp\nclass TestSystem {\n  public:\n    void onInit();\n    void onUpdate();\n};\n```\n\nThe `onInit` signal is dispatched by the engine before entering the main loop. Systems that define the `onInit()` member function will be able to run code on initialization.\n\nSimilarly, the `onUpdate` signal is also dispatched by the engine. It will call the `onUpdate()` member function every frame on all the systems where it is defined.\n\n\u003e The engine also dispatches `onBeforeInit` and `onAfterInit` as well as `onBeforeUpdate` and `onAfterUpdate`.\n\nMember functions called by signals support some form of dependency injection. You can get a reference to the engine instance by specifying it as a parameter.\n\n```cpp\nclass TestSystem {\n  public:\n    void onInit(Engine \u0026engine) {\n        engine.shutdown();\n    }\n};\n```\n\nIt's possible to create your own signals with the `GOOMY_SIGNAL(NAME)` macro.\n\n```cpp\nGOOMY_SIGNAL(onCustomEvent);\n```\n\nYou don't have to use the macro but declaring a custom signal manually can get a little verbose.\n\n```cpp\nstruct onCustomEvent {\n    template \u003ctypename T, typename... Args\u003e\n    using detector =\n        decltype(std::declval\u003cT\u003e().onCustomEvent(std::declval\u003cArgs\u003e()...));\n\n    template \u003ctypename T, typename... Args\u003e\n    static void invoke(T \u0026instance, Args \u0026\u0026... args) {\n        instance.onCustomEvent(std::forward\u003cArgs\u003e(args)...);\n    }\n};\n```\n\nThe macro does make things a little more readable, but any struct with a templated `detector` type and a static function template `invoke()` can be used as a signal.\n\n```cpp\nvoid TestSystem::onUpdate(Engine \u0026engine) {\n    engine.dispatch\u003conCustomEvent\u003e();\n}\n```\n\nThe `engine.dispatch\u003cSignalType\u003e()` function lets you pass parameters to the systems handling the signal. In that case, the framework will only call the associated member functions if the signature is compatible with the types of the parameters.\n\n```cpp\nvoid TestSystem::onUpdate(Engine \u0026engine) {\n    engine.dispatch\u003conCustomEvent\u003e(42);\n}\n\nvoid OtherSystem::onCustomEvent(int number) {}\n\n// Injecting the engine still works\nvoid OtherSystem::onCustomEvent(Engine \u0026engine, int number) {}\n\n// Can omit the parameters if the function doesn't need them\nvoid OtherSystem::onCustomEvent() {}\n```\n\nHowever, if the types of the parameters are not compatible, the function will not get called.\n\n```cpp\nvoid TestSystem::onUpdate(Engine \u0026engine) {\n    engine.dispatch\u003conCustomEvent\u003e(\"will not call OtherSystem::onCustomEvent\");\n}\n\nvoid OtherSystem::onCustomEvent(int number) {}\n```\n\n---\n\n**💡 Tip**\n\nIt's important to understand that signals don't have any runtime footprint. Every call to the `engine.dispatch\u003cSignalType\u003e()` function effectively results in inlined calls to the corresponding member functions on systems with compatible definitions.\n\n---\n\n### Components\n\nJust like with systems, there are no restrictions when it comes to what kind of class can be a component.\n\n```cpp\nclass Dummy {};\n```\n\nComponents must be registered when creating your custom `Engine` type.\n\n```cpp\ngoomy::Register\u003cDummy\u003e;\n```\n\nThe engine instance allows you to create and destroy entities and components, and iterate through them. You can check out the [API reference](https://github.com/vberlier/goomy#api-reference) for more information.\n\n```cpp\nclass Dummy {\n  public:\n    int number;\n    Dummy(int number) : number(number) {}\n};\n\nvoid TestSystem::onUpdate(Engine \u0026engine) {\n    engine.entity().with\u003cDummy\u003e(42);\n}\n\nvoid OtherSystem::onUpdate(Engine \u0026engine) {\n    for (auto component : engine.components\u003cDummy\u003e()) {\n        component.data().number++;\n    }\n}\n```\n\nSince iterating over all the components of a given type is a very common operation, the framework makes it possible to lift the iteration in the dependency injection machinery and ask for a component reference to be injected in any member function called by a signal.\n\n```cpp\nvoid OtherSystem::onUpdate(Dummy \u0026dummy) {\n    dummy.number++;\n}\n```\n\nHere, the `onUpdate()` function will be called each frame for every single `Dummy` component. This particular example shows that you can ask for the component reference directly, but it's also possible to request a component reference wrapper instead when necessary (more details in the [component API reference](https://github.com/vberlier/goomy#component)).\n\n```cpp\nvoid OtherSystem::onUpdate(goomy::Component\u003cEngine, Dummy\u003e component) {\n    if (++component.data().number \u003e 9000) {\n        component.entity().destroy();\n    }\n}\n```\n\nNote that component reference injection is compatible with engine injection and custom parameters.\n\n```cpp\nvoid TestSystem::onUpdate(Engine \u0026engine) {\n    engine.dispatch\u003conCustomEvent\u003e(42);\n}\n\n// All of these will get called for each Dummy component\nvoid OtherSystem::onCustomEvent(Dummy \u0026dummy, int number) {}\nvoid OtherSystem::onCustomEvent(Engine \u0026engine, Dummy \u0026dummy, int number) {}\nvoid OtherSystem::onCustomEvent(Dummy \u0026dummy) {}\n```\n\n## API reference\n\n### Engine\n\nThe engine instance lets you interact with the systems, entities, and components you declared when creating your custom `Engine` type. You can use it to get a reference to any of your custom systems, dispatch signals, iterate through entities and components and more. Note that the engine is explicitly marked as non-copiable, and cannot be moved.\n\n#### `engine.get\u003cSystemType\u003e()`\n\nInstantiating a custom `Engine` type creates instances of all the mounted system types. You can get a reference to any of your system instances by calling the templated `get\u003cSystemType\u003e()` member function.\n\n```cpp\nauto \u0026system1 = engine.get\u003cSystem1\u003e();\n```\n\nNote that the returned reference is a very thin wrapper that simply marks the system as non-copiable to prevent any accident.\n\n#### `engine.dispatch\u003cSignalType\u003e(Args \u0026\u0026... args)`\n\nThis function lets you dispatch signals throughout your systems. The template parameter of the `dispatch\u003cSignalType\u003e()` member function must be a signal class/struct, usually generated with the `GOOMY_SIGNAL(NAME)` macro.\n\n```cpp\nGOOMY_SIGNAL(onCustomEvent);\n\nengine.dispatch\u003conCustomEvent\u003e(arg1, arg2, ...);\n```\n\nThis code will essentially go through every system instance at compile-time and generate a call to the `onCustomEvent()` member function if it is defined.\n\n#### `engine.entity()`\n\nYou can create entities with the `entity()` member function. This function returns a lightweight wrapper around a reference to the newly created internal entity.\n\n```cpp\nauto entity = engine.entity();\n```\n\nFor more details check out the [entity API reference](https://github.com/vberlier/goomy#entity).\n\n#### `engine.entity(std::size_t id)`\n\nThis function lets you retrieve an entity by its id.\n\n```cpp\nauto entity = engine.entity(42);\n```\n\n#### `engine.entities()`\n\nYou can iterate over every entity by using the `entities()` member function.\n\n```cpp\nfor (auto entity : engine.entities()) {\n    // ...\n}\n```\n\n#### `engine.getEntityCount()`\n\nReturns the current number of entities. The function doesn't take into account entities that have been created or destroyed in the current frame.\n\n```cpp\nauto count = engine.getEntityCount();\n```\n\n#### `engine.components\u003cComponentType\u003e()`\n\nThis lets you iterate over all the components of a specific type.\n\n```cpp\nfor (auto component1 : engine.components\u003cComponent1\u003e()) {\n    // ...\n}\n```\n\nThe iterator doesn't yield references to the internal components directly, but lightweight wrappers that provide a few useful member functions to let you access the id of the component or its associated entity for instance.\n\nFor more details check out the [component API reference](https://github.com/vberlier/goomy#component).\n\n#### `engine.loop()`\n\nThis function is usually called in your program's `main()`. It runs the main execution loop and will block until the engine gets shut down.\n\n```cpp\nengine.loop();\n```\n\n#### `engine.shutdown()`\n\nYou can stop the main execution loop by calling the `shutdown()` member function. The engine will cleanly finish executing the current frame and exit.\n\n```cpp\nengine.shutdown();\n```\n\n#### `engine.deltaTime()`\n\nThis returns the number of seconds that it took for executing the frame.\n\n```cpp\nposition += velocity * engine.deltaTime();\n```\n\n### Entity\n\nThe framework lets you interact with the internal entities through lightweight reference wrappers.\n\nEntities are internally all stored contiguously in memory, in a standard `std::vector`. The engine guarantees that the vector holding the entities internally will not be mutated during the frame by buffering entity creation and deletion in separate containers, which in turn means that entity reference wrappers are valid for the entire frame.\n\nHowever, the engine flushes the pending modifications at the end of each frame, which potentially mutates the internal vector and invalidates references and reference wrappers. Unless you know what you're doing, the lifetime of any entity reference wrapper should not exceed the frame it was created in.\n\n#### `entity.id()`\n\nReturns the id of the entity. The id can be used to retrieve the entity from the engine instance with the `engine.entity(id)` member function.\n\n```cpp\nauto id = entity.id();\n```\n\nNote that this id can change over time when other entities are destroyed.\n\n#### `entity.engine()`\n\nReturns a reference to the engine instance.\n\n```cpp\nauto \u0026engine = entity.engine();\n```\n\n#### `entity.has\u003cComponentType\u003e()`\n\nReturns whether the entity has a component of the given type.\n\n```cpp\nbool hasComponent1 = entity.has\u003cComponent1\u003e();\n```\n\n#### `entity.get\u003cComponentType\u003e()`\n\nThis function returns a reference wrapper to the component of the given type attached to the entity.\n\n```cpp\nauto component1 = entity.get\u003cComponent1\u003e();\n```\n\nThis function should only be called if the component actually exists, so make sure to check for the component with the `has\u003cComponentType\u003e()` member function where appropriate.\n\n#### `entity.create\u003cComponentType\u003e(Args \u0026\u0026... args)`\n\nCreate and attach a new component of the given type to the entity. The arguments are passed to the constructor of the component. The function returns a component reference wrapper.\n\n```cpp\nauto component1 = entity.create\u003cComponent1\u003e(arg1, arg2, ...);\n```\n\nNote that if a component of the given type is already attached to the entity the new component will take its place and the current one will be destroyed.\n\n#### `entity.with\u003cComponentType\u003e(Args \u0026\u0026... args)`\n\nThis function is very similar to the `entity.create\u003cComponentType\u003e()` member function, but returns the entity itself instead of a reference wrapper to the newly created component.\n\n```cpp\nentity\n    .with\u003cComponent1\u003e(arg1, arg2, ...)\n    .with\u003cComponent2\u003e(arg1, arg2, ...)\n    .with\u003cComponent3\u003e(arg1, arg2, ...);\n```\n\nThe fluent API makes it easy to create multiple components at the same time.\n\n#### `entity.destroy()`\n\nDestroys the entity. This will also destroy all the components associated with the entity.\n\n```cpp\nentity.destroy();\n```\n\n### Component\n\nThe framework lets you interact with the internal component instances through lightweight reference wrappers.\n\nInstances of each type of component are internally stored contiguously in memory in separate standard `std::vector`, in a very similar way to how entities are stored internally as well. The engine guarantees that the vectors holding the components internally will not be mutated during the frame by buffering component creation and deletion in separate containers, which in turn means that component reference wrappers are valid for the entire frame.\n\nHowever, the engine flushes the pending modifications at the end of each frame, which potentially mutates the internal vectors and invalidates references and reference wrappers. Just like with entity reference wrappers, unless you know what you're doing, the lifetime of any component reference wrapper should not exceed the frame it was created in.\n\n#### `component.id()`\n\nReturns the id of the component. Note that this id is only unique for components of the same type, and can change over time when other components of the same type are destroyed.\n\n```cpp\nauto id = component.id();\n```\n\n#### `component.engine()`\n\nReturns a reference to the engine instance.\n\n```cpp\nauto \u0026engine = component.engine();\n```\n\n#### `component.entity()`\n\nReturns a reference wrapper to the associated entity.\n\n```cpp\nauto entity = component.entity();\n```\n\n#### `component.data()`\n\nThis function returns a reference to the declared component instance.\n\n```cpp\nauto \u0026data = component.data();\n```\n\n#### `component.destroy()`\n\nDestroys the component.\n\n```cpp\ncomponent.destroy();\n```\n\n## Credits\n\nThis project is partly inspired by a talk by Allan Deutsch:\n\n- [Game Engine API Design](https://www.youtube.com/watch?v=W3ViIBnTTKA)\n\nOther useful resources:\n\n- [Cpu Caches and Why You Care (Scott Meyers)](https://www.youtube.com/watch?v=WDIkqP4JbkE)\n- [Modern Template Metaprogramming: A Compendium, Part I \u0026 II (Walter E. Brown)](https://www.youtube.com/watch?v=Am2is2QCvxY)\n- [Universal References in C++11 (Scott Meyers)](https://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Scott-Meyers-Universal-References-in-Cpp11)\n- [Data-Oriented Design and C++ (Mike Acton)](https://www.youtube.com/watch?v=rX0ItVEVjHc)\n- [The Detection Idiom: A simpler way to SFINAE (Marshall Clow)](https://www.youtube.com/watch?v=o1ekBpEFcPc)\n\n---\n\nLicense - [MIT](https://github.com/vberlier/goomy/blob/master/LICENSE)\n","funding_links":[],"categories":["[ECS Libraries](#contents)","ECS Libraries"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvberlier%2Fgoomy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvberlier%2Fgoomy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvberlier%2Fgoomy/lists"}