Entidy
is an Entity-Component-System
written in C++17.
Features:
- Separation between C++ types (
struct
orclass
) andComponents
- A querying language
- Automatic memory management
- Static or Dynamic build.
#include <iostream>
#include <entidy/Entidy.h>
using namespace std;
using namespace entidy;
struct Vec3 { int x, y, z; };
int main()
{
Entidy registry;
auto e = registry.Create();
registry.Emplace<Vec3>(e, "position");
registry.Emplace<Vec3>(e, "velocity");
auto view = registry.Select({"position", "velocity"})
.Having("position & velocity");
view.Each([&](Entity e, Vec3* pos, Vec3* vel)
{
cout << e << "\n";
});
return 0;
}
For more examples, refer to the examples
and benchmark
directories in this
repository.
The most evident advantage of separation of C++ types from components is reusing existing code.
As demonstrated in the example above, the system designer can create a sigle
type representing similar components without having to re-write
the types over and over; in our case Vec3
was used to represent both
position
and velocity
, which helps reduce clutter.
Likewise, it makes it possible to use primitive types and strings as components
directly, without necessarily wrapping them in structs for identification.
Given that Entidy
separates C++ types from components by design, it allows
system designers to create components on-the-fly.
...
auto component_name = GenerateFreshComponentNameSomehow();
registry.Emplace<Vec3>(e, component_name);
...
auto view = registry.Select({component_name})
.Having(component_name);
view.Each([&](Entity e, Vec3* x)
{
cout << e << "\n";
});
...
This case is useful for temporary components, such as flags (e.g. mark an
entity for deletion) and tags (e.g. mark an entity as is_enemy
).
Let's suppose we want to model an ownership relationship where one entity owns 1 or many other entities.
If we were to have C++ types as components, we would need to model the relationship inside the types themselves using composition: an owner entity would need to have a component with a vector of children entities, and children would need to reference their parent entitie(s) in their respective components.
This approach poses 2 main problems:
- The overhead of book-keeping and its maintenance on behalf of the developer, which is the product of the redundancy in representing those relationships.
- At least 2 queries need to be performed in order to produce a view that gives us access to the needed components.
Entidy
solves this elegantly by giving the ability to create components on
the fly to represent those relationships.
#include <iostream>
#include <string>
#include <entidy/Entidy.h>
using namespace std;
struct City { int id; };
struct Peasant { int id; };
int main()
{
Entidy registry;
auto city_1 = registry.Create();
auto peasant_1 = registry.Create();
auto peasant_2 = registry.Create();
registry.Emplace<City>(city_1, "city");
registry.Emplace<Peasant>(peasant_1, "peasant");
registry.Emplace<Peasant>(peasant_2, "peasant");
auto lives_in_city_1 = "lives_in_" + std::to_string(city_1);
registry.Emplace<bool>(peasant_1, lives_in_city_1);
registry.Emplace<bool>(peasant_2, lives_in_city_1);
auto view = registry.Select({})
.Having("peasant & " + lives_in_city_1);
...
return 0;
}
In order to select entities having exactly the components you're looking for,
components should figure in Select
and Having
as follows:
auto view = registry.Select({"position", "velocity"})
.Having("position & velocity");
Additional constaints can be added as such:
auto view = registry.Select({"position", "velocity"})
.Having("position & velocity & (!peasant | city)");
If you want to query for entities that may or may not have certain componenets,
you can ommit the optional components from Having
as such:
auto view = registry.Select({"position", "velocity"})
.Having("position");
In this case, the view will give you access to position
and velocity
,
however, you have to check whether the velocity
pointer is nullptr
before
accessing it.
view.Each([&](Entity e, Vec3* pos, Vec3* vel)
{
if(vel != nullptr)
cout << e << "\n";
});
The query language used in Having
has the following operators:
Operator | Meaning | Example |
---|---|---|
& |
AND | position & velocity |
| |
OR | position | velocity |
! |
NOT | position & !velocity |
Expressions can be nested using parenthesis as such:
position & velocity & !(city | peasant)
In some cases, the system designer would want to access query results by index,
as opposed to calling the Each
function and passing it a lambda.
Entidy
provides the At
function that works as follows:
...
auto view = registry.Select({"position", "velocity"})
.Having("position & velocity");
for(auto i = 0; i < view.Size(); ++i)
{
auto entity = view.At(i);
auto position = *view.At<Vec3, 0>(i);
auto velocity = *view.At<Vec3, 1>(i)
...
}
...
Entidy emphasizes performance when querying components. Our benchmarks (check
the benchmark
directory) shows that Entidy is comparable in performance to
existing ECS libraries like entt.
Entidy uses cmake
. You can specify the following options when building:
Option | Description | Default |
---|---|---|
ENTIDY_BUILD_EXAMPLES | Build the examples | ON |
ENTIDY_BUILD_BENCHMARK | Build the benchmarks | ON |
ENTIDY_BUILD_STATIC | Build a static library | ON |