Skip to content

Releases: richardbiely/gaia-ecs

v0.8.8

v0.8.7

23 Sep 17:00
9b03c83
Compare
Choose a tag to compare

This is an important bug-fix release with some interesting internal changes.

First and foremost, despite all the unit tests and memory sanitization, some nasty bugs made it through which have been addressed now. Most of them were logical errors, some were more involved, though. The list of fixes is quite extensive so make sure to update. The core should be very stable now.

A few smaller features were added to the project to make your live easier.

Among them, implicit pairs:

ecs::World w;
ecs::Entity e = w.add();
// Old way of adding a pair to an entity e
w.add(e, ecs::Pair(start, pos));
// You can use a simpler notation now
w.add(e, {start, pos});

Component layouts are now defined via GAIA_LAYOUT. The previous version has been deprecated and is no longer recommended:

struct Position {
  GAIA_LAYOUT(SoA); // Treat this component as SoA (structure of array)
  float x, y, z;
};

The query engine started transitioning to a virtual machine. What this is means is that before the query is first run, it breaks the process into a sequence of specialized instructions in the process called compilation. This eliminates the need to run some logic every time a new archetypes is encountered and makes queries more efficient. The virtual machine is going to keep improving over time and will make queries even faster. It will also make it easier to extend the query engine significantly.
One big new feature, although still only in preview, is the introduction of systems as entities. An undocumented way to use systems until now was:
class MoveSystem final: public ecs::System {
  ecs::Query m_q;

public:
  void OnCreated() override {
    m_q = world().query().all<Position&, Velocity>();
  }

  void OnUpdate() override {
    // Update position based on current velocity
    m_q.each([&](Position& p, const Velocity& v) {
      p.x += v.x;
      p.y += v.y;
    });
  }
};

...
ecs::World w;
ecs::SystemManager sm(w);
// Register the system in the manager
auto& mySystem = sm.add<MoveSystem>();
// Disable the system
mySystem.enable(false);
// Enable the system again
mySystem.enable(true);

while (!terminate_program) {
   // Run all systems on the manager
   sm.update();
   // Handle various world-specific stuff (GC, defragmentation, finalize deletion etc.)
   w.update();
}

Systems can now be defined as entities:

SystemBuilder mySystem = w.system()
  .all<Position&, Velocity>()
  // Logic to execute every time the system is invoked.
  .on_each([](Position& p, const Velocity& v) {
    p.x += v.x * dt;
    p.y += v.y * dt;
    p.z += v.z * dt;
  });


// Retrieve the entity representing the system.
Entity mySystemEntity = mySystem.entity();
// Disable the entity. This effectively disables the system.
w.disable(mySystemEntity);
// Enable the entity. This effectively makes the system runnable again.
w.enable(mySystemEntity);

// The system can be run manually ...
mySystem.exec();
// ... or it can be run as the world seems fit
w.update();

This will allow systems to be queried and create complex relationships among them. For instance, you will be able to tell "give me all systems that operate on XYZ and turn them off", or "give me all systems which are a dependency of this one". This has many practical applications in debugging, monitoring and more which we will get into once the system is fully implemented.


Another preview feature is the introduction of a new storage type for components. So far, only archetype storage has been used. Although great in many things, archetypes are a bit slower when it comes to adding/removing components because content of all entities belonging to archetype A needs to be moved to archetype B. To mitigate this issue a new sparse storage made its way into the project. A packed array or a sparse set storage can be used to store components which are likely to be added and removed or if stability of reference is required. Components living inside the sparse storage never move and thus they are fast to add or remove.
struct SparseComponent {
  GAIA_LAYOUT(Sparse); // Store the component in a sparse storage
  uint32_t id;
  uint32_t data;
};
// Define a comparison operator
bool operator==(const SparseTestItem& a, const SparseTestItem& b) {
	return a.id == b.id && a.data == b.data;
}
// Define to_sparse_id function which extracts a unique id among all SparseComponents
template <>
struct gaia::cnt::to_sparse_id<SparseTestItem> {
  static sparse_id get(const SparseTestItem& item) noexcept {
    return item.id;
  }
};

Data structures are implemented, they are not connected to Gaia yet, though. I will write more about this feature and its implications once it fully lands. For now, it can be used like this:

cnt::sparse_storage<PositionSparse> arr;
arr.add(SparseComponent{100, 0});
arr.add(SparseComponent{200, 1});
const bool has100 = arr.has(100); // true
arr.del(100);
const bool has100 = arr.has(100); // false
const auto data = arr.operator[200].data; // returns 1

Release notes

