Giter Club home page Giter Club logo

gecs's Introduction

GECS

gecs 是一个参考了EnTT源码结构,和Bevy-ECSAPI的用于游戏开发的ECS系统。采用C++17。

使用方法

demo下有一个完整的例子

基本例子

gecs的API大量借鉴了bevy游戏引擎,下面是一个简单例子:

// 包含头文件
#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<Name> querier, resource<Res> 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<Name>(entity1, Name{"ent1"});
        auto entity2 = cmds.create();
        cmds.emplace<Name>(entity2, Name{"ent2"});

        cmds.emplace_resource<Res>(Res{123});
    };

    // 注册这个函数指针
    world.regist_startup_system<startup>();

    // 使用普通函数
    world.regist_update_system<update_system>();
    // 使用函数指针也可
    // world.regist_update_system<&update_system>();

    world.startup();
    world.update();

    return 0;
}

world

world 是整个ECS的核心类,管理几乎所有ECS数据。

使用默认构造函数创建一个即可:

gecs::world world;

一个典型的ECS程序一般如下:

// 创建world
gecs::world world;

// 声明一个registry
auto& reg = gaming_world.regist_registry("gaming");

// 注册startup system
reg.regist_starup_system<your_startup_system1>();
reg.regist_starup_system<your_startup_system2>();
...

// 注册update system 
reg.regist_update_system<your_update_system1>();
reg.regist_update_system<your_update_system2>();
...

// 启动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即可注册:

world.regist_startup_system<your_startup_system>();

update system使用regist_update_system即可注册:

// 使用lambda,无捕获的lambda会被转换为普通函数,在lambda前面加`+`可以获得对应函数类型
// 含有两个querier和一个resource
world.regist_update_system<+[](querier<Name> q1, querier<Family> q2, resource<FamilyBook> res)>();
// 含有一个commands和一个q1
world.regist_update_system<+[](commands cmd, querier<Name> q1)>();

querier和resource

querier

querier用于从world中查询拥有某种组件的实体,一般作为system的参数:

// q1查询所有含有Name实体的组件,q2查询所有函数Family实体的组件。并且Name组件不可变,Family可变
void update_system(querier<Name> q1, querier<mut<Family>> q2) { ... }

只有使用mut<T>模板包裹组件类型时,才能够得到可变组件。这是为了之后对各个系统进行并行执行打下基础。

可以直接遍历querier来得到所有实体和对应组件:

for (auto& [entity, name] : q1) {
    ...
}

// 组件很多时按querier类型中声明的顺序得到
for (auto& [entity, comp1, comp2, comp3] : multi_queirer) {
    ...
}

可以使用一些条件来进行查询:

  • only<Ts...>:要求实体只能拥有指定的组件,使用此条件时不能有其他参数:
    void update_system(querier<only<Comp1, Comp2>> q); // 会查询所有只含有Comp1, Comp2的组件
    
    void update_system(querier<Comp1, only<Comp2, Comp3>>); // 非法!only只能单独存在且只有一个
  • without<T>:要求实体不能拥有此组件,语句中只能有一个without,并且语句中必须含有其他的无条件查询类型:
    void system(querier<Comp1, without<Comp2>>);    //查询所有含有Comp1但不含有Comp2的组件
    void system(querier<Comp1, without<Comp2, Comp3>>); // 查询所有含Comp1,但不含有Comp2和Comp3的组件
    
    void system(querier<without<Comp2>>);   // 非法!必须含有至少一个无查询条件的类型

resource

resource则是对资源的获取。资源是一种在ECS中唯一的组件:

void system(resource<Name> res) {
    // 通过operator->直接获得。不存在资源会导致程序崩溃!
    res->name = "ent";
}

commands

commands是用于向world中添加/删除实体/组件/资源的类:

void system(commands cmds) {
    // 创建entity
    auto entity = cmds.create();
    // 附加组件到实体
    cmds.emplace<Name>(entity, Name{"ent"});

    // 从实体上删除组件
    cmds.remove<Name>(entity);

    // 删除实体及其所有组件
    cmds.destroy(entity);

    // 设置资源
    cmds.emplace_resource<Res>(Res{});

    // 移除并释放资源
    cmds.remove_resource<Res>();
}

