https://github.com/visualgmq/gecs
An ECS framework referenced Bevy-ECS, EnTT
https://github.com/visualgmq/gecs
Last synced: about 1 year ago
JSON representation
An ECS framework referenced Bevy-ECS, EnTT
- Host: GitHub
- URL: https://github.com/visualgmq/gecs
- Owner: VisualGMQ
- Created: 2023-08-08T11:53:03.000Z (almost 3 years ago)
- Default Branch: main
- Last Pushed: 2025-01-08T14:15:37.000Z (over 1 year ago)
- Last Synced: 2025-03-26T14:38:42.592Z (over 1 year ago)
- Language: C++
- Homepage:
- Size: 362 KB
- Stars: 29
- Watchers: 1
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: ReadMe.md
Awesome Lists containing this project
README
# GECS
`gecs` 是一个参考了[EnTT](https://github.com/skypjack/entt)源码结构,和[Bevy-ECS](https://bevyengine.org/)API的用于游戏开发的ECS系统。采用C++17。
## 使用方法
`demo`下有一个完整的例子
### 基本例子
`gecs`的API大量借鉴了`bevy`游戏引擎,下面是一个简单例子:
```cpp
// 包含头文件
#include "gecs/gecs.hpp"
using namespace gecs;
// 一个component类型
struct Name {
std::string name;
};
// 一个resource类型
struct Res {
int value;
};
// 每帧都会更新的system
void update_system(commands cmds, querier querier, resource res) {
for (auto& [_, name] : querier) {
std::cout << name.name << std::endl;
}
std::cout << res->value << std::endl;
}
int main() {
world world;
// 得到Lambda对应的函数指针
constexpr auto startup = +[](commands cmds) {
auto entity1 = cmds.create();
cmds.emplace(entity1, Name{"ent1"});
auto entity2 = cmds.create();
cmds.emplace(entity2, Name{"ent2"});
cmds.emplace_resource(Res{123});
};
// 注册这个函数指针
world.regist_startup_system();
// 使用普通函数
world.regist_update_system();
// 使用函数指针也可
// world.regist_update_system<&update_system>();
world.startup();
world.update();
return 0;
}
```
### world
`world` 是整个ECS的核心类,管理几乎所有ECS数据。
使用默认构造函数创建一个即可:
```cpp
gecs::world world;
```
一个典型的ECS程序一般如下:
```cpp
// 创建world
gecs::world world;
// 声明一个registry
auto& reg = gaming_world.regist_registry("gaming");
// 注册startup system
reg.regist_starup_system();
reg.regist_starup_system();
...
// 注册update system
reg.regist_update_system();
reg.regist_update_system();
...
// 启动ECS
world.startup();
// 游戏循环中每帧更新ECS
while (shouldClose()) {
world.update();
}
// 结束ECS
// 也可以不调用,world析构时会自动调用
world.shutdown();
```
`world`是由多个`registry`组成的。`registry`中存储着有关的entity,component,system。只有`resource`是在各个`registry`间是通用的。
使用`world.regist_registry(name)`注册一个`registry`。然后可以将系统注册在此`registry`上。
一般来说你不会使用超过一个的`registry`。
### system
`system`分为两种:
* `startup system`:在启动时执行一次,主要用于初始化数据
* `update system`:每帧运行一次
`system`**不是**`std::function`类型,而是普通函数类型。所以若想使用lambda,则不能有任何捕获。
`system`没有固定的函数声明,但只能包含零个或多个`querier`/`resource`/`commands`。参数顺序没有要求。
`startup system`使用`regist_startup_system`即可注册:
```cpp
world.regist_startup_system();
```
`update system`使用`regist_update_system`即可注册:
```cpp
// 使用lambda,无捕获的lambda会被转换为普通函数,在lambda前面加`+`可以获得对应函数类型
// 含有两个querier和一个resource
world.regist_update_system<+[](querier q1, querier q2, resource res)>();
// 含有一个commands和一个q1
world.regist_update_system<+[](commands cmd, querier q1)>();
```
### querier和resource
#### querier
`querier`用于从`world`中查询拥有某种组件的实体,一般作为`system`的参数:
```cpp
// q1查询所有含有Name实体的组件,q2查询所有函数Family实体的组件。并且Name组件不可变,Family可变
void update_system(querier q1, querier> q2) { ... }
```
只有使用`mut`模板包裹组件类型时,才能够得到可变组件。这是为了之后对各个系统进行并行执行打下基础。
可以直接遍历`querier`来得到所有实体和对应组件:
```cpp
for (auto& [entity, name] : q1) {
...
}
// 组件很多时按querier类型中声明的顺序得到
for (auto& [entity, comp1, comp2, comp3] : multi_queirer) {
...
}
```
可以使用一些条件来进行查询:
* `only`:要求实体只能拥有指定的组件,使用此条件时不能有其他参数:
```cpp
void update_system(querier> q); // 会查询所有只含有Comp1, Comp2的组件
void update_system(querier>); // 非法!only只能单独存在且只有一个
```
* `without`:要求实体不能拥有此组件,语句中只能有一个`without`,并且语句中必须含有其他的无条件查询类型:
```cpp
void system(querier>); //查询所有含有Comp1但不含有Comp2的组件
void system(querier>); // 查询所有含Comp1,但不含有Comp2和Comp3的组件
void system(querier>); // 非法!必须含有至少一个无查询条件的类型
```
#### resource
`resource`则是对资源的获取。资源是一种在ECS中唯一的组件:
```cpp
void system(resource res) {
// 通过operator->直接获得。不存在资源会导致程序崩溃!
res->name = "ent";
}
```
### commands
`commands`是用于向`world`中添加/删除实体/组件/资源的类:
```cpp
void system(commands cmds) {
// 创建entity
auto entity = cmds.create();
// 附加组件到实体
cmds.emplace(entity, Name{"ent"});
// 从实体上删除组件
cmds.remove(entity);
// 删除实体及其所有组件
cmds.destroy(entity);
// 设置资源
cmds.emplace_resource(Res{});
// 移除并释放资源
cmds.remove_resource();
}
```
有些组件必须一起创建才能正常工作,而`Bundle`可以一次性创建多个组件以防遗忘:
`Bundle`不是一个具体类,而是用户自定义的POD类。类中的所有成员变量会被作为component附加在entity上:
```cpp
struct Comp1 {};
struct Comp2 {};
// 定义一个bundle
struct CompBundle {
Comp1 comp1;
Comp2 comp2;
};
// in main():
cmds.emplace_bundle(entity, CompBundle{...});
```
创建之后`entity`将会拥有`Comp1`和`Comp2`两个组件。
### registry
`registry`是当前world中的registry类型,保存着和此registry有关的所有entity,component,system的信息。并且可以对其进行任意操作。
**不到万不得已不推荐使用此**类型。
要想使用可以在`system`中通过`gecs::registry`得到,多个`registry`底层是同一个(当前`registry`)
```cpp
void system(gecs::registry reg);
```
### state
`state`用于切换`registry`中的状态。每个`state`都存储着一系列的system。通过切换`state`可以快速在同一个`registry`中切换不同的功能。
使用`registry.add_state(numeric)`来创建一个`state`。`state`由整数或者枚举表示(推荐枚举):
```cpp
enum class States {
State1,
State2,
};
registry.add_state(States::State1);
```
使用如下方法向`state`中添加一个系统:
```cpp
// 添加一个开始系统
registry.regist_enter_system_to_state(GameState::Welcome)
// 添加一个退出系统
.regist_exit_system_to_state(GameState::Welcome)
// 添加一个更新系统
.regist_update_system_to_state(GameState::Welcome);
```
每次切换`state`的时候,都会调用当前`state`的所有exit system,并调用新`state`的所有enter system。切换`state`使用:
```cpp
registry.switch_state_immediatly(state);
registry.switch_state(state);
```
来切换`state`。`switch_state()`会延迟到这一帧结束时切换。
`state`的系统和`registry`中的系统执行顺序如下:
```cpp
registry::startup
|
state::enter
|
|<--------------
| |
registry::update |
| game loop
state::update |
| |
|--------------|
|
state::exit
|
registry::shutdown
```
### 系统的增加和删除
使用`registry`可以直接增加/删除系统:
```cpp
// 为registry增加/删除系统
registry.regist_startup_system();
registry.remove_startup_system();
// 为state增加/删除系统
registry.regist_enter_system_to_state(State::State1);
registry.remove_enter_system_to_state(State::State1);
```
注意:**为`registry`增加/删除的系统会立刻应用上,而为`state`增加/删除的系统会在`world.update()`末尾附加上。**
这意味着在某个startup系统中,可以为`registry`的startup系统增加新系统,增加的系统在之后会被执行。而若在`state`的某个enter system中新增另一个enter system,则毫无意义,因为新增的system会在`state`的所有enter system调用完毕后再附加在`state`上。除非你从另一个`state`切换到这个`state`,这样此`state`会再次调用所有的enter system(包括后附加的)。
exit system同理。
目前暂不支持在system中提供增加/删除registry system的方法,因为这样做会导致system混乱。原则上来说,registry的system只能在ECS启动前完成全部初始化。但是可以在运行时改变`state`的system(通过`registry`)。
### signal系统
signal类似于Qt的信号槽或Godot的signal。用于更好地实现观察者模式。
在`system`声明中,可以使用`event_dispatcher`来注册/触发/缓存一个T类型事件:
```cpp
void Startup(gecs::commands cmds, gecs::event_dispatcher quit,
gecs::event_dispatcher keyboard);
```
`event_dispatcher`可以链接多个回调函数,以便于在事件触发时自动调用此函数:
```cpp
constexpr auto f = +[](const SDL_QuitEvent& event,
gecs::resource> ctx) {
ctx->shouldClose = true;
};
// 使用sink()函数获得信号槽,然后增加一个函数
quit.sink().add();
```
函数会按照加入的顺序被调用。
函数的声明和`system`一样,只是第一个参数必须是事件类型`T`相关的`const T&`。
删除事件回调函数也是通过信号槽删除,这里不再演示。
想要缓存新事件,可以使用`enqueue()`函数:
```cpp
void EventDispatcher(gecs::resource> ctx,
gecs::event_dispatcher quit,
gecs::event_dispatcher keyboard) {
while (SDL_PollEvent(&ctx->event)) {
if (ctx->event.type == SDL_QUIT) {
// 放入一个SDL_QuitEvent
quit.enqueue(ctx->event.quit);
}
if (ctx->event.type == SDL_KEYDOWN || ctx->event.type == SDL_KEYUP) {
// 放入一个SDL_KeyboardEvent
keyboard.enqueue(ctx->event.key);
}
}
}
```
缓存的事件会被放在缓存列表里,可以被一次性全部触发:
```cpp
quit.trigger_cached();
// 如果有必要,触发完不要忘了删除所有缓存事件
quit.clear_cache();
// 或者,也可以调用update()自动做上面两个事情
quit.update();
```
如果不触发,在`world.update()`的最后(所有`update system`调用后)会自动触发所有事件并删除。
如果想要立刻触发,使用:
```cpp
quit.trigger(YourQuitEvent);
```
来触发。
### Demo
为了测试ECS的稳定性,编写了一个Demo。默认是不编译的,需要SDL2库。请在根目录下运行。
