{"id":28717418,"url":"https://github.com/scurrond/necs","last_synced_at":"2025-06-15T03:15:39.994Z","repository":{"id":289452433,"uuid":"971294465","full_name":"scurrond/necs","owner":"scurrond","description":"One-Header C++ ECS","archived":false,"fork":false,"pushed_at":"2025-05-21T18:47:41.000Z","size":277,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-21T19:50:07.420Z","etag":null,"topics":["cpp20","ecs","entity-component-system","game-development","gamedev","library"],"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/scurrond.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}},"created_at":"2025-04-23T09:55:21.000Z","updated_at":"2025-04-30T09:19:40.000Z","dependencies_parsed_at":"2025-04-23T11:24:32.997Z","dependency_job_id":"63174e81-0f90-4154-b088-271611258ac0","html_url":"https://github.com/scurrond/necs","commit_stats":null,"previous_names":["scurrond/necs"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/scurrond/necs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scurrond%2Fnecs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scurrond%2Fnecs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scurrond%2Fnecs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scurrond%2Fnecs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/scurrond","download_url":"https://codeload.github.com/scurrond/necs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scurrond%2Fnecs/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259914924,"owners_count":22931334,"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":["cpp20","ecs","entity-component-system","game-development","gamedev","library"],"created_at":"2025-06-15T03:15:39.115Z","updated_at":"2025-06-15T03:15:39.934Z","avatar_url":"https://github.com/scurrond.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Overview\n\n**NECS** (Nano ECS) is a single-header Entity-Component-System (ECS) library being developed for C++20. It is designed to be simple, small and easy to use.\n\n## Contents\n\n### encs.hpp\n\nLibrary header file.\n\n### extra/model.hpp\n\nData models for testing and benchmarking.\n\n### extra/benchmarks\n\n- Main benchmarking setup. All benchmarks are done on an archetype with 3 components that are being updated with arbitrary data, iterated a 1000 times and then have averages calculated for them. Some benchmarks are missing for snooze, kill and wake operations for updates and executes.\n- Build file for benchmarks. Currently only available for windows. You must include an entity count as a console argument when running it.\n- Result text files for different operations per entity count.\n\nNOTE:\nCurrently there is a query overhead for very low entity counts (under 100) of a few 100ns. Queries seem to start performing well at 100+, and well outpacing the for_each iteration method at 1000+ entities. See the relevant files for results.\n\n### extra/tests\n\nTest setup \u0026 build file. Not finished.\n\n### extra/examples\n\nExample files of different setups. Not finished.\n\n## Foreword\n\nHello \u0026 thank you for checking out my repo! Let me know about any concerns or bugs regarding my code, I appreaciate any feedback and criticism you have.\n\nIf you want to give my library a go, keep in mind that this is a personal project serving my own development and needs as a programmer first, and that the project will change frequently as I expand my knowledge pool.\n\nThe workflow that NECS supports relies heavily on knowing exactly what the structure of your data is (no arbitrary archetype-switching or runtime types), which is how I prefer to approach design. A built-in entity hierarchy system is coming eventually to enable dynamic structures.\n\nCurrently, NECS supports smaller projects and single-threaded use only. Read: I haven't tested it enough yet.\n\n## Features\n\n- The library is a single header and can be dropped into any project.\n- It uses the standard library only.\n- Entity data is structured inside tuples and vectors, using an SoA approach \u0026 allowing for fast queries. This also allows for compile-time filtering with some template magic.\n- Sleep system: entities are associated with a state and, in addition to their archetype storage, are located in one of two pools: living or sleeping. These are iterated through separately.\n- Dead entities are not considered during iterations, but their ids (indices) are reused (an id-lock system is coming eventually that will allow for ids to be lifetime-unique).\n- Pool memory only grows by default, with dead entities being swapped to the end to allow for reuse. Pools can be manually trimmed by using trim() to remove dead memory.\n- User-defined events + several built-in ones to track changes to data.\n\n# Getting started\n\n## Requirements\n\n- C++20\n\n## Installation\n\nJust drop the `necs.hpp` file into your include path and you are good to go!\n\n```cpp\n#include \"necs.hpp\"\n```\n\n## Example setup\n\n```cpp\n#pragma once\n\n#include \"../../necs.hpp\"\n\nusing namespace NECS;\n\n// ----------------------------------------------------------------------------\n// Components\n// ----------------------------------------------------------------------------\n\nstruct Name\n{\n    std::string value;\n};\n\nstruct Position\n{\n    size_t x;\n    size_t y;\n};\n\nstruct Health\n{\n    int value;\n};\n\n// ----------------------------------------------------------------------------\n// Archetypes\n// ----------------------------------------------------------------------------\n\nusing Monster = Data\u003cPosition, Name, Health\u003e;\n\n// ----------------------------------------------------------------------------\n// Events\n// ----------------------------------------------------------------------------\n\nstruct QuitEvent {};\n\n// ----------------------------------------------------------------------------\n// Registry data\n// ----------------------------------------------------------------------------\n\nusing Archetypes = Data\u003cMonster\u003e;\nusing Events = Data\u003cQuitEvent\u003e;\nusing Singletons = Data\u003c\u003e;\n\nRegistry\u003cArchetypes, Events, Singletons\u003e registry;\n\nint main()\n{\n    registry.populate(Monster(), 100);\n\n    for (auto [id, data] : registry.query_in\u003cMonster, Name\u003e())\n    {\n        auto\u0026 [name] = data;\n\n        name.value = \"New name\";\n    }\n\n    return 0;\n}\n```\n\n## Creating\n\n```cpp\nvoid create()\n{\n    // Adds a single monster to the system\n    registry.create(Monster());\n\n    // Adds 100 monsters to the system, calls create under the hood\n    registry.populate(Monster(), 100);\n}\n```\n\n## Events\n\n```cpp\nvoid events()\n{\n    // Subscribe to built in component events\n    registry.subscribe\u003cDataUpdated\u003cName\u003e\u003e\n    ([](DataUpdated\u003cName\u003e event){\n        // do stuff\n    });\n\n    // Subscribe to built in archetype events\n    registry.subscribe\u003cDataUpdated\u003cMonster\u003e\u003e\n    ([](DataUpdated\u003cMonster\u003e event){\n        // do stuff\n    });\n\n    // Subscribe to built in entity events\n    registry.subscribe\u003cEntityCreated\u003e\n    ([](EntityCreated event){\n        // do stuff\n    });\n\n    // Subscribe to custom events\n    registry.subscribe\u003cQuitEvent\u003e\n    ([](QuitEvent event){\n        // do stuff\n    });\n\n    // Call events\n    registry.call(QuitEvent{});\n\n    // Disable event listener\n    registry.close\u003cQuitEvent\u003e();\n\n    // Enable event listener\n    registry.open\u003cQuitEvent\u003e();\n}\n```\n\n## Checking\n\n```cpp\nvoid check()\n{\n    // read-only location info reference for entity with id 0\n    auto\u0026 [type, index, state, id_lock] = registry.info(0);\n\n    // are there any entities of this archetype in living\n    registry.is_empty\u003cMonster\u003e();\n\n    // are there any entities of this archetype in sleeping\n    registry.is_empty\u003cMonster\u003e(true);\n\n    // is the entity with id 0 a monster\n    registry.is_type\u003cMonster\u003e(0);\n\n    // is the entity with id 0 dead\n    registry.is_state(0, DEAD);\n\n    // can the entity's id be reused on death\n    registry.is_locked(0);\n\n    // does the entity have a name component\n    registry.has_component\u003cName\u003e(0);\n}\n```\n\n## State management\n\n```cpp\nvoid manage()\n{\n    // Changes data after update is called\n    registry.queue(0, KILL);\n    registry.queue(1, SNOOZE);\n    registry.update();\n\n    // Changes data instantly\n    registry.execute(1, WAKE);\n    registry.execute(2, KILL);\n    registry.execute(3, SNOOZE);\n}\n```\n\n## Single access\n\n```cpp\nvoid access()\n{\n    // VIEW returns nullopt if entity is dead or the type is incorrect\n    auto [name0] = registry.view\u003cMonster, Name\u003e(0).value();\n\n    // GET panics if the type is incorrect or the entity is DEAD\n    auto [name3] =  registry.get\u003cMonster, Name\u003e(3);\n\n    // FIND filters and iterates over every archetype, returns a view\n    auto [name1] = registry.find\u003cName\u003e(1).value();\n}\n```\n\n## Query\n\n```cpp\nvoid query()\n{\n    // Query in a single archetype\n    auto iterator = registry.query_in\u003cMonster, Name, Position\u003e();\n\n    for (auto [id, data] : iterator)\n    {\n        auto\u0026 [name, position] = data;\n\n        name.value = \"New name\";\n    }\n\n    // Query with different requirements\n    Query\u003cName\u003e query = registry.query\u003cName\u003e();\n    Query\u003cName\u003e query_with = registry.query_with\u003cData\u003cPosition\u003e, Name\u003e();\n    Query\u003cName\u003e query_without = registry.query_without\u003cData\u003cPosition\u003e, Name\u003e();\n    Query\u003cName\u003e query_with_without = registry.query_with_without\u003cData\u003cPosition\u003e, Data\u003cHealth\u003e, Name\u003e();\n\n    // Iterate with for loop\n    for (auto [id, data] : query)\n    {\n        auto\u0026 [name] = data;\n\n        name.value = \"New name\";\n    }\n\n    // Iterate with callback\n    query.for_each([](Extraction\u003cName\u003e e)\n    {\n        auto\u0026 [id, data] = e;\n\n        auto\u0026 [name] = data;\n\n        name.value = \"New name\";\n    });\n}\n```\n\n# Bouncing balls example (using raylib, OUTDATED)\n\n```cpp\n#include \"include/raylib.h\"\n#include \"include/necs.hpp\"\n#include \u003ccmath\u003e\n\nusing namespace NECS;\n\nconst int WINDOW_W = 800;\nconst int WINDOW_H = 800;\nconst float BALL_SPEED = 300;\nconst float BALL_RADIUS = 10;\n\n// COMPONENTS\n\nstruct Position\n{\n    float x;\n    float y;\n};\nstruct Direction\n{\n    float x;\n    float y;\n};\n\n// ARCHETYPES\nusing Ball = Data\u003cColor, Position, Direction\u003e;\n\n// EVENTS\nstruct SpawnBalls\n{\n    int amount;\n    Vector2 bounds_max;\n    Vector2 bounds_min;\n};\n\n// QUERIES\nusing DrawQuery = Query\u003cPosition, Color\u003e;\nusing MoveQuery = Query\u003cPosition, Direction\u003e;\n\n// REGISTRY DATA\n\nusing Archetypes = Data\u003cBall\u003e;\nusing Events = Data\u003cSpawnBalls\u003e;\nusing Singletons = Data\u003c\u003e;\nusing Queries = Data\u003cDrawQuery, MoveQuery\u003e;\n\nRegistry\u003cArchetypes, Events, Singletons, Queries\u003e registry;\n\nauto ran_char(int min, int max)\n{\n    return static_cast\u003cunsigned char\u003e(GetRandomValue(min, max));\n}\n\nauto ran_float(float min, int max)\n{\n    return static_cast\u003cfloat\u003e(GetRandomValue(static_cast\u003cint\u003e(min), static_cast\u003cint\u003e(max)));\n}\n\nauto normalize(float\u0026 x, float\u0026 y)\n{\n    if (x != 0 \u0026\u0026 y != 0)\n    {\n        auto mag = sqrt(pow(x, 2) + pow(y, 2));\n\n        x = x / mag;\n        y = y / mag;\n    }\n}\n\nvoid InitSystem()\n{\n    auto spawn_balls = [](SpawnBalls event)\n    {\n        auto [amount, bounds_max, bounds_min] = event;\n\n        for (int i = 0; i \u003c amount; i++)\n        {\n            Color col =\n            {\n                ran_char(0, 255),\n                ran_char(0, 255),\n                ran_char(0, 255),\n                255\n            };\n            Position pos =\n            {\n                ran_float(bounds_min.x, bounds_max.x),\n                ran_float(bounds_min.y, bounds_max.y)\n            };\n            Direction dir =\n            {\n                ran_float(-100, 100),\n                ran_float(-100, 100)\n            };\n            normalize(dir.x, dir.y);\n\n            registry.create(Ball{col, pos, dir});\n        }\n    };\n\n    registry.subscribe\u003cSpawnBalls\u003e(spawn_balls);\n    registry.call\u003cSpawnBalls\u003e\n    ({\n        GetRandomValue(10, 20),\n        {static_cast\u003cfloat\u003e(WINDOW_W), static_cast\u003cfloat\u003e(WINDOW_H)},\n        {0, 0}\n    });\n}\n\nvoid InputSystem()\n{\n    auto target = GetMousePosition();\n\n    if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT))\n    {\n        int f = 5;\n        int x = static_cast\u003cint\u003e(target.x);\n        int y = static_cast\u003cint\u003e(target.y);\n\n        Vector2 max =\n        {\n            static_cast\u003cfloat\u003e(GetRandomValue(x, y + f)),\n            static_cast\u003cfloat\u003e(GetRandomValue(y, y + f))\n        };\n\n        Vector2 min =\n        {\n            static_cast\u003cfloat\u003e(GetRandomValue(x - f, x)),\n            static_cast\u003cfloat\u003e(GetRandomValue(y - f, y))\n        };\n\n        registry.call\u003cSpawnBalls\u003e\n        ({\n            GetRandomValue(1, 3),\n            max,\n            min\n        });\n    }\n}\n\nvoid MoveSystem()\n{\n    auto delta = GetFrameTime();\n\n    for (auto [id, data] : registry.query\u003cMoveQuery\u003e())\n    {\n        auto\u0026 [pos, dir] = data;\n\n        pos.x += dir.x * BALL_SPEED * delta;\n        pos.y += dir.y * BALL_SPEED * delta;\n\n        if (abs(pos.x) \u003e WINDOW_W || pos.x \u003c 0 )\n        {\n            dir.x = dir.x * -1;\n        }\n        if (abs(pos.y) \u003e WINDOW_H || pos.y \u003c 0 )\n        {\n            dir.y = dir.y * -1;\n        }\n    }\n}\n\nvoid DrawSystem()\n{\n    BeginDrawing();\n\n    ClearBackground(RAYWHITE);\n\n    for (auto [id, data] : registry.query\u003cDrawQuery\u003e())\n    {\n        auto\u0026 [pos, col] = data;\n\n        DrawCircle(pos.x, pos.y, BALL_RADIUS, col);\n    }\n\n    DrawText(TextFormat(\"Running at: %i FPS, Ball count %i\", GetFPS(), registry.pool_count\u003cBall\u003e()), 190, 200, 20, LIGHTGRAY);\n\n    EndDrawing();\n}\n\nint main ()\n{\n    InitWindow(WINDOW_W, WINDOW_H, \"Balls\");\n    InitAudioDevice();\n    InitSystem();\n\n    while (WindowShouldClose() == false)\n    {\n        InputSystem();\n        MoveSystem();\n        DrawSystem();\n    }\n\n    CloseAudioDevice();\n    CloseWindow();\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fscurrond%2Fnecs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fscurrond%2Fnecs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fscurrond%2Fnecs/lists"}