{"id":23428362,"url":"https://github.com/visualgmq/gecs","last_synced_at":"2025-04-12T20:20:53.259Z","repository":{"id":187199906,"uuid":"676083699","full_name":"VisualGMQ/gecs","owner":"VisualGMQ","description":"An ECS framework referenced Bevy-ECS, EnTT","archived":false,"fork":false,"pushed_at":"2025-01-08T14:15:37.000Z","size":371,"stargazers_count":29,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-26T14:38:42.592Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/VisualGMQ.png","metadata":{"files":{"readme":"ReadMe.md","changelog":null,"contributing":null,"funding":null,"license":null,"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}},"created_at":"2023-08-08T11:53:03.000Z","updated_at":"2025-03-26T09:52:52.000Z","dependencies_parsed_at":"2023-09-27T15:29:05.189Z","dependency_job_id":"9548b743-d421-40e5-907a-6a4e04967cbe","html_url":"https://github.com/VisualGMQ/gecs","commit_stats":null,"previous_names":["visualgmq/gecs"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/VisualGMQ%2Fgecs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/VisualGMQ%2Fgecs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/VisualGMQ%2Fgecs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/VisualGMQ%2Fgecs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/VisualGMQ","download_url":"https://codeload.github.com/VisualGMQ/gecs/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248626345,"owners_count":21135646,"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":[],"created_at":"2024-12-23T07:12:14.024Z","updated_at":"2025-04-12T20:20:53.233Z","avatar_url":"https://github.com/VisualGMQ.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# GECS\n\n`gecs` 是一个参考了[EnTT](https://github.com/skypjack/entt)源码结构，和[Bevy-ECS](https://bevyengine.org/)API的用于游戏开发的ECS系统。采用C++17。\n\n## 使用方法\n\n`demo`下有一个完整的例子\n\n### 基本例子\n\n`gecs`的API大量借鉴了`bevy`游戏引擎，下面是一个简单例子：\n\n```cpp\n// 包含头文件\n#include \"gecs/gecs.hpp\"\n\nusing namespace gecs;\n\n// 一个component类型\nstruct Name {\n    std::string name;\n};\n\n// 一个resource类型\nstruct Res {\n    int value;\n};\n\n// 每帧都会更新的system\nvoid update_system(commands cmds, querier\u003cName\u003e querier, resource\u003cRes\u003e res) {\n    for (auto\u0026 [_, name] : querier) {\n        std::cout \u003c\u003c name.name \u003c\u003c std::endl;\n    }\n\n    std::cout \u003c\u003c res-\u003evalue \u003c\u003c std::endl;\n}\n\nint main() {\n    world world;\n\n    // 得到Lambda对应的函数指针\n    constexpr auto startup = +[](commands cmds) {\n        auto entity1 = cmds.create();\n        cmds.emplace\u003cName\u003e(entity1, Name{\"ent1\"});\n        auto entity2 = cmds.create();\n        cmds.emplace\u003cName\u003e(entity2, Name{\"ent2\"});\n\n        cmds.emplace_resource\u003cRes\u003e(Res{123});\n    };\n\n    // 注册这个函数指针\n    world.regist_startup_system\u003cstartup\u003e();\n\n    // 使用普通函数\n    world.regist_update_system\u003cupdate_system\u003e();\n    // 使用函数指针也可\n    // world.regist_update_system\u003c\u0026update_system\u003e();\n\n    world.startup();\n    world.update();\n\n    return 0;\n}\n```\n\n### world\n\n`world` 是整个ECS的核心类，管理几乎所有ECS数据。\n\n使用默认构造函数创建一个即可：\n\n```cpp\ngecs::world world;\n```\n\n一个典型的ECS程序一般如下：\n\n```cpp\n// 创建world\ngecs::world world;\n\n// 声明一个registry\nauto\u0026 reg = gaming_world.regist_registry(\"gaming\");\n\n// 注册startup system\nreg.regist_starup_system\u003cyour_startup_system1\u003e();\nreg.regist_starup_system\u003cyour_startup_system2\u003e();\n...\n\n// 注册update system \nreg.regist_update_system\u003cyour_update_system1\u003e();\nreg.regist_update_system\u003cyour_update_system2\u003e();\n...\n\n// 启动ECS\nworld.startup();\n\n// 游戏循环中每帧更新ECS\nwhile (shouldClose()) {\n    world.update();\n}\n\n// 结束ECS\n// 也可以不调用，world析构时会自动调用\nworld.shutdown();   \n```\n\n`world`是由多个`registry`组成的。`registry`中存储着有关的entity,component,system。只有`resource`是在各个`registry`间是通用的。\n\n使用`world.regist_registry(name)`注册一个`registry`。然后可以将系统注册在此`registry`上。\n\n一般来说你不会使用超过一个的`registry`。\n\n### system\n\n`system`分为两种：\n* `startup system`：在启动时执行一次，主要用于初始化数据\n* `update system`：每帧运行一次\n\n`system`**不是**`std::function`类型，而是普通函数类型。所以若想使用lambda，则不能有任何捕获。\n\n`system`没有固定的函数声明，但只能包含零个或多个`querier`/`resource`/`commands`。参数顺序没有要求。\n\n`startup system`使用`regist_startup_system`即可注册：\n\n```cpp\nworld.regist_startup_system\u003cyour_startup_system\u003e();\n```\n\n`update system`使用`regist_update_system`即可注册:\n\n```cpp\n// 使用lambda，无捕获的lambda会被转换为普通函数，在lambda前面加`+`可以获得对应函数类型\n// 含有两个querier和一个resource\nworld.regist_update_system\u003c+[](querier\u003cName\u003e q1, querier\u003cFamily\u003e q2, resource\u003cFamilyBook\u003e res)\u003e();\n// 含有一个commands和一个q1\nworld.regist_update_system\u003c+[](commands cmd, querier\u003cName\u003e q1)\u003e();\n```\n\n### querier和resource\n\n#### querier\n\n`querier`用于从`world`中查询拥有某种组件的实体，一般作为`system`的参数：\n\n```cpp\n// q1查询所有含有Name实体的组件，q2查询所有函数Family实体的组件。并且Name组件不可变，Family可变\nvoid update_system(querier\u003cName\u003e q1, querier\u003cmut\u003cFamily\u003e\u003e q2) { ... }\n```\n\n只有使用`mut\u003cT\u003e`模板包裹组件类型时，才能够得到可变组件。这是为了之后对各个系统进行并行执行打下基础。\n\n可以直接遍历`querier`来得到所有实体和对应组件：\n\n```cpp\nfor (auto\u0026 [entity, name] : q1) {\n    ...\n}\n\n// 组件很多时按querier类型中声明的顺序得到\nfor (auto\u0026 [entity, comp1, comp2, comp3] : multi_queirer) {\n    ...\n}\n```\n\n可以使用一些条件来进行查询：\n\n* `only\u003cTs...\u003e`：要求实体只能拥有指定的组件，使用此条件时不能有其他参数：\n    ```cpp\n    void update_system(querier\u003conly\u003cComp1, Comp2\u003e\u003e q); // 会查询所有只含有Comp1, Comp2的组件\n\n    void update_system(querier\u003cComp1, only\u003cComp2, Comp3\u003e\u003e); // 非法！only只能单独存在且只有一个\n    ```\n* `without\u003cT\u003e`：要求实体不能拥有此组件，语句中只能有一个`without`，并且语句中必须含有其他的无条件查询类型：\n    ```cpp\n    void system(querier\u003cComp1, without\u003cComp2\u003e\u003e);    //查询所有含有Comp1但不含有Comp2的组件\n    void system(querier\u003cComp1, without\u003cComp2, Comp3\u003e\u003e); // 查询所有含Comp1，但不含有Comp2和Comp3的组件\n\n    void system(querier\u003cwithout\u003cComp2\u003e\u003e);   // 非法！必须含有至少一个无查询条件的类型\n    ```\n\n#### resource\n\n`resource`则是对资源的获取。资源是一种在ECS中唯一的组件：\n\n```cpp\nvoid system(resource\u003cName\u003e res) {\n    // 通过operator-\u003e直接获得。不存在资源会导致程序崩溃！\n    res-\u003ename = \"ent\";\n}\n```\n\n### commands\n\n`commands`是用于向`world`中添加/删除实体/组件/资源的类：\n\n```cpp\nvoid system(commands cmds) {\n    // 创建entity\n    auto entity = cmds.create();\n    // 附加组件到实体\n    cmds.emplace\u003cName\u003e(entity, Name{\"ent\"});\n\n    // 从实体上删除组件\n    cmds.remove\u003cName\u003e(entity);\n\n    // 删除实体及其所有组件\n    cmds.destroy(entity);\n\n    // 设置资源\n    cmds.emplace_resource\u003cRes\u003e(Res{});\n\n    // 移除并释放资源\n    cmds.remove_resource\u003cRes\u003e();\n}\n```\n\n有些组件必须一起创建才能正常工作，而`Bundle`可以一次性创建多个组件以防遗忘：\n`Bundle`不是一个具体类，而是用户自定义的POD类。类中的所有成员变量会被作为component附加在entity上：\n\n```cpp\nstruct Comp1 {};\nstruct Comp2 {};\n\n// 定义一个bundle\nstruct CompBundle {\n    Comp1 comp1;\n    Comp2 comp2;\n};\n\n// in main():\ncmds.emplace_bundle\u003cCompBundle\u003e(entity, CompBundle{...});\n```\n\n创建之后`entity`将会拥有`Comp1`和`Comp2`两个组件。\n\n### registry\n\n`registry`是当前world中的registry类型，保存着和此registry有关的所有entity,component,system的信息。并且可以对其进行任意操作。\n**不到万不得已不推荐使用此**类型。\n\n要想使用可以在`system`中通过`gecs::registry`得到，多个`registry`底层是同一个（当前`registry`）\n\n```cpp\nvoid system(gecs::registry reg);\n```\n\n### state\n\n`state`用于切换`registry`中的状态。每个`state`都存储着一系列的system。通过切换`state`可以快速在同一个`registry`中切换不同的功能。\n\n使用`registry.add_state(numeric)`来创建一个`state`。`state`由整数或者枚举表示（推荐枚举）：\n\n```cpp\nenum class States {\n    State1,\n    State2,\n};\n\nregistry.add_state(States::State1);\n```\n\n使用如下方法向`state`中添加一个系统：\n\n```cpp\n// 添加一个开始系统\nregistry.regist_enter_system_to_state\u003cOnEnterWelcome\u003e(GameState::Welcome)\n    // 添加一个退出系统\n    .regist_exit_system_to_state\u003cOnExitWelcome\u003e(GameState::Welcome)\n    // 添加一个更新系统\n    .regist_update_system_to_state\u003cFallingStoneGenerate\u003e(GameState::Welcome);\n```\n\n每次切换`state`的时候，都会调用当前`state`的所有exit system，并调用新`state`的所有enter system。切换`state`使用：\n\n```cpp\nregistry.switch_state_immediatly(state);\nregistry.switch_state(state);\n```\n\n来切换`state`。`switch_state()`会延迟到这一帧结束时切换。\n\n`state`的系统和`registry`中的系统执行顺序如下：\n\n```cpp\nregistry::startup\n        |\n  state::enter\n        |\n        |\u003c--------------\n        |              |\nregistry::update       |\n        |          game loop\n  state::update        |\n        |              |\n        |--------------|\n        |\n   state::exit\n        |\nregistry::shutdown\n```\n\n### 系统的增加和删除\n\n使用`registry`可以直接增加/删除系统:\n\n```cpp\n// 为registry增加/删除系统\nregistry.regist_startup_system\u003cSys\u003e();\nregistry.remove_startup_system\u003cSys\u003e();\n\n// 为state增加/删除系统\nregistry.regist_enter_system_to_state\u003cSys\u003e(State::State1);\nregistry.remove_enter_system_to_state\u003cSys\u003e(State::State1);\n```\n\n注意：**为`registry`增加/删除的系统会立刻应用上，而为`state`增加/删除的系统会在`world.update()`末尾附加上。**\n\n这意味着在某个startup系统中，可以为`registry`的startup系统增加新系统，增加的系统在之后会被执行。而若在`state`的某个enter system中新增另一个enter system，则毫无意义，因为新增的system会在`state`的所有enter system调用完毕后再附加在`state`上。除非你从另一个`state`切换到这个`state`，这样此`state`会再次调用所有的enter system（包括后附加的）。\n\nexit system同理。\n\n目前暂不支持在system中提供增加/删除registry system的方法，因为这样做会导致system混乱。原则上来说，registry的system只能在ECS启动前完成全部初始化。但是可以在运行时改变`state`的system（通过`registry`）。\n\n### signal系统\n\nsignal类似于Qt的信号槽或Godot的signal。用于更好地实现观察者模式。\n\n在`system`声明中，可以使用`event_dispatcher\u003cT\u003e`来注册/触发/缓存一个T类型事件：\n\n```cpp\nvoid Startup(gecs::commands cmds, gecs::event_dispatcher\u003cSDL_QuitEvent\u003e quit,\n             gecs::event_dispatcher\u003cSDL_KeyboardEvent\u003e keyboard);\n```\n\n`event_dispatcher`可以链接多个回调函数，以便于在事件触发时自动调用此函数：\n\n```cpp\nconstexpr auto f = +[](const SDL_QuitEvent\u0026 event,\n                        gecs::resource\u003cgecs::mut\u003cGameContext\u003e\u003e ctx) {\n    ctx-\u003eshouldClose = true;\n};\n\n// 使用sink()函数获得信号槽，然后增加一个函数\nquit.sink().add\u003cf\u003e();\n```\n\n函数会按照加入的顺序被调用。\n\n函数的声明和`system`一样，只是第一个参数必须是事件类型`T`相关的`const T\u0026`。\n\n删除事件回调函数也是通过信号槽删除，这里不再演示。\n\n想要缓存新事件，可以使用`enqueue()`函数:\n\n```cpp\nvoid EventDispatcher(gecs::resource\u003cgecs::mut\u003cGameContext\u003e\u003e ctx,\n                     gecs::event_dispatcher\u003cSDL_QuitEvent\u003e quit,\n                     gecs::event_dispatcher\u003cSDL_KeyboardEvent\u003e keyboard) {\n    while (SDL_PollEvent(\u0026ctx-\u003eevent)) {\n        if (ctx-\u003eevent.type == SDL_QUIT) {\n            // 放入一个SDL_QuitEvent\n            quit.enqueue(ctx-\u003eevent.quit);\n        }\n        if (ctx-\u003eevent.type == SDL_KEYDOWN || ctx-\u003eevent.type == SDL_KEYUP) {\n            // 放入一个SDL_KeyboardEvent\n            keyboard.enqueue(ctx-\u003eevent.key);\n        }\n    }\n}\n```\n\n缓存的事件会被放在缓存列表里，可以被一次性全部触发：\n\n```cpp\nquit.trigger_cached();\n\n// 如果有必要，触发完不要忘了删除所有缓存事件\nquit.clear_cache();\n\n// 或者，也可以调用update()自动做上面两个事情\nquit.update();\n```\n\n如果不触发，在`world.update()`的最后（所有`update system`调用后）会自动触发所有事件并删除。\n\n如果想要立刻触发，使用：\n\n```cpp\nquit.trigger(YourQuitEvent);\n```\n\n来触发。\n\n### Demo\n\n为了测试ECS的稳定性，编写了一个Demo。默认是不编译的，需要SDL2库。请在根目录下运行。\n\n![demo](./snapshot/snapshot.png)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvisualgmq%2Fgecs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvisualgmq%2Fgecs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvisualgmq%2Fgecs/lists"}