有些组件必须一起创建才能正常工作,而Bundle可以一次性创建多个组件以防遗忘: Bundle不是一个具体类,而是用户自定义的POD类。类中的所有成员变量会被作为component附加在entity上:

struct Comp1 {};
struct Comp2 {};

// 定义一个bundle
struct CompBundle {
    Comp1 comp1;
    Comp2 comp2;
};

// in main():
cmds.emplace_bundle<CompBundle>(entity, CompBundle{...});

创建之后entity将会拥有Comp1Comp2两个组件。

registry

registry是当前world中的registry类型,保存着和此registry有关的所有entity,component,system的信息。并且可以对其进行任意操作。 不到万不得已不推荐使用此类型。

要想使用可以在system中通过gecs::registry得到,多个registry底层是同一个(当前registry

void system(gecs::registry reg);

state

state用于切换registry中的状态。每个state都存储着一系列的system。通过切换state可以快速在同一个registry中切换不同的功能。

使用registry.add_state(numeric)来创建一个statestate由整数或者枚举表示(推荐枚举):

enum class States {
    State1,
    State2,
};

registry.add_state(States::State1);

使用如下方法向state中添加一个系统:

// 添加一个开始系统
registry.regist_enter_system_to_state<OnEnterWelcome>(GameState::Welcome)
    // 添加一个退出系统
    .regist_exit_system_to_state<OnExitWelcome>(GameState::Welcome)
    // 添加一个更新系统
    .regist_update_system_to_state<FallingStoneGenerate>(GameState::Welcome);

每次切换state的时候,都会调用当前state的所有exit system,并调用新state的所有enter system。切换state使用:

registry.switch_state_immediatly(state);
registry.switch_state(state);

来切换stateswitch_state()会延迟到这一帧结束时切换。

state的系统和registry中的系统执行顺序如下:

registry::startup
        |
  state::enter
        |
        |<--------------
        |              |
registry::update       |
        |          game loop
  state::update        |
        |              |
        |--------------|
        |
   state::exit
        |
registry::shutdown

系统的增加和删除

使用registry可以直接增加/删除系统:

// 为registry增加/删除系统
registry.regist_startup_system<Sys>();
registry.remove_startup_system<Sys>();

// 为state增加/删除系统
registry.regist_enter_system_to_state<Sys>(State::State1);
registry.remove_enter_system_to_state<Sys>(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>来注册/触发/缓存一个T类型事件:

void Startup(gecs::commands cmds, gecs::event_dispatcher<SDL_QuitEvent> quit,
             gecs::event_dispatcher<SDL_KeyboardEvent> keyboard);

event_dispatcher可以链接多个回调函数,以便于在事件触发时自动调用此函数:

constexpr auto f = +[](const SDL_QuitEvent& event,
                        gecs::resource<gecs::mut<GameContext>> ctx) {
    ctx->shouldClose = true;
};

// 使用sink()函数获得信号槽,然后增加一个函数
quit.sink().add<f>();

函数会按照加入的顺序被调用。

函数的声明和system一样,只是第一个参数必须是事件类型T相关的const T&

删除事件回调函数也是通过信号槽删除,这里不再演示。

想要缓存新事件,可以使用enqueue()函数:

void EventDispatcher(gecs::resource<gecs::mut<GameContext>> ctx,
                     gecs::event_dispatcher<SDL_QuitEvent> quit,
                     gecs::event_dispatcher<SDL_KeyboardEvent> 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);
        }
    }
}

缓存的事件会被放在缓存列表里,可以被一次性全部触发:

quit.trigger_cached();

// 如果有必要,触发完不要忘了删除所有缓存事件
quit.clear_cache();

// 或者,也可以调用update()自动做上面两个事情
quit.update();

如果不触发,在world.update()的最后(所有update system调用后)会自动触发所有事件并删除。

如果想要立刻触发,使用:

quit.trigger(YourQuitEvent);

来触发。

Demo

为了测试ECS的稳定性,编写了一个Demo。默认是不编译的,需要SDL2库。请在根目录下运行。

demo

gecs's People

Contributors

visualgmq avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.