Fixes:
Fixed: Invalid discord link
Fixed: Dangling archetypes possible in the entity->archetype map
Fixed: Build issues with certain preprocessor macro combinations
Fixed: Missing restrict keyword
Fixed: ThreadPool would not handle spurious wake ups
Fixed: shift_elements_right_aos(_n) not working correctly for all kind of types
Fixed: Exclusive NO queries regression
Fixed: Matching of wildcard queries
Fixed: Matching of NO queries
Fixed: Proper return value resolution for container functions front and back
Fixed: Dummy example build + stack allocator on MSVC
Fixed: Out-of-bounds array access in duel
Fixed: Unaligned free used after aligned alloc
Fixed: Empty chunks and archetypes deleted again
Fixed: CantCombine not working properly
Fixed: move_elements_aos would use a const pointer for the address to move from
Fixed: Wrong path in build_clang.sh
Fixed: "no" query operation would not match Is relationships properly
Fixed: world::is and world::in did the opposite of what they should
Fixed: Reliance on an undefined behavior in the unit test
Fixed: Incorrect destructor called when swapping entities inside chunks
Fixed: Wrong memory written to when adding components
Fixed: Make sure docker script files are always in linux format
Fixed: Unused variable that could trigger a warning

Changes:
Changed: ecs::DependsOn –> ecs::Requires
Changed: Use mold/gold linkers when available by default because they perform better than the default linker for clang/GNU
Changed: Dont use mold on Apple
Changed: Catch2 updated to v3.6.0
Changed: Tracy updated to version 0.11.1

Tweaks:
Tweaked: Simplified check for (,) pairs in World::has
Tweaked: Query size update
Tweaked: Further reduction of Query size. Serialization buffer made temporary and moved to World.
[Tweaked: Query invalidation made automatic based on the com...

Read more

v0.8.6

08 Apr 07:28
7d8dfdb
Compare
Choose a tag to compare

This is a maintenance release bringing mostly bug fixes and internal improvements.

New features are of internal nature. In order to have more control over allocations, the project switched to using allocators for everything (we already had ChunkAllocator for handling allocation of memory for archetype chunks). This way, every allocation can be monitored and if necessary, allocation strategy for any given place in the project can be easily changed. Also, this gives the user the ability to override the default allocation with a custom one if needed.

The project supports allocation tracing via Tracy. This gives us the ability to see where any allocation comes from and how the situation changes over time:
tracy_1

Release notes

Fixed:

  • added fallback for setting thread name on Win (e.g. if Win10 is not the target platform) 6de6982
  • build with profiler enabled dd6c809
  • GAIA_LOG_W/E would not output to stderr entirely 08a0fbe
  • possible build errors d55f235
  • ::emplace_back would not return a reference 1e1d22a
  • ecs::call_ctor would not work with braces initialization 1e1d22a
  • incorrect ranges used for indexed view/view_mut dcd9931
  • ecs::Core no longer excluded by default on queries 5b05dba
  • remapping not applied properly for chunk batching 0b3ef53
  • numerical overflow in the app example d2db469

Changed:

  • Picobench and Catch2 version bump b53fdb4
  • all Gaia-ECS allocations go through internal DefaultAllocator by default 58a17a0
  • benchmarks will no longer generate output in profiling mode 5ac6adb
  • use 64b hash for entity lookup 1820ff4
  • World::has_none -> has_no 5b05dba

Tweaked:

Added:

  • DefaultAllocator 58a17a0
  • StackAllocator fcdb1ea
  • support for custom allocator on containers 58a17a0
  • support for named allocation (currently exposed for profiling sessions only) 98b92b0
  • support for mutex profiling 278e757
  • World::set_max_lifespan to change how long an empty archetype stays alive until deleted 506efc5

Full Changelog: v0.8.5...v0.8.6

v0.8.5

11 Mar 18:59
f9eafc9
Compare
Choose a tag to compare

This release brings many stability improvements. Memory and address sanitisation tests were extended and as a result new bugs could be discovered and fixed. Internal support for SoA types was brought 99% on-par with ordinary AoS structures.

New features

Iterator view index

Performance of views can be improved slightly by explicitly providing the index of the component in the query.

ecs::Query q = w.query();
q.any<Something>().all<Position&, Velocity>();

q.each([](ecs::IterAll& it) {
  auto s = it.view<Somehting>(0); // Something is fhe first defined component in the query
  auto p = it.view_mut<Position>(1); // Position is the second defined component in the query
  auto v = it.view<Velocity>(2); // Velocity is the third defined component in the query
  ....
}

Batched entity creation

Many entities can be added or duplicated at once, now. This is more performant than creating entities one by one.

// Create 1000 empty entities
w.add(1000);
w.add(1000, [](Entity newEntity) {
  // Do something with the new entity
  // ...
})

// Create an entity with Position and Velocity.
ecs::Entity e = w.add();
w.add(e, position, Position{0, 100, 0});
w.add(e, velocity, Position{0, 0, 1});

// Create 1000 more entites like "e".
// Their component values are not initilazed to any particular value.
w.add_n(e, 1000);
w.add_n(e, 1000, [](Entity newEntity) {
  // Do something with the new entity
  // ...
});

// Create 1000 more entities like "e".
// Their component values are going to be the same as "e".
w.copy_n(e, 1000);
w.copy_n(e, 1000, [](Entity newEntity) {
  // Do something with the new entity
  // ...
});

New Contributors

  • @abeimler made their first contribution in #20 - this is the first-ever community contributor :)

Release notes

Fixed:

  • compiling Errors by @abeimler in #20
  • various possible compilation issues on different compilers 18d55d7 69d7ea1
  • component comparison fallback function would only compare one byte 541ade3
  • reset thread handles when changing the number of workers 91524bb
  • variable hiding in Roguelike example a16300b
  • broken member function detection a813211
  • broken reflection for size+data serialization 6e09cb0
  • incorrectly done unit test check for serialization 6e09cb0
  • wrong way to delete chunk memory with GAIA_ECS_CHUNK_ALLOCATOR disabled 4601dd4
  • uninitialized chunk memory when building with GAIA_ECS_CHUNK_ALLOCATOR disabled b3e3ae0
  • component cache items never deleted 4601dd4
  • wrong paths in build_clang.sh ec4d7b6
  • span not throwing asserts 1d65f73
  • data alignment in containers 1f32ccd
  • use launder to make sure unnecessary optimizations don't happen to pointers inside placement new area 9364cf7
  • defragmentation wouldn't move data around 5eae076
  • defragmetantion could cause a crash 5eae076
  • World::cleanup leaves the world in an unusable state ea0f0e6 e3255d9
  • full SoA support brought up-to-date with AoS 02d51ea cca7f6f

Changed/tweaked:

  • no need for entity record lookup for new entities d344cb5
  • entity creation modified df5d73b
  • ECS iterators passed by reference from now on 59be1d2 5354961 295a171
  • Improved performance of adding/moving entities 7f6ce62
  • World::bulk renamed to World::build 1c94118)
  • World::set(Entity) renamed to World::acc_mut(Entity) 1c94118
  • World::get(Entity) renamed to World::acc(Entity) 1c94118
  • entity setters now return a reference to data (AoS) or the data accessor object (SoA) 1c94118 73913af
  • extended sanitization 4601dd4
  • ComponentCache::clear made hidden from users ac198f4
  • archetypes keep their index in the list for fast lookup during removals 0d3a770
  • simplified archetypes tracking during defragmentation 5eae076

Added:

Full Changelog: v0.8.4...v0.8.5

v0.8.4

20 Feb 17:57
63c1ea3
Compare
Choose a tag to compare

This is a critical fix version addressing a build issue on Windows platform.

Fixed:

Full Changelog: v0.8.3...v0.8.4

v0.8.3

18 Feb 18:39
c634eb8
Compare
Choose a tag to compare

This release brings some important stability fixes and powerful new features.

Compile time pairs

Previously, to define a pair, you needed to use the entity id.

struct Likes{};
struct Carrot{};
ecs::Entity likes = w.add<Likes>.entity;
ecs::Entity carrot = w.add<Carrot>.entity;
ecs::Query q = w.query().all( ecs::Pair(likes, carrot) );

A shorter version exists now:

struct Likes{};
struct Carrot{};
ecs::Query q = w.query().all( ecs::pair<Likes, Carrot>() );

Pairs with data

Pairs do not need to be formed from tag entities only anymore. You can use components to build a pair which means they can store data, too!
To determine the storage type of Pair(relation, target) the following logic is applied:

  1. if "relation" is non-empty, the storage type is rel.
  2. if "relation" is empty and "target" is non-empty, the storage type is "target".
struct Start{};
struct Position{ int x, y; };
...
ecs::Entity e = w.add();
// Add (Start, Position) from component entities.
ecs::Entity start_entity = w.add<Start>.entity;
ecs::Entity pos_entity = w.add<Position>.entity;
w.add(e, ecs::Pair(start_entity, pos_entity));
// Add (Start, Position) pair to entity e using a compile-time component pair.
w.add<ecs::pair<Start, Position>(e);
// Add (Start, Position) pair to entity e using a compile-time component pair
// and set its value. According the rules defined above, the storage type used
// for the pair is Position.
w.add<ecs::pair<Start, Position>(e, {10, 15});

// Create a query matching all (Start, Position) pairs using component entities
ecs::Query q0 = w.query().all( ecs::Pair(start_entity, pos_entity) );
// Create a query matching all (Start, Position) pairs using a compile-time structure
ecs::Query q1 = w.query().all< ecs::pair<Start, Position> >();

Fixed target queries (first iteration)

It is possible to target multiple source entities from a query now.
This is a feature demo, it is very limited in usage at the moment.

struct Foo { int val; };
ecs::Entity src = w.add();
e.add<Foo>({10});

ecs::Entity foo = w.add<Foo>();
ecs::Query q = w.query().all(foo, src).all<Position>();
q.each([](ecs::Entity entity) {
  // runs for each entity with Position, but only if there is an entity "src" with Foo on it
});

Inheritance (first iteration)

Entities can inherit from other entities by using the (Is, target) relationship. This is a powerful feature that helps you identify an entire group of entities using a single entity.

ecs::World w;
ecs::Entity animal = w.add();
ecs::Entity herbivore = w.add();
ecs::Entity rabbit = w.add();
ecs::Entity hare = w.add();
ecs::Entity wolf = w.add();

w.as(wolf, animal); // equivalent of w.add(wolf, ecs::Pair(ecs::Is, animal))
w.as(herbivore, animal); // equivalent of w.add(herbivore, ecs::Pair(ecs::Is, animal))
w.as(rabbit, herbivore); // equivalent of w.add(rabbit, ecs::Pair(ecs::Is, herbivore))
w.as(hare, herbivore); // equivalent of w.add(hare, ecs::Pair(ecs::Is, herbivore))

bool herbibore_is_animal = w.is(herbivore, animal); // true
bool rabbit_is_herbivore = w.is(rabbit, herbivore); // true
bool rabbit_is_animal = w.is(rabbit, animal); // true
bool animal_is_rabbit = w.is(animal, rabbit); // false
bool wolf_is_herbivore = w.is(wolf, herbivore); // false
bool wolf_is_animal = w.is(wolf, animal); // true

// Iterate everything that is animal
ecs::Query q = w.query().all(Pair(ecs::Is, animal));
q.each([](ecs::Entity entity) {
  // runs for herbivore, rabbit, hare and wolf
});

// Iterate everything that is animal but skip wolfs
ecs::Query q2 = w.query().all(Pair(ecs::Is, animal)).no(wolf);
q2.each([](ecs::Entity entity) {
  // runs for herbivore, rabbit, hare
});

Exclusive no queries

Until now, queries composed of "no" terms were not accepted. This restriction has now been lifted.

ecs::Query q = w.query().no<Position>();
q.each([&](ecs::Iter iter) {
  // Runs for each entity without Position
  ...
  ...
});

Job system improvements

Priorities

Nowdays, CPUs have multiple cores. Each of them is capable of running at different frequencies depending on the system's power-saving requirements and workload. Some CPUs contain cores designed to be used specifically in high-performance or efficiency scenarios. Or, some systems even have multiple CPUs.

Therefore, it is important to have the abilitly to utilize these CPU features with the right workload for our needs. Gaia-ECS allows jobs to be assigned a priority tag. You can either create a high-priority jobs (default) or low-priority ones.

The operating system should try to schedule the high-priority jobs to cores with highest level of performance (either performance cores, or cores with highest frequency etc.). Low-priority jobs are to target slowest cores (either efficiency cores, or cores with lowest frequency).

Where possible, the given system's QoS is utilized (Windows, MacOS). In case of operating systems based on Linux/FreeBSD that do not support QoS out-of-the-box, thread priorities are utilized.

Note, thread affinity is not managed and left on default because this plays better with QoS and gives the operating system more control over scheduling.

// Create a job designated for performance cores
mt::Job job0;
job0.priority = JobPriority::High;
job0.func = ...;
tp.sched(job0);

// Create a job designated for efficiency cores
mt::Job job0;
job0.priority = JobPriority::Low;
job0.func = ...;
tp.sched(job0);

Threads

The total number of threads created for the pool is set via ThreadPool::set_max_workers. By default, the number of threads created is equal to the number of available CPU threads minus 1 (the main thread). However, no matter have many threads are requested, the final number if always capped to 31 (ThreadPool::MaxWorkers). You number of available threads for you hardware can be retrived via ThreadPool::hw_thread_cnt.

auto& tp = mt::ThreadPool::get();

// Get the number of hardware threads.
const uint32_t hwThreads = tp.hw_thread_cnt();

// Create "hwThreads" worker threads. Make all of them high priority workers.
tp.set_max_workers(hwThreads, hwThreads);

// Create "hwThreads" worker threads. Make 1 of them a high priority worker.
// If more then 1 worker threads were created, the rest of them is dedicated
// for low-priority jobs.
tp.set_max_workers(hwThreads, 1);

// No workers threads are used. If there were any before, they are destroyed.
// All processing is happening on the main thread.
tp.set_max_workers(0, 0);

The number of worker threads for a given performance level can be adjusted via ThreadPool::set_workers_high_prio and ThreadPool::set_workers_low_prio. By default, all workers created are high-priority ones.

auto& tp = mt::ThreadPool::get();

// Get the number of worker threads available for this system.
const uint32_t workers = tp.workers();

// Set the number of worker threads dedicted for performance cores.
// E.g. if workers==5, this dedicates 4 worker threads for high-performance workloads
// and turns the remaining 1 into an efficiency worker.
tp.set_workers_high_prio(workers - 1);

// Make all workers high-performance ones.
tp.set_workers_high_prio(workers);

// Set the number of worker threads dedicted for efficiency cores.
// E.g. if workers==5, this dedicates 4 worker threads for efficiency workloads loads
// and turns the remaining 1 into a high-performance worker.
tp.set_workers_low_prio(workers - 1);

// Make all workers low-performance ones.
tp.set_workers_low_prio(workers);

The main thread normally does not participate as a worker thread. However, if needed, it can join workers by calling ThreadPool::update from the main thread.

auto& tp = mt::ThreadPool::get();

ecs::World w1, w2;
while (!stopProgram) {
  // Create many jobs here
  // ...

  // Update both worlds
  w1.update();
  w2.update();

  // Help execute jobs on the main thread, too
  tp.update();
}

If you need to designate a certain thread as the main thread, you can do it by calling ThreadPool::make_main_thread from that thread.

Note, the operating system has the last word here. It might decide to schedule low-priority threads to high-performance cores or high-priority threads to efficiency cores depending on how the scheduler decides it should be.

Release notes

Fixed:

  • hashmap element cloning 70d38e6
  • archetype graph not properly cleaned when deleting entities b68b668
  • adding an entity to another entity would not create a new archetype 6bbe350
  • entity deletion safety 6a23a22
  • ecs::pair would not be able to discern gen & uni entities 5b8efde
  • duplicates archetype matches in cache after a new archetype has been added in between queries 92198c5
  • accessing released memory possible when deleting chunks b1abdd5
  • make compile-time type-info strings match on supported compilers c634eb8

Changed/Tweaked:

  • use direct hashing for id->archetype maps b19a090
  • improved component diags 254abed

Added:

Read more

v0.8.2

16 Dec 06:10
9df49b3
Compare
Choose a tag to compare

This is a maintenance release with a few small new features at the top.

Most of fixes and tweaks are related to CMake and Docker.

Iteration benchmark was rewritten so it does not generate thousands of various compile-time recursive operations to improve compilation times and ease of use (80s along with linking down to insignificant amount of time).

Also, starting with this release, sponsorship has became available:
https://github.com/sponsors/richardbiely
Therefore, if you like the project and want to support it, give it a star or became a sponsor. Thank you everyone, you rock!

Entity dependencies

Defining dependencies among entities is made possible via the (DependsOn, target) relationship.

When adding an entity with a dependency to some source it is guaranteed the dependency will always be present on the source as well. It will also be impossible to delete it.

ecs::World w;
ecs::Entity rabbit = w.add();
ecs::Entity animal = w.add();
ecs::Entity herbivore = w.add();
ecs::Entity carrot = w.add();
w.add(carrot, ecs::Pair(ecs::DependsOn, herbivore));
w.add(herbivore, ecs::Pair(ecs::DependsOn, animal));

// carrot depends on herbivore so the later is added as well.
// At the same time, herbivore depends on animal so animal is added, too.
w.add(rabbit, carrot);
const bool isHerbivore = w.has(rabbit, herbivore)); // true
const bool isAnimal = w.has(rabbit, animal); // true

// Animal will not be removed from rabbit because of the dependency chain.
// Carrot depends on herbivore which depends on animal.
w.del(rabbit, animal); // does nothing
// Herbivore will not be removed from rabbit because of the dependency chain.
// Carrot depends on herbivore.
w.del(rabbit, herbivore); // does nothing

// Carrot can be deleted. It requires herbivore is present which is true.
w.del(rabbit, carrot); // removes carrot from rabbit

Entity constraints

Entity constrains are used to define what entities cannot be combined with others. Constraints are defined via the (DependsOn, target) relationship.

ecs::World w;
ecs::Entity weak = w.add();
ecs::Entity strong = w.add();
w.add(weak, ecs::Pair(ecs::CantCombine, strong));

ecs::Entity e = w.add();
w.add(e, strong);
// This is an invalid operation.
w.add(e, weak);

GAIA_DEVMODE

Developer mode is a macro that enables more integrity checks for various internal operations. Some of them are expensive and almost redundant but keep the code safe from regressions and various hard-to-locate bugs. However, they are not really useful for anyone but library maintainers. Therefore, they are now enabled only when GAIA_DEVMODE macro is on:

#define GAIA_DEVMODE 1
#include <gaia.h>

Release notes

Fixed:

  • World::relation(s) not returning proper entities 826043d

  • wildcard relationship deletion - 30af94d

  • certain CMake settings not reflected in generate_compile_commands target dcf6c58

  • crash when accessing EntityDesc of a pair (only regular entities have it) 30af94d

  • crash when copying an entity with entities attached to it 12757c7

  • crash when using diagnostics with an entity without the EntityDesc 5b145e0

  • asserts handling with GAIA_DISABLE_ASSERTS 5a2aef1

  • uninitialized variables on Archetype 8e938db

  • darr_ext move constructor ed99fd6

  • non-Xcode build workaround for MacOS with Xcode 15 or later installed 83a1a33

  • benchmark CMake configuration 689cabb

  • string argument expansion for Docker builds 689cabb

  • the correct compiler is used always for Docker builds 79717d3

Changed:

  • OnDeleteTarget behavior changed fbe6e10
  • "optional" for string queries uses ? instead of + now 2dc3b8e
  • IteratorXYZ renamed to IterXYZ 63a53af
  • handle serialization asserts at compile-time to avoid issues with older compilers 7075135
  • picobench for benchmarking made an external CMake dependency 26687dd
  • Catch2 updated to 3.5.0 10564cf
  • iIteration benchmark updated ccb8bf0 d9ab565

Tweaked:

  • improved query matching performance for simple queries 930e86f
  • one less hash combination when calculating the query hash 59b6a11
  • QOL improvement for IterAll 539d000

Added:

  • support for Pair(DependsOn, X) 27ff060
  • support for Pair(CantCombine, X) 28128a7 5a2aef1
  • GAIA_DEVMODE for library maintainers 63d7226
  • more configuration options to CMake 689cabb

Full Changelog: v0.8.1...v0.8.2

v0.8.1

04 Dec 19:34
04475c1
Compare
Choose a tag to compare

This is a critical fix addressing wrongly sorted internal Query data structures.
Additionally, it changes how data is matched. User-created queries will no longer match special core components.

A new cleanup rule was added that tells the framework a certain entity is not allowed to be deleted. They can be used by adding a relationship pair (OnDelete, Error) and/or (OnDeleteTarget, Error) to an entity.

ecs::World w;
ecs::Entity rabbit = w.add();
ecs::Entity eats = w.add();
ecs::Entity carrot = w.add();
w.add(rabbit, ecs::Pair(eats, carrot));

w.add(carrot, ecs::Pair(OnDelete, Error));
w.del(carrot); // This asserts and reports errors!

w.add(carrot, ecs::Pair(OnDeleteTarget, Error));
w.del(rabbit, ecs::Pair(eats, carrot)); // This asserts and reports errors!

Release notes

Fixed:

  • Wrong query items ordering 7d99342

Changed:

  • Changed: Core components can't be deleted 04475c1
  • Changed: Special core components won't be matched by user queries 04475c1

Added:

  • (OnDelete[Target], Error) to prevent entities from getting deleted 04475c1

Full Changelog: v0.8.0...v0.8.1

v0.8.0

04 Dec 13:34
4f84a0b
Compare
Choose a tag to compare

This release brings basic support for entity relationships. It is a big thing as is going to take a few more releases to make it great but that can wait. Let us unwrap what an entity relationship actually is.

Entity relationship

Entity relationship is a feature that allows users to model simple relations, hierarchies or graphs in an ergonomic, easy and safe way.
Each relationship is expressed as following: "source, (relation, target)". All three elements of a relationship are entities. We call the "(relation, target)" part a relationship pair.

Relationship pair is a special kind of entity where the id of the "relation" entity becomes the pair's id and the "target" entity's id becomes the pairs generation. The pair is created by calling ecs::Pair(relation, target) with two valid entities as its arguments.

Adding a relationship to any entity is as simple as adding any other entity.

ecs::World w;
ecs::Entity rabbit = w.add();
ecs::Entity hare = w.add();
ecs::Entity carrot = w.add();
ecs::Entity eats = w.add();

w.add(rabbit, ecs::Pair(eats, carrot));
w.add(hare, ecs::Pair(eats, carrot));

ecs::Query q = w.query().all(ecs::Pair(eats, carrot));
q.each([](ecs::Entity entity)) {
  // Called for each entity implementing (eats, carrot) relationship.
  // Triggers for rabbit and hare.
}

Wildcard queries

This by itself would not be much different from adding entities/component to entities. After all, a similar result can be achieved by creating a "eats_carrot" tag and assigning it to "hare" and "rabbit". What sets relationships apart is the ability to use wildcards in queries.

There are three kinds of wildcard queries possible:

  • ( X, * ) - X that does anything
  • ( * , X ) - anything that does X
  • ( * , * ) - anything that does anything (aka any relationship)

The "*" wildcard is expressed via All entity.

w.add(rabbit, ecs::Pair(eats, carrot));
w.add(hare, ecs::Pair(eats, carrot));
w.add(wolf, ecs::Pair(eats, rabbit));

ecs::Query q1 = w.query().all(ecs::Pair(eats, All));
q1.each([]()) {
  // Called for each entity implementing (eats, *) relationship.
  // This can be read as "entity that eats anything".
  // Triggers for rabbit, hare and wolf.
}

ecs::Query q2 = w.query().all(ecs::Pair(All, eats));
q2.each([]()) {
  // Called for each entity implementing (*, carrot) relationship.
  // This can be read as "anything that has something with carrot".
  // Triggers for rabbit and hare.
}

ecs::Query q3 = w.query().all(ecs::Pair(All, All));
q3.each([]()) {
  // Called for each entity implementing (*, *) relationship.
  // This can be read as "anything that does/has anything".
  // Triggers for rabbit, hare and wolf.
}

Relationships can be ended by calling World::del (just like it is done for regular entities/components).

// Rabbit no longer eats carrot
w.del(rabbit, ecs::Pair(eats, carrot));

Whether a relationship exists can be check via World::has (just like it is done for regular entities/components).

// Checks if rabbit eats carrot
w.has(rabbit, ecs::Pair(eats, carrot));
// Checks if rabbit eats anything
w.has(rabbit, ecs::Pair(eats, All));

A nice side-effect of relationship is they allow for multiple components/entities of the same kind be added to one entity.

// "eats" is added twice to the entity "rabbit"
w.add(rabbit, ecs::Pair(eats, carrot));
w.add(rabbit, ecs::Pair(eats, salad));

Targets

Targets of a relationship can be retrieved via World::target.

ecs::World w;
ecs::Entity rabbit = w.add();
ecs::Entity carrot = w.add();
ecs::Entity salad = w.add();
ecs::Entity eats = w.add();

w.add(rabbit, ecs::Pair(eats, carrot));
w.add(rabbit, ecs::Pair(eats, salad));

// Returns carrot (carrot entity was created before salad)
ecs::Entity first_target = w.target(rabbit, eats);

// Appends carrot and salad entities to the array
cnt::sarr_ext<ecs::Entity, 32> what_rabbit_eats;
w.target(rabbit, eats, what_rabbit_eats);

Cleanup rules

When deleting an entity we might want to define how the deletion is going to happen. Do we simply want to remove the entity or does everything connected to it need to get deleted as well? This behavior can be customized via relationships called cleanup rules.

Cleanup rules are defined as ecs::Pair(Condition, Reaction).

Condition is one of the following:

  • OnDelete - deleting an entity/pair
  • OnDeleteTarget - deleting a pair's target

Reaction is one of the following:

  • Remove - removes the entity/pair from anything referencing it
  • Delete - delete everything referencing the entity

The default behavior of deleting an entity is to simply remove it from the parent entity. This is an equivalent of Pair(OnDelete, Remove) relationship pair attached to the entity getting deleted.
Additionally, a behavior which can not be changed, all relationship pairs formed by this entity need to be deleted as well. This is needed because entity ids are recycled internally and we could not guarantee that the relationship entity would be be used for something unrelated later.

ecs::Entity rabbit = w.add();
ecs::Entity hare = w.add();
ecs::Entity eats = w.add();
ecs::Entity carrot = w.add();
w.add(rabbit, ecs::Pair(eats, carrot));
w.add(hare, ecs::Pair(eats, carrot));

// Delete the rabbit. Everything else is unaffected.
w.del(rabbit);
// Delete eats. Deletes eats and all associated relationships.
w.del(eats); 

Creating custom rules is just a matter of adding the relationship to an entity.

ecs::Entity bomb_exploding_on_del = w.add();
w.add(bomb_exploding_on_del, ecs::Pair(OnDelete, Delete));

// Attach a bomb to our rabbit
w.add(rabbit, bomb_exploding_on_del);

// Deleting the bomb will take out all entities associated with it along. Rabbit included.
w.del(bomb_exploding_on_del); 

A native ChildOf entity is defined that can be used to express a physical hierarchy. It defines a (OnDelete, Delete) relationship so if the parent is deleted, all the children all deleted as well.

Define Query from string

Queries can be constructed using a string notation now. This allows you to define the entire query or its parts using a string composed of simple expressions. Any spaces in between modifiers and expressions are trimmed.

Supported modifiers:

  • ; - separates expressions
  • + - query::any
  • ! - query::none
  • & - read-write access
  • %e - entity value
  • (rel,tgt) - relationship pair, a wildcard character in either rel or tgt is translated into All
// Some context for the example
struct Position {...};
struct Velocity {...};
struct RigidBody {...};
struct Fuel {...};
auto player = w.add();
ecs::Entity player = w.add();

// Create the query from a string expression.
ecs::Query q = w.query()
  .add("&Position; !Velocity; +RigidBody; (Fuel,*); %e", player.value());

// It does not matter how we split the expressions. This query is the same as the above.
ecs::Query q1 = w.query()
  .add("&Position; !Velocity;")
  .add("+RigidBody; (Fuel,*)")
  .add("%e", player.value());

// The queries above can be rewritten as following:
ecs::Query q2 = w.query()
  .all<Position&>()
  .none<Velocity>()
  .any<RigidBody>()
  .all(ecs::Pair(w.add<Fuel>().entity, All)>()
  .all(player);

Release notes

Fixed:

  • darr_ext move constructor and move assignment operator 59b2ac7
  • sarr_ext::back() would return the item at the index capacity()-1 rather than size()-1 3ce995f)

Changed:

  • Prevent accidental Entity construction from integers aedb16e
  • LookupKey constructors made explicit to avoid incorrect type construction c881274

Tweaked:

  • Improved data movement performance when deleting entities 395b0b5

Added:

  • Support for simple relationships 09753c8
  • Support for wildcard relationships (X, * ), ( * , X) and ( * , * ) 3d53209
  • Support for defining queries via a string d7a29f4
  • Support for entity cleanup rules 456dc1d
  • Implicit ChildOf entity representing a physical hierarchy 2dba671
  • ecs::World::target to retrieve a list of relationship targets for a given source entity ba21c9e

Full Changelog: v0.7.9...v0.8.0

v0.7.9

26 Nov 19:33
ab43ecf
Compare
Choose a tag to compare

This release brings some important fixes and delivers even bigger features.

The long awaited transition towards components being entities has now been finished. This helped simplify the project internally and provided groundwork for future expansions. As a result, new template-free version of many API functions come to be.

The hottest new feature is the ability to define tags at run-time.

ecs::World w;
ecs::Entity player0 = w.add();
ecs::Entity player1 = w.add();
ecs::Entity player2 = w.add();
ecs::Entity teamA = w.add();
ecs::Entity teamB = w.add();
// Add player0 and player1 to teamA
w.add(player0, teamA);
w.add(player1, teamA);
// Add player2 to teamB
w.add(player2, teamB);

Components can now be registered easily via the new API.

const ecs::EntityComponentInfo& ci_position = w.add<Position>();
const ecs::EntityComponentInfo& ci_velocity = w.add<Velocity>();

This is most often going to be used with the new entity-based API for queries. All versions of the API can be combined together.

ecs::Entity p = w.add<Position>().entity;
ecs::Entity v = w.add<Velocity>().entity;

// Low-level API
ecs::Query q = w.query()
  // Position, read-write access
  .add({p, QueryOp::All, QueryAccess::Write}) 
  // Velocity, read-only access
  .add({v, QueryOp::Any, QueryAccess::Read}); 
q.each([&](ecs::Iterator iter) {
  ...
});

// Shortcut
ecs::Query q = w.query()
  // Position, read-write access
  .all(p, true)
  // Velocity, read-only access
  .any(v);
q.each([&](ecs::Iterator iter) {
  const bool hasVelocity = iter.has(v);
  ...
});

// Compile-time API
ecs::Query q = w.query()
  // Position, read-write access
  .all<Position&>()
  // Velocity, read-only access
  .any<Velocity>()
q.each([&](ecs::Iterator iter) {
  const bool hasVelocity = iter.has(v);
  ...
});

When editing the archetype of your entity it is now possible to manually commit all changes by calling ecs::CompMoveHelper::commit(). This is useful in scenarios where you have some branching and do not want to duplicate your code for both branches or simply need to add/remove components based on some complex logic. Commit is also called at the end of the object's scope automatically. After the call the object is reset to its default state.

ecs::CompMoveHelper builder = w.bulk(e);
builder
  .add<Velocity>()
  .del<Position>()
  .add<Rotation>();
if (some_conditon) {
  bulider.add<Something1, Something2, Something3>();
}
builder.commit();

A similar feature has come to the bulk-setter object which can be used in scenarios with complex logic now as well.

auto setter = w.set(e);
setter.set<Velocity>({0, 0, 2});
if (some_condition)
  setter.set<Position>({0, 100, 0});
setter.set...;

That is all for now. Enjoy the new features. Most important changes are listed bellow.

Fixed:

  • ComponentDesc::func_swap swapping incorrectly 0a6db45
  • enabling entities 354ad39
  • moving entities when changing archetypes 354ad39
  • getters not deducing the return type properly (dropping the reference) 354ad39
  • entity record index overriden when adding entites 354ad39
  • ilist::alloc with context not handling allocs correctly 9d27b5f
  • Entity::operator< would only consider the id part of the entire value 9bc32c1

Changed:

  • ComponentCache made local to ecs::world a1095ae
  • components as entities 480b62d 49e9f5d
  • minimum alignment of non-empty types set to 4/8 bytes (32/64-bit systems) 480b62d
  • GAIA_SETFMT -> GAIA_STRFMT 354ad39

Removed:

Added:

  • ComponentCacheItem lookup by symbol name fa7ca40
  • ComponentCacheItem lookup by entity 9ab5e5c
  • ComponentCacheItem pointer lookup fa7ca40
  • CompMoveHelper::commit() 480b62d
  • ecs::Entity::entity to tell entities and components apart 9d27b5f
  • container support operator!= now 64cf952
  • value setting via attached entity 930b1b8
  • support for manually stating entites in queries cd22b7a

Full Changelog: https://github.com/richardbiely/gaia-ecs/commits/v0.7.9