One more ECS. You can check this repo for use case.
The first step is to create a new World
.
World world;
The World
has a Registry
inside. The Registry
adds systems and functions for execution. Also it has prepare
and exec
methods to select entities and calculate one frame respectively. The prepare
method is thread safe, so you can call it from the render
thread if you have separate threads for graphic and logic.
auto* reg = world.getRegistry();
reg->addSystem<MySystem>(/*args...*/);
reg->initNewSystems();
// after you have some systems you can get access to them
auto* my_system = reg.getSystem<MySystem>();
assert(my_system);
while(true) {
reg->prepare();
reg->exec();
}
reg.removeSystem<MySystem>();
To create your own system you need to derive from BaseSystem
class and define void setup(Registry&)
function. You also can override virtual void stop(Registry&)
if you want to execute code before System
will be removed from registry.
struct MySystem final : BaseSystem {
MySystem() = default;
~MySystem() override = default;
void setup(Registry&);
void stop(Registry&) override;
};
Each system can have functions to modify components. You need to provide filters to get entities from the World
.
struct MySystem final : BaseSystem {
//...
private:
using UpdateTransformFilter = Filter<Require<Transform>, Exclude<Camera>>;
using UpdateCameraTransformFilter = Filter<Require<Transform, Camera>>;
void updateTransform(OBSERVER(UpdateTransformFilter));
void updateCameraTransform(OBSERVER(UpdateCameraTransformFilter));
// you can have more than one filter
void update(OBSERVER(UpdateTransformFilter), OBSERVER(UpdateCameraTransformFilter));
}
void MySystem::updateTransform(OBSERVER(UpdateTransformFilter) observer) {
for (auto e : observer) {
auto& [translation, rotation, scale] = e.get<Transform>();
translation = {0, 1, 2};
}
}
void MySystem::updateCameraTransform(OBSERVER(UpdateCameraTransformFilter) observer) {
for (auto e : observer) {
auto& [translation, rotation, scale] = e.get<Transform>();
auto& [pitch, yaw, view] = e.get<Camera>();
translation = {4, 5, 6};
pitch = 42.f;
}
}
// OBSERVER_EMPTY is for execute function every frame without entities. You can use observer to create a new entity.
void func(OBSERVER_EMPTY) {}
void MySystem::setup(Registry& reg) {
ECS_REG_FUNC(reg, MySystem::updateTransform);
ECS_REG_FUNC(reg, MySystem::updateCameraTransform);
ECS_REG_FUNC(reg, MySystem::update); // same registration for all functions
// to register external function use ECS_REG_EXTERN_FUNC macro.
ECS_REG_EXTERN_FUNC(reg, func);
}
void MySystem::stop(Registry& reg) {
// when you want to remove function from execution you can use
ECS_UNREG_FUNC(reg, MySystem::updateTransform);
ECS_UNREG_FUNC(reg, func);
}
Or you can register your function from outside.
auto& reg = *world.getRegistry();
auto* my_system = reg.addSystem<MySystem>();
ECS_REG_FUNC_SYS(reg, MySystem::updateTransform, my_system);
ECS_REG_FUNC_SYS(reg, MySystem::updateCameraTransform, my_system);
ECS_REG_FUNC_SYS(reg, MySystem::update, my_system);
reg.initNewSystems();
You can use any type as component, but you need to register it before. To do this we have a ComponentRegistrant
helper class. You can check entity_debug.cpp
for examples.
struct Camera {
float pitch;
float yaw;
glm::mat4 view;
};
struct Transform {
glm::vec3 translation;
glm::vec3 rotation;
glm::vec3 scale;
};
//...
ComponentRegistrant<Transform, Camera>(m_world)
.createStorage(); // without storage you cannot use components
Check observer.h
or world.h
for more information
void MySystem::func(OBSERVER(Filter) observer) {
for (auto e : observer) {
// you can get access to all components like this
auto [Transform, Camera] = e.get();
// NOTE: Tags will be ignored and you will get only components with data
// WARN: returns std::tuple<T&, ...>
// you can create a new empty entity
auto new_entity = observer.create();
// entity.func<T>() is eq of observer.func<T>(e)
// assign components
new_entity.emplace<Transform>({});
// or
new_entity.emplace(Camera{});
// or
new_entity.forceEmplace<Camera>({}); // to override if exists
// or
new_entity.emplaceTagged<Camera>({}); // emplace component and tag Updated<Camera>
new_entity.markUpdated<Camera>(); // add tag Updated<Camera>
new_entity.clearUpdateTag<Camera>(); // remove tag Updated<Camera>
// NOTE: you can use Updated<T> tag to only get components you marked Updated
// get by ref for modify or const ref to read only (must be in Requires and not in Exclude)
auto& camera = new_entity.get<Camera>();
auto* camera_ptr = new_entity.tryGet<Camera>(); // can be used without restrictions
// check if Entity has Component
bool has_camera = new_entity.has<Camera>();
// erase components
new_entity.erase<Transform, Camera>();
// and destroy entity
new_entity.destroy();
}
// you also able to remove array of Entities
observer.destroy(); // will destroy all entities matched by Filter
}
// or you can use `world`
auto entity = world.create();
world.emplace<Camera>(entity);
// you cannot use entity.emplace<T>() here, because World returns Entity ID
// and you have to explicitly pass it to all functions
// but you can create an empty observer and get all functionality
auto observer = Observer(world);
You can use Archetype
to pack components.
struct Player {};
struct Boss {};
struct Damage {
int damage = 0;
};
struct HP {
int hp = 0;
};
struct Name {
std::string name;
};
using PlayerArchetype = Archetype<Name, HP, Damage, Player>;
struct PlayerType : PlayerArchetype {
PlayerType() : PlayerArchetype({"Player"}, {100}, {3}) {};
};
using BossArchetype = Archetype<Name, HP, Damage, Boss>;
struct BossType : BossArchetype {
BossType() : BossArchetype({"Boss"}, {1000}, {10}) {};
};
You can also use it as a filter.
using PlayerFilter = Filter<Require<PlayerArchetype>>;
using BossFilter = Filter<Require<BossArchetype>>;
And create entity with all components in one line.
void MySystem::create(OBSERVER(PlayerFilter) observer) {
// just Archetype with default values of components
observer.create<PlayerArchetype>();
// or a new entity with PlayerType defaults
observer.create<PlayerType>();
// or exact instance
PlayerType instance;
instance.name = "John";
instance.damage = 42;
observer.create(std::move(instance));
}
If you want to use ECS in separate thread you can use Registry
functions for it:
syncWithRender()
- ECS function. Wait untill the Render callsframeSynchronized()
functionframeSynchronized()
- Render function. Call to notify ECS that frame is sinchronized and it can process the next one.waitFrame()
- Render function. Wait while ECS calculates the next frame.exec()
- ECS function. At the end of the frame calculation sets the flag for thewaitFrame()
function
NOTE: You can call
reg->prepare()
function in the sync data step from the Render thread.
Example
Render thread: | ECS thread |
---|---|
reg.waitFrame(); | reg.syncWithRender(); |
sync data and filter Entities | -wait- |
reg.frameSynchronized(); | -wait- |
render data | reg.exec(); |
You can dispatch a separate job to work in background but you also need to sync it with your system and properly stop before the system is destroyed. You can override System::stop()
function for it.
struct MySystem final : BaseSystem {
//...
private:
ECS_JOB worker();
};
void MySystem::setup(Registry& reg) {
using namespace std::chrono_literals;
ECS_JOB_RUN(reg, MySystem::worker, 1s); //execute function every second until ECS_JOB_STOP
}
ECS_JOB MySystem::worker() {
// WARN: provide exit state in the `Stop` function to properly exit the application
if (done) {
return ECS_JOB_STOP;
}
return ECS_JOB_CONTINUE;
}
PVS-Studio - static analyzer for C, C++, C#, and Java code.