Releases: richardbiely/gaia-ecs
v0.8.0
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
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
v0.7.8
A smaller release with something big cooking inside and some handy new features.
Components can now be added and removed in batches which results in less archetype-switching of the modified entity and way better performance. In the example bellow a single archetype movement is performed after the last call to add():
ecs::World w;
// Create an entity with Position.
ecs::Entity e = w.add();
w.add<Position>();
...
w.bulk(e)
// add Velocity to entity e
.add<Velocity>()
// remove Position from entity e
.del<Position>()
// add Rotation to entity e
.add<Rotation>()
// add a bunch of other components to entity e
.add<Something1, Something2, Something3>();
With this syntax new in place the batch version of set() function has been slightly altered to match it:
// Old version of the set function
w.set<Position>(e, {1,2,3})
.set<Velocity>({1,0,0});
// New version
w.set(e)
.set<Position>({1,2,3})
.set<Velocity>({1,0,0});
Each entity can be assigned a unique name now. This is useful for debugging or entity lookup when entity id is not present for any reason. If you try to assign an already existing name to some entity, the function does nothing (and triggers an assert if you have them enabled).
ecs::World w;
ecs::Entity e = w.add();
// Entity "e" named "my_unique_name".
// The string is copied and stored internally.
w.name(e, "my_unique_name");
// If you know the length of the string, you can provide it as well
w.name(e, "my_unique_name", 14);
// Pointer to the string used as entity name for entity "e"
const char* name = w.name(e);
// Entity identified by the string returned.
// In this case, "e_by_name" and "e" are equal.
ecs::Entity e_by_name = w.get("my_unique_name");
// The name can be unset by setting it to nullptr
w.name(e, nullptr);
If you already have a dedicated string storage it would be a waste to duplicate the memory. In this case you can use ecs::world::name_raw to name entities. In this case the string is NOT copied and NOT stored internally. You are responsible for its lifetime. The pointer also needs to be stable. Otherwise, any time your storage tries to move the string to a different place you have to unset the name before it happens and set it anew after the move is done.
const char* pUserManagedString = ...;
w.name_raw(e, pUserManagedString);
// If you now the length, you can provide it
w.name_raw(e, pUserManagedString, userManagedStringLength);
// If the user-managed string pointer is not stable, you need to unset the name before the pointer changes location
w.name_raw(e, nullptr);
...
// ... the change of pointer happens
...
// After the user-managed string changed location and obtained a new pointer, you set the name again
w.name_raw(e, pUserManagedString);
Fixed:
- entities with uni components could be moved to a non-matching uni chunk by defragmentation b18f56f
- deleting an empty archetype might have cause wrong query matching e13d754eb44ff309dc831f5de1a982c5d36b71f
- proper checks for the root archetype 36f3f4f
- ecs::world::cleanup not clearing everything properly 0d7e26a
- various theoretical issues with bash scripts 743f38a
Changed:
- bulk operations behavior bd3dd6a
- internal refactoring in preparation for Entity and Component merger 6e3ead1
Tweaked:
- slightly less work when comparing queries c4e8de7
Added:
- ability to bulk-add/del components b03ea78
- support for naming entities 30b9ed6
- GAIA_ASSUME as a wrapper for [[assume]] and similar compiler-specific attributes 4531995
Full Changelog: v0.7.7...v0.7.8
v0.7.7
This version serves primarily as a big stability update. Some critical parts of the code were fixed and more unit tests were added.
No real new features were added for the end-user but a lot of tweaking happened behind the scenes which helped improve performance and memory consumption of the system.
One such example is removal of archetype. If left empty (no entity in the world belonging to them) for a few dozen of frames, the world automatically deletes the archetype making it a bit more faster for any new queries to match archetypes. Memory consumption is also lowered.
Components now have meta-data encoded inside their identifier directly which means we no longer have to make component descriptor lookups so often.
World iteration was simplified. Any iteration has to happen explicitly via queries now. Previously existing ecs::world::each was removed because it was not flexible enough and served as just another way to iterate the world. This way, the API is cleaner and more expressive.
Fixed:
- chunk allocator would break if a huge number of pages were requested at once c6fba0f
- query lookup no longer hacked in 40d22de
- ecs::Query::arr() used chunk view rather than iterator view to match results 20b8113
- constructors/destructors were not called in some cases in containers 45151d5
- container reallocation not handled correctly in some cases 45151d5
- container erase functions not working correctly in some cases 45151d5
- container comparisons not working correctly 45151d5
- container resizing 78821f8
- GAIA_ASSERT not working with complex expressions b1c9253
- unique component constructors not called only automatically with chunk creation ab5d9f4
- GCC 13 stringop-overflow bug workaround f7f48e7 d1fbd93
- wrong type deduction on view_auto 3b1f433
- component cache wouldn't properly zero-initialize for component descriptors 42b179c 149bc99
- empty archetype lifetime handling 840c613
- empty chunks not removed properly f08d5f005d188c0e6a8987f6bf4c32a254eb342
Changed:
- Archetype::archetype_id() renamed toArchetype::id() d686233
- CK_Generic renamed to CK_Gen 56c0e8a
- CK_Chunk renamed to CK_Uni 56c0e8a
- components turned into uint64_t with meta-data encoded inside 36068d3
- archetype graph diagnostics turned back on after being disabled accidentally 6c5b643
- chunk component renamed to unique component to avoid confusion with Chunk 56c0e8a
- quom installed into a virtual Python environment if not previously available af49471
- [breaking change] T& used for mutable type in queries so it is less confusing and more in-line with how c++ type system works eb39152
- some template guides replaced with decltype(auto) for improved readability and type deduction e02f7cb
Removed:
- [breaking change] ComponentInfo merged with ComponentDesc 36068d3
- GAIA_USE_SIMD_COMP_IDX removed 36068d3
- [breaking change] Iterator*::each template removed because some compilers wouldn't vectorize it (MSVC) and it didn't really help with anything cdfb6ea
- [breaking change] world::each removed in favor of query::each as it was not flexible and served as just another way to iterate the world 67f66b4
Tweaked:
- fast-path for component descriptors up to 1024 elements. Use a map after that 149bc99
- components turned into uint64_t with meta-data encoded inside in order to eliminate component descriptor lookups in many places 36068d3
Added:
- profiling mode added to each performance benchmark 929f50b
- number of defragmented entities per tick can be changed via world::defrag_entities_per_tick d25d8b6
- added GAIA_FOR family of macros 720568d
- added GAIA_EACH family of macros cdfb6ea
- archetypes removed when kept empty for a few frames df08d5f
Full Changelog: v0.7.6...v0.7.7
v0.7.6
Building on the previous version, 0.7.6 finishes code refactoring which means:
- namespaces are all set the way they should be now
- ComponentType is now ComponentKind so its name does not collide with the component type system
- any chunk iteration is done using chunk iterators which simplifies code a lot in many places and helps with readability
- has_entities renamed replaced with empty
- calc_entity_cnt renamed to count
- set_structural_changes renamed to lock
- get_structural_changes renamed to locked
This also means new features can finally start being added to the framework.
Additionally, iterator refactoring helped also with iterator performance and usability.
Unlike IteratorDisabled or IteratorAll, Iterator suffered by not getting its code vectorized in all cases. This was because compilers were not able to guarantee the memory it iterates was contiguous. All it took was changing its loop from
for (uint32_t i = from; i<to; ++i) ...
to
for (uint32_t i = 0; i<to; ++i) ...
and making sure the span of data it receives is already offset.
Any usage of std::forward and std::move has been replaced with custom GAIA_FWD and GAIA_MOVE performing casts directly.
Besides shorter name, it helps with compilation time and performance of non-optimized builds. Albeit, mostly with older compilers.
In non-optimized bulids older compilers would treat both as functions calls rather than intrinsics which is pretty terrible. Most of all in framework/library code such as Gaia-ECS where a lot of perfect forwarding and moves are used.
Changed:
- query, component and archetype namespaces dropped 6705b2e
- ComponentType renamed to ComponentKind 3ba4e60
- has_entities replaced with empty 15dbb8d
- calc_entity_cnt renamed to count 15dbb8d
- set_structural_changes renamed to lock, get_structural_changes renamed to locked e03e169
Fixed:
- project would not compile on Clang 8 7d2cb2b
- check for lock when enabling entities e03e169
- core::each_tuple and core::each_tuple_ext not iterating properly c306db9
Tweaked:
Added:
Full Changelog: v0.7.5...v0.7.6
v0.7.5
This release contains major refactoring. Both in folder structure and function naming. No new features were introduced.
All functions have been renamed so they are shorter and require less time typing. While this may look unnecessary, I believe it really helps with the experience and makes developing a project using the framework a lot smoother and easier to understand.
Folder structure has changed, with each folder containing a different namespace. Namespaces are standalone now, with no cross references (aka namespace A using namespace B and vice-verse).
Code was further simplified by removing the no-fragmentation mode which could previously be enforced via GAIA_AVOID_CHUNK_FRAGMENTATION. I believe defragmentation is good-enough solution that optimizes performance over time to a point where no-fragmentation mode is no longer necessary. Instead, GAIA_DEFRAG_ENTITIES_PER_FRAME was introduced which is used to control how many entities per frame can be defragmented.
Fixed:
- missing virtual in BaseSystemManager 6704024
- chunk indices handling above 15-bits e9579bd
- entities not deleted correctly 954529a
- memory leak in move operators on darray, darray_ext, sarray_ext 3c3452f)
Changed:
- DataBuffer_SerializationWrapper merged with DataBuffer and renamed to SerializationBuffer cd59d44
- enabling entities refactored 7be6fc3
- iterators refactored f2b1464
- folder structure refactoring e361561
- naming refactoring
Removed:
- GAIA_AVOID_CHUNK_FRAGMENTATION (replaced fully with defragmentation) 422f7b1
Added:
- GAIA_DEFRAG_ENTITIES_PER_FRAME for controlling how many entities per frame are defragmented 422f7b1
Full Changelog: v0.7.4...v0.7.5
v0.7.4
v0.7.3
Fixed:
- mem_alloc_align would not work it the requested memory size was not a multiple of the alignment 1bd265a
- build issues on some compilers e5ab096
Changed:
- archetype map no longer handles hash collisions in a hacky way d81ca23
- precise SoA data size calculation for archetypes a8d6d12
Added:
Full Changelog: v0.7.2...v0.7.3
v0.7.2
Fixed:
- handling of ARM in CMake 66aef05
- handling of libc++ in CMake 35303be
- single header gaia.h issues with C++20 and later c4e310d
- broken type deduction when ComponentType::CT_Generic was stated explicitely 134c604
- defragmentation on chunkless archetypes 1fc0603
Changed:
Tweaked:
- packages lookup in CMake improved 6987ac3
- less template instantiation 47f41b5 65e0db5
- improved chunk utilization
Added:
- uncached querries 08506d9
- custom data serialization 9fc149e
- chunks of multiple sizes (8K and 16K) c514275
Full Changelog: v0.7.1...v0.7.2
v0.7.1
Fixed:
- incorrect container assertions cec1248
- DataBuffer::SaveComponent wouldn't work correctly with rvalues fc4b1fe
- chunks would be filled with 1 less entity than their capacity allowed 85d2341
- GAIA_USE_LIBCPP usage 9cec3a3
- C++20 and later compilation issues 53a8da5
Changed:
Tweaked:
- component cache inlining cb873f5
- improved performance of ComponentInfo comparison 9e03ee9
- containers::bitset::flip performance improvement 0a58afb
Full Changelog: v0.7.0...v0.7.1