-
Notifications
You must be signed in to change notification settings - Fork 1
ECS
Our ECS is designed to be simple and fast. For our game we have a use case where we have a lot of entities, but those entities are relatively the same.
The API of the Ecs is relatively simple.
// Creation
EntityId Ecs::CreateEntity();
// Deletion
void Ecs::DestroyEntity(EntityId id);
// Validate EntityId
bool IsValid(EntityId id);
// Add component
template<T>
void Ecs::Assign(EntityId id, T&& cmp);
// Remove component
template<T>
void Ecs::Remove(EntityId id);
// Validate component(s)
template<Ts..>
bool Ecs::Has(EntityId id); //<-- Public API not exposed yet
// Get component
template<T>
T& Ecs::Get(EntityId id);
// Get a list of entities with the all specified components
template<Ts...>
vector<EntityId> Ecs::QueryEntities();
using namespace eyos;
using EyosEcs = Ecs<Transform, Model3D, InstancedModel, ecs_builtins::EcsTrackable>;
void Test() {
EyosEcs ecs{};
Material testMaterial{};
{
EntityId model = ecs.CreateEntity();
ecs.Assign(model, Transform{ glm::vec3{0, 0, 0}, glm::quat{ glm::vec3{} } });
ecs.Assign(model, Model3D{ bunnyMesh, &testMaterial });
}
{
EntityId model = ecs.CreateEntity();
ecs.Assign(model, Transform{ glm::vec3{25, 0, 0}, glm::quat{ glm::vec3{0, 0, 0} }, glm::vec3{ 0.25f } });
ecs.Assign(model, Model3D{ bunnyMesh, &testMaterial });
}
std::vector<EntityId> entityModels = ecs.QueryEntities<Transform, Model3D>();
for (EntityId id : entityModels) {
auto& transform = ecs.Get<Transform>(id);
auto& model = ecs.Get<Model3D>(id);
RenderModel(transform, model);
}
}
EntityId's are not guaranteed to stay valid.
The reason for this is:
In the ECS we optimize DestroyEntity(id)
by swapping to the end to avoid copying all other entities 1 place back.
This has as a consequence that any entity at the end of the entityArray
can be invalidated, care must be used when storing a EntityId for longer then 1 Query.
In the case that the ability to reference an entity is required, a solution exists: ecs_builtins::EcsTrackable
.
By Assign
ing this component to an entity. A sparseToDense entry will be kept in memory so in the case that the Entity will be swapped away from it's original index, the new index can be used.
This is often necessary for gameplay.
For instance, when selecting a unit, it's required that we keep track of the same entity. So, at runtime when the unit is selected, we can Assign<ecs_builtins::EcsTrackable>(entityId)
and be safe that the EntityId will be valid. When we deselect the unit, we can Remove<ecs_builtins::EcsTrackable>(entityId)
to remove the sparseToDense entry internally.
The Ecs only uses 1 dense entity vector. Which means that every entity will take up the space of all the components that can possibly fit on the entity. 0 sized structs will only be kept in the componentBitsets. And are thus a good fit for tagging entities.
The idea behind the EntityId is that it is just a number and a version packed into 1 struct. The id is (usually) just the index into the entity array. And the version is to check if the id is still valid. It looks approximately like this:
struct EntityId {
uint32_t index : 24;
uint8_t version;
};
The Ecs only uses 1 dense entity vector. 0 sized structs will only be kept in the componentBitsets. We can track an entity by bookkeeping a sparseToDense entry in a map.
At this time, the EntityId::index consists of 24 bits. This means that we can store a maximum of 2^24=16.777.216 entities. The reason for this is that we use the last 8 bits of the EntityId for keeping a version, thus leaving 32-8=24 bits for the index.
At this time, the componentBitsets are hard coded as uint16_t. This means that we cannot store more than 16 component types. Although this can be easily changed once we reach the limit.
At this time, we have no use for multiple of the same components per entity, so no thought has gone into the implementation to return multiple components of the same type (It would most likely give you the same component back twice).
It should be a multithreaded system
template<typename... Ts>
struct ComponentQuery {
std::tuple<std::vector<Ts>...> componentArrays;
auto begin();
auto end();
};
class SystemScheduler{
//TODO: Write out the public API
};
namespace eyos{
template<>
struct SystemThings<class MovementSystem> {
// We want to get a mutable ref to position, a readonly ref to velocity, and a copy of the health component.
using Query = Types<Position3D, const Velocity&, Health>;
using Dependencies = Types<>;
}
class MovementSystem : eyos::System<MovementSystem> {
using Super = eyos::System<MovementSystem>;
public:
void Init(World& world) override;
void Update(Super::SystemComponentQuery& query);
void Shutdown() override;
private:
World* world;
};
void MovementSystem::Update(Super::SystemComponentQuery& query) {
for(auto entityProxy : query) {
auto&&[pos, vel, health] = entityProxy;
if(health.health > 0)
pos += vel * world->time.GetDeltaTime();
}
}
}
int main() {
eyos::World world{};
eyos::SystemScheduler scheduler{};
scheduler.AddSystem(eyos::MovementSystem {});
while(true) {
scheduler.Update(world);
}
}