# gaia-ecs **Repository Path**: input-output/gaia-ecs ## Basic Information - **Project Name**: gaia-ecs - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-29 - **Last Updated**: 2026-08-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ![gaia-ecs](docs/img/logo.png) [![Version][badge.version]][version] [![license][badge.license]][license] [![language][badge.language]][language] [![discord][badge.discord]][discord] [![codacy][badge.codacy]][codacy] [badge.version]: https://img.shields.io/github/v/release/richardbiely/gaia-ecs?style=for-the-badge [badge.license]: https://img.shields.io/badge/license-MIT-blue?style=for-the-badge [badge.language]: https://img.shields.io/badge/language-C%2B%2B17-yellow?style=for-the-badge&color=blue [badge.discord]: https://img.shields.io/discord/1183706605108330516?style=for-the-badge&label=Discord [badge.codacy]: https://img.shields.io/codacy/grade/bb28fa362fce4054bbaf7a6ba9aed140?style=for-the-badge [![Documentation](https://img.shields.io/badge/docs-doxygen-blue?style=for-the-badge)](https://richardbiely.github.io/gaia-ecs/) [version]: https://github.com/richardbiely/gaia-ecs/releases [license]: https://en.wikipedia.org/wiki/MIT_License [language]: https://en.wikipedia.org/wiki/C%2B%2B17 [discord]: https://discord.gg/wJjK72yze2 [codacy]: https://app.codacy.com/gh/richardbiely/gaia-ecs/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade **Gaia-ECS** is a fast, ergonomic C++17 ECS framework designed to be the option you can actually learn in an afternoon without reading a manual the size of a novel. You get complex queries with relationship traversal, per-component AoS/SoA layouts, integrated serialization, and multithreading with job dependencies — all behind an API that stays out of your way. ***Highlights:*** * Clean, safe API — no boilerplate, no footguns * Hybrid storage model — table storage for fast iteration, sparse storage for low-cost frequent modification * Expressive queries: [relationships](#relationships), wildcards, hierarchy traversal (DFS/BFS), variables * Per-component [AoS or SoA data layout](#data-layouts) with minimal code changes * Integrated [compile-time](#compile-time-serialization) and [runtime](#runtime-serialization) serialization * [Multithreading](#multithreading) with job dependencies, including [parallel ECS](#parallel-execution) iteration * No external dependencies, no STL strings or containers * [Single-header](#single-header) option — drop it in and go * Compiles fast, runs on [all major compilers](https://github.com/richardbiely/gaia-ecs/actions), even in web browser thanks to [Emscripten](https://emscripten.org) ***Quality:*** * Thoroughly documented — both public API and internals * Correctness ensured by thousands of [unit tests](#testing), in-code asserts, and sanitizers NOTE: Due to its extensive use of acceleration structures and caching, this library is not a good fit for hardware with very limited memory resources (measured in MiBs or less). Micro-controllers, retro gaming consoles, and similar platforms should consider alternative solutions. # Table of Contents * [Introduction](#introduction) * [ECS](#ecs) * [Implementation](#implementation) * [Project structure](#project-structure) * [Usage](#usage) * [Minimum requirements](#minimum-requirements) * [Basic operations](#basic-operations) * [Create or delete entity](#create-or-delete-entity) * [Add or remove component](#add-or-remove-component) * [Component presence](#component-presence) * [Component scope](#component-scope) * [Storage modes and non-fragmenting membership](#storage-modes-and-non-fragmenting-membership) * [Name entity](#name-entity) * [Component hooks](#component-hooks) * [Bulk editing](#bulk-editing) * [Set or get component value](#set-or-get-component-value) * [Copy entity](#copy-entity) * [Entity cleanup](#entity-cleanup) * [Batched creation](#batched-creation) * [Entity lifespan](#entity-lifespan) * [Archetype lifespan](#archetype-lifespan) * [Data processing](#data-processing) * [Query](#query) * [Simple query](#simple-query) * [Query traversal](#query-traversal) * [Traversal order](#traversal-order) * [Query variables](#query-variables) * [Multi-variable queries](#multi-variable-queries) * [Query low-level API](#query-low-level-api) * [Query string](#query-string) * [Uncached query](#uncached-query) * [Query remarks](#query-remarks) * [Iteration](#iteration) * [Constraints](#constraints) * [Change detection](#change-detection) * [Grouping](#grouping) * [Sorting](#sorting) * [Parallel execution](#parallel-execution) * [Relationships](#relationships) * [Relationship basics](#relationship-basics) * [Relations](#relations) * [Targets](#targets) * [Entity dependencies](#entity-dependencies) * [Combination constraints](#combination-constraints) * [Exclusivity](#exclusivity) * [Entity inheritance](#entity-inheritance) * [Prefabs](#prefabs) * [Cleanup rules](#cleanup-rules) * [Hierarchies](#hierarchies) * [Unique components](#unique-components) * [Delayed execution](#delayed-execution) * [Command Merging rules](#command-merging-rules) * [Systems](#systems) * [System basics](#system-basics) * [System dependencies](#system-dependencies) * [Systems and jobs](#system-jobs) * [System callbacks and command buffers](#system-callbacks-and-command-buffers) * [Data layouts](#data-layouts) * [Serialization](#serialization) * [Compile-time serialization](#compile-time-serialization) * [Runtime serialization](#runtime-serialization) * [World serialization](#world-serialization) * [Runtime components](#runtime-components) * [Registration](#registration) * [Field metadata](#field-metadata) * [Typed runtime schemas](#typed-runtime-schemas) * [Schema manifests and validated edits](#schema-manifests-and-validated-edits) * [Nested structs and fixed arrays](#nested-structs-and-fixed-arrays) * [Opaque adapters](#opaque-adapters) * [Dynamic vectors](#dynamic-vectors) * [Enum and bitmask metadata](#enum-and-bitmask-metadata) * [Raw access and cursors](#raw-access-and-cursors) * [Data on runtime relationships](#data-on-runtime-relationships) * [Querying runtime components](#querying-runtime-components) * [Multithreading](#multithreading) * [Worlds, threads, and allocation arenas](#worlds-threads-and-allocation-arenas) * [Jobs](#jobs) * [Job dependencies](#job-dependencies) * [Priorities](#priorities) * [Threads](#threads) * [Scheduler adapters](#scheduler-adapters) * [Customization](#customization) * [Logging](#logging) * [Requirements](#requirements) * [Compiler](#compiler) * [Dependencies](#dependencies) * [Installation](#installation) * [CMake](#cmake) * [Project settings](#project-settings) * [Sanitizers](#sanitizers) * [Single-header](#single-header) * [Conan](#conan) * [Repository structure](#repository-structure) * [Examples](#examples) * [Benchmarks](#benchmarks) * [Profiling](#profiling) * [Testing](#testing) * [Documentation](#documentation) * [Future](#future) * [Contributions](#contributions) * [License](#license) # Introduction ## ECS [Entity-Component-System (ECS)](https://en.wikipedia.org/wiki/Entity_component_system) is an architectural pattern that organizes code around data rather than objects, following the principle of [composition over inheritance](https://en.wikipedia.org/wiki/Composition_over_inheritance). Instead of modeling your program around real-world objects (car, house, human), you think in terms of the data needed to achieve a result. When moving something from A to B you don't care if it's a car or a plane — you only care about its position and velocity. This makes code easier to maintain, extend, and reason about, while also being more naturally suited to how modern hardware works. Think of ECS as a database engine without [ACID](https://en.wikipedia.org/wiki/ACID) constraints — optimized for latency and throughput beyond what a general-purpose database could achieve, at the cost of data safety guarantees. Queries over entities are fast by design, and data locality is a first-class concern rather than an afterthought. The three building blocks are: * **Entity** — a unique id that represents an object in your world * **Component** — a piece of data attached to an entity (position, velocity, health) * **System** — logic that operates on all entities matching a given set of components A vehicle is any entity with Position and Velocity. Add Driving and it's a car. Add Flying and it's a plane. The movement systems only care about the components they need — nothing else. ## Implementation **Gaia-ECS** supports table storage and sparse storage for components. Table storage is implemented with archetypes: unique combinations of components are grouped into archetypes — think of them as [database tables](https://en.wikipedia.org/wiki/Table_(database)) where components are columns and entities are rows. Each archetype is made up of chunks: fixed-size allocation blocks selected per archetype from a small set of size classes. Compact archetypes use small cache-friendly chunks, while wider dense archetypes can use larger chunks to reduce per-chunk iteration overhead. Components of the same type are laid out linearly within a chunk, minimizing heap allocations and keeping iteration cache-friendly. The main strengths of this layout are fast iteration, predictable memory usage, and natural parallelism. The tradeoff is that adding or removing fragmenting ids requires moving data between archetypes — mitigated here by an archetype graph and support for batched component changes. Gaia-ECS also supports selected component data living outside chunk columns when a different tradeoff is needed. That keeps the main archetype path optimized for dense iteration while still allowing more dynamic, optional, or less cache-sensitive data to use a different storage path. Queries are compiled into bytecode and executed by an internal virtual machine, ensuring only the complexity your query actually needs is paid for. Components are themselves entities with a `Component` tag attached. Treating components as first-class entities is what enables relationships and keeps the overall design orthogonal — the same mechanisms that handle entities handle components, without special cases. ## Project structure The entire project is implemented inside gaia namespace. It is further split into multiple sub-projects each with a separate namespaces. - `core` - core functionality, use by all other parts of the code - `mem` - memory-related operations, memory allocators - `cnt` - data containers - `meta` - reflection framework - `ser` - serialization framework - `mt` - multithreading framework - `ecs` - the ECS part of the project The project has a dedicated `external` section that contains 3rd-party code. At present, it only includes a modified version of the [robin-hood](https://github.com/martinus/robin-hood-hashing) hash-map. # Usage ## Minimum requirements ```cpp #include ``` The entire framework is placed in a namespace called **gaia**. The ECS part of the library is found under **gaia::ecs** namespace.
In the code examples below we will assume we are inside gaia namespace. ## Basic operations ### Create or delete entity Entity a unique "thing" in `ecs::World`. Creating an entity at runtime is as easy as calling `World::add`. Deleting is done via `World::del`. Once deleted, entity is no longer valid and if used with some APIs it is going to trigger a debug-mode assert. Verifying that an entity is valid can be done by calling `World::valid`. ```cpp ecs::World w; // Create a new entity ecs::Entity e = w.add(); // Check if "e" is valid. Returns true. bool isValid = w.valid(e); // true // Delete the entity w.del(e); // Check if "e" is still valid. Return false. isValid = w.valid(e); // false ``` It is also possible to attach entities to entities. This effectively means you are able to create your own components/tags at runtime. ```cpp 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); ``` ### Name entity Each entity can be assigned a unique name. This is useful for debugging or entity lookup when entity id is not present for any reason. ```cpp 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); // Non-owning util::str_view of the string used as entity name for entity "e" auto name = w.name(e); // returns: "my_unique_name" // 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); auto unnamed = w.name(e); // returns: empty util::str_view ``` If you already have a dedicated string storage it would be a waste to duplicate the memory. In this case you can use `World::name_raw` to name entities. It does NOT copy and does NOT store the string internally which means you are responsible for its lifetime. The pointer, contents, and length must stay stable while the raw name is registered. Otherwise, any time your storage tries to move or rewrite the string you have to unset the name before it happens and set it anew after the change is done. ```cpp const char* pUserManagedString = ...; w.name_raw(e, pUserManagedString); // If you know 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); ``` Hierarchical name lookup is also possible. ```cpp auto europe = wld.add(); auto slovakia = wld.add(); auto bratislava = wld.add(); wld.child(slovakia, europe); wld.child(bratislava, slovakia); wld.name(europe, "europe"); wld.name(slovakia, "slovakia"); wld.name(bratislava, "bratislava"); auto e1 = wld.get("europe.slovakia"); // returns slovakia auto e2 = wld.get("europe.slovakia.bratislava"); // returns bratislava ``` Hierarchical entity lookup follows direct `ChildOf` links and direct non-fragmenting `Parent` links. Use `ChildOf` when the hierarchy should participate in archetype identity, and use `Parent` when you need non-fragmenting hierarchy edges. Character '.' (dot) is used as a separator. Therefore, dots can not be used inside entity names. ```cpp auto e = wld.add(); wld.name(e, "eur.ope"); // invalid name, the naming request is going to be ignored ``` ### Names and lookup `World::name` gives an entity its normal world [name](#name-entity). `World::alias` gives an entity one extra lookup name. `World::get` is the normal lookup entry point. It first tries ordinary entity names, including hierarchical paths such as `gameplay.render`, and then falls back to component lookup rules when the string does not name an entity directly. Names and aliases are both unique lookup keys. A name is the entity’s canonical world name and participates in hierarchy. An alias is an extra flat lookup key that does not change the entity’s place in that hierarchy. Components add a few extra naming helpers because they need more than one identity. A component symbol is the stable C++ identity, such as `gameplay_render::Position`. A component path is the scoped lookup name, such as `gameplay.render.Position`. A display name is the prettier label you would prefer to show in logs and tools. Most code should just use `World::get`. Use a name when the entity should live in the normal world hierarchy. Use an alias when you want one extra flat lookup key without changing that hierarchy. Use a symbol when you need a stable component identity, for example in semantic JSON. Use a path when you want to refer to a component by its place in the ECS scope hierarchy. If a name does not resolve the way you expect, `World::resolve(out, name)` collects every entity the world naming rules can see for that string. ```cpp namespace gameplay_render { struct Position {}; } ecs::World w; const ecs::Entity gameplay = w.add(); w.name(gameplay, "gameplay"); const ecs::Entity render = w.add(); w.name(render, "render"); w.child(render, gameplay); const ecs::Entity player = w.add(); w.name(player, "player"); w.child(player, gameplay); w.alias(player, "MainPlayer"); w.get("gameplay.player"); // player w.get("MainPlayer"); // player ecs::Entity positionComp = ecs::EntityBad; w.scope(render, [&] { positionComp = w.add().entity; }); w.symbol(positionComp); // "gameplay_render::Position" w.path(positionComp); // "gameplay.render.Position" w.get("gameplay.render.Position"); // positionComp w.get("Position"); // positionComp if "Position" is globally unique, otherwise the closest scoped match wins and ambiguous global short names do not resolve w.alias(positionComp, "RenderPosition"); w.get("RenderPosition"); // positionComp ``` ### Component scope `World::scope(scope)` sets the current component scope and returns the previous one. `World::scope(scope, func)` does the same thing for the duration of one callable and restores the old scope afterwards. `World::lookup_path(scopes)` sets an ordered list of lookup scopes used for unqualified component lookup. Each scope is searched like a temporary component scope: the scope first, then its parents. `World::lookup_path()` returns the current list. The scope entity and its `ChildOf` ancestors should have names, because Gaia-ECS builds the scoped path from that entity hierarchy. New components registered while a scope is active use that scope to build their default path name. For example, registering `Position` while the current component scope is the entity path `gameplay.render` gives the component the path name `gameplay.render.Position`. Unqualified component lookup checks the active scope first, then walks up parent scopes, then searches each lookup-path scope in order while also walking up its parents, then falls back to global exact symbol lookup, global path lookup, global unique short-symbol lookup, and finally alias lookup. That means a lookup from `gameplay.render` can still find `gameplay.Position` when there is no closer match in `gameplay.render`, a lookup path such as `{tools, render}` can prefer `gameplay.tools.Device` before falling back to `gameplay.Device`, and a bare `"Position"` can resolve globally when that short symbol is unique. String queries follow the same rules, but they capture the active scope and lookup path when the query expression is parsed. In practice that means `w.scope(render, [&] { q.add("Position"); });` or `w.lookup_path(scopes); q.add("Position");` resolve `Position` while `add(...)` runs, store the resulting component id in the query, and will not be rewritten later if the scope or component naming metadata changes. If a query uses a bare short name such as `"Position"`, that global fallback only works when the short symbol is unique. Scoped component registration looks like this: ```cpp namespace gameplay_types { struct Position {}; } namespace render_types { struct Position {}; } ecs::World w; const ecs::Entity gameplay = w.add(); w.name(gameplay, "gameplay"); const ecs::Entity render = w.add(); w.name(render, "render"); w.child(render, gameplay); w.scope(gameplay, [&] { w.add(); // the registered symbol is "gameplay_types::Position" // the scoped path is "gameplay.Position" w.scope(render, [&] { w.add(); // the registered symbol is "render_types::Position" // the scoped path is "gameplay.render.Position" const auto renderLookup = w.get("Position"); // the component entity registered under "gameplay.render.Position" }); const auto gameplayLookup = w.get("Position"); // the component entity registered under "gameplay.Position" }); ``` If you prefer creating named scopes directly from strings, `World::module(module_path)` creates or reuses the named `ChildOf` chain and returns the deepest scope entity. You can then pass that entity to `World::scope(...)` explicitly when you want to activate it. ```cpp namespace gameplay_render { struct Position {}; struct Velocity {}; } ecs::World w; const ecs::Entity renderModule = w.module("gameplay.render"); w.scope(renderModule, [&] { w.add(); w.add(); }); const auto positionComp = w.get("Position"); // returns: the component entity registered under "gameplay.render.Position" in this example because the short name is unique const auto velocityComp = w.get("gameplay.render.Velocity"); // returns: the component entity registered under "gameplay.render.Velocity" ``` `World::module(...)` only creates or finds the scope hierarchy. `World::scope(...)` is the step that makes registration and relative lookup happen inside that scope. ### Storage modes and non-fragmenting membership Gaia-ECS provides two component storage modes: - **Table storage** is the default. Internally, the payload is stored in an archetype chunk, providing fast sequential iteration. Its memory address can change when the entity moves between archetypes. - **Sparse storage** stores the payload in a separate sparse store. It provides a stable memory address, but does not provide the sequential access of table storage. Storage mode only determines where the payload lives. It does not determine whether the component id participates in archetype identity. A component using sparse storage still changes the entity's archetype when it is added or removed. #### Selecting a storage mode For a typed C++ component, the storage policy can be declared with `GAIA_STORAGE`: ```cpp struct Cooldown { GAIA_STORAGE(Sparse); float value = 0.0f; }; ecs::World w; auto e = w.add(); w.add(e); auto cooldownValue = w.set(e); cooldownValue.value = 1.5f; ``` `GAIA_STORAGE(Table)` is the default and usually does not need to be written. A typed component's declared policy is authoritative: adding `ecs::Sparse` or `ecs::DontFragment` to a typed table component does not change its storage. Runtime component descriptors can still select their storage mode before instances are attached. #### Non-fragmenting membership `ecs::DontFragment` controls archetype membership, not payload storage. It keeps the component id outside the entity's archetype, so adding or removing the component does not move the entity between archetypes. Typed non-fragmenting components must declare `GAIA_STORAGE(Sparse)` because table payloads require an archetype column. ```cpp struct Cooldown { GAIA_STORAGE(Sparse); float value = 0.0f; }; ecs::World w; const auto& cooldown = w.add(); // Keep Cooldown membership outside archetype identity. w.add(cooldown.entity, ecs::DontFragment); auto e = w.add(); w.add(e); auto cooldownValue = w.set(e); cooldownValue.value = 1.5f; ``` The resulting configurations are: | Typed declaration and traits | Storage mode | Component id | Add/remove changes archetype | |---|---|---|---| | `GAIA_STORAGE(Table)` | `Table` | In archetype | Yes | | `GAIA_STORAGE(Sparse)` | `Sparse` | In archetype | Yes | | `GAIA_STORAGE(Sparse)` + `DontFragment` | `Sparse` | Outside archetype | No | Rule of thumb: - Keep hot, common, frequently iterated data in table storage. - Use `Sparse` when the payload needs a stable address, but the component should still participate in archetype identity. - Use `GAIA_STORAGE(Sparse)` with `DontFragment` for frequently toggled typed optional state such as cooldowns, temporary status effects, markers, or runtime tool state. - Avoid sparse storage for components such as `Position` or `Velocity` that benefit from sequential table access, unless profiling justifies it. Directly adding or removing an already-registered `DontFragment` component is safe during serial query iteration because the entity does not move to another archetype. If the active query filters on that component, later rows are matched against the current world state rather than a snapshot taken before iteration. >**NOTE:
** SoA components do not support sparse storage and remain in table storage. `GAIA_STORAGE(Sparse)` and `ecs::Sparse` apply only to plain AoS generic components.
>**NOTE:
** Runtime component storage and fragmentation traits must be set before the component has instances attached to entities. They do not override a typed component's `GAIA_STORAGE` policy.
>**NOTE:
** Once a component is marked `ecs::Sparse` or `ecs::DontFragment`, it stays that way. Both entities carry the [ecs::Requires trait](#entity-dependencies).
### Component presence Whether or not a certain component is associated with an entity can be checked in two different ways. Either via an instance of a World object or by the means of `Iter` which can be acquired when running [queries](#query). ```cpp // Check if entity e has Velocity (via world). const bool hasVelocity = w.has(e); // Check if entity wheel is attached to the car const bool hasWheel = w.has(car, wheel); ... // Check if entities optionally have Position, or Velocity. ecs::Query q = w.query().any().any(); q.each([&](ecs::Iter& it) { const bool hasPosition = it.has(); const bool hasVelocity = it.has(); ... }); ``` Providing entities is supported as well. ```cpp auto p = w.add().entity; auto v = w.add().entity; // Check if entities optionally have Position, or Velocity. ecs::Query q = w.query().any(p).any(v); q.each([&](ecs::Iter& it) { const bool hasPosition = it.has(p); const bool hasVelocity = it.has(v); ... }); ``` ### Add or remove component Components can be created using `World::add` This function returns a descriptor of the object which is created and stored in the component cache. Each component is assigned one entity to uniquely identify it. You do not have to do this yourself, the framework performs this operation automatically behind the scenes any time you call some compile-time API where you interact with your structure. However, you can use this API to quickly fetch the component's entity if necessary. ```cpp struct Position { float x, y, z; }; const ecs::ComponentCacheItem& cci = w.add(); ecs::Entity position_entity = cci.entity; ``` Because components are entities as well, adding them is very similar to what we have seen previously. ```cpp struct Position { float x, y, z; }; struct Velocity { float x, y, z; }; ecs::World w; // Create an entity with Position and Velocity. ecs::Entity e = w.add(); w.add(e, {0, 100, 0}); w.add(e, {0, 0, 1}); // Remove Velocity from the entity. w.del(e); ``` This also means the code above could be rewritten as following: ```cpp // Create Position and Velocity entities ecs::Entity position = w.add().entity; ecs::Entity velocity = w.add().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, Velocity{0, 0, 1}); // Remove Velocity from the entity. w.del(e, velocity); ``` When adding components following restrictions apply: * There can be at most 32 ids in an entity's archetype (components, tags and relationships stored in archetype chunks). Components marked with `ecs::DontFragment` and stored outside archetypes do not consume these slots. If you need more archetype-resident ids you can merge some of your components, or rethink the strategy because too many fragmenting ids usually implies design issues (e.g. object-oriented thinking or mirroring real-life abstractions too directly in ECS). * Maximum size of a registered component type is currently 4095 bytes. This limit comes from the component metadata and chunk layout used internally. If this is not enough for you, store a pointer or handle to data that lives outside ECS. Note, this limit is still enforced today even for components that use sparse storage. * [SoA](#data-layouts) components can have at most 4 members and each of them can be at most 255 bytes long. * Components must be default-constructible (either the default constructor is present or you provide one yourself). If your component contains members that are not default-constructible (e.g. from a 3rd party library that is beyond your control), you need to work this around. You will need to store a pointer, or come up with different means of accessing this data. * Implicit registration only creates the default component form. If a component needs non-default traits such as `ecs::Sparse` or `ecs::DontFragment`, register it explicitly first and then apply the trait to the component entity. Implicit registration can also be disabled entirely with `GAIA_ECS_AUTO_COMPONENT_REGISTRATION`. Runtime-defined components are covered separately in [Runtime components](#runtime-components). ### Component hooks It is possible to register add/del/set hooks for components. When a given component is added to an entity, deleted from it, or the value is set the hook triggers. This comes handy for debugging, or when specific logic is needed for a given component. Component hooks are unique. Each component can have at most one add hook, and one delete hook. ```cpp ecs::World w; const ecs::ComponentCacheItem& pos_item = w.add(); ecs::ComponentCache::hooks(pos_item).func_add = [](const ecs::World& w, const ecs::ComponentCacheItem& cci, ecs::Entity src) { // Position component added to entity "src" // ... }; ecs::Entity e = w.add(); // The add hook will trigger w.add(e); ``` Hooks can easily be removed: ```cpp const ecs::ComponentCacheItem& pos_item = w.add(); ecs::ComponentCache::hooks(pos_item).func_add = nullptr; ecs::Entity e = w.add(); // The add hook will not be triggered because we removed the hook w.add(e); ``` It is also possible to set up a "set" hook. For explicit setter APIs such as `w.set(e) = ...` and `w.acc_mut(e).set(...)`, the hook runs after the new value has been written back. ```cpp ecs::World w; const ecs::ComponentCacheItem& pos_item = w.add(); ecs::ComponentCache::hooks(pos_item).func_set = [](const ecs::World& w, const ecs::ComponentRecord& rec, Chunk& chunk) { // Position component value has been updated // ... }; ecs::Entity e = w.add(); w.add(e); // Don't trigger the set hook, yet w.set(e) = {}; // Trigger the set hook w.acc_mut(e).set({}); // Trigger the set hook w.acc_mut(e).sset({}); // Don't trigger the set hook ``` Unlike *add* and *del* hooks, *set* hooks will not tell you what entity the hook triggered for. This is because any write access is done for the entire chunk, not just one of its entities. If one-entity behavior is required, the best thing you can do is moving your entity to a separate archetype (e.g. by adding some unique tag component to it). Hooks can be disabled by defining GAIA_ENABLE_HOOKS 0. Add and del hooks are controled by GAIA_ENABLE_ADD_DEL_HOOKS, set hooks by GAIA_ENABLE_SET_HOOKS. They are all enabled by default. ### Observers Observers are a mechanism that allows you to register to certain events and listen to them triggering. Similar to hooks, you can listen to add, del or set events. However, unlike hooks there can be any number of these per given component or entity. Observers can be looked at as a reactive alternative to systems. They allow different parts of the application to react to something happening immediately. The feature can be enabled by defining `GAIA_OBSERVERS_ENABLED 1`, and is enabled by default. Under the hood they use the query engine, just like systems. Systems are meant to be used as a regular part of the frame. Observers are event reactions. Their cost is less predictable because events may be rare, happen in bursts, or not happen in a frame at all. If the same work needs to run predictably every frame, use a system instead. Because observers are query-backed, query shaping helpers such as `depth_order(...)` can be used on them as well when you want cached top-down breadth-first iteration over fragmenting hierarchies like `ChildOf`. Observers also expose the same query cache controls as plain queries. By default an observer keeps cached query state locally. Use `scope(ecs::QueryCacheScope::Shared)` only when many identical observer query shapes are rebuilt and you want them to reuse one shared cache entry. Use `kind(ecs::QueryCacheKind::None)` only for special cases where you explicitly do not want observer query caches. Observer events currently mean: * `OnAdd` - an entity starts matching because ids were added * `OnDel` - an entity stops matching because ids were removed * `OnSet` - a value of an already present component was explicitly written `OnSet` is triggered by APIs such as `set(entity)`, `set(entity, object)`, `acc_mut(entity).set(...)`, `modify(entity)`, and `modify(entity, object)`. It is not triggered by `sset(...)`, `modify(...)`, or by the initial `add(entity, value)` that creates the component. `set(entity)` uses a write-back proxy, so `OnSet` is emitted after the full expression or scope writes the final value back. Mutable query and observer callbacks follow the same rule. When a callback writes through `Position&` or `it.view_mut()`, `OnSet` is emitted after the callback returns, not in the middle of the callback. Following is an observer that generates an OnAdd event every time some entity is added Position and Velocity. ```cpp ecs::World w; w.observer() .event(ObserverEvent::OnAdd) .all() .all() .on_each([&](ecs::Iter& it) { // Called for each entity that has Position and Velocity added // ... }); ecs::Entity e = w.add(); // Observer will not trigger yet, only Position is added. w.add(e); // Observer will trigger for the entity "e" now, because both Position and Velocity were added to it w.add(e); // Does not the observer for e1 when creating a copy. To do so, use copy_ext, ecs::Entity e1 = w.copy(e); // Triggers the observer for e2 because a new entity was created that is a copy of "e", and has both Position and Velocity added. ecs::Entity e2 = w.copy_ext(e); // Creates 1000 observer-visible copies of "e". w.copy_ext_n(e, 1000); w.copy_ext_n(e, 1000, [](ecs::Entity newEntity) { // Do something with the new entity // ... }); w.copy_ext_n(e, 1000, [](ecs::CopyIter& it) { auto entityView = it.view(); // You can also access the view of components attached to the copied entities auto someView = it.view(); GAIA_EACH(it) { // Do something with the new entities // ... } }); // Prepare a new entity "e3". ecs::Entity e3 = w.add(); // We want to add Position and Velocity to "e3". Our observer won't triggered yet because changes are not committed. ecs::EntityBuilder builder = w.build(e3); builder .add() .add(); // Commit changes. The observer will triggers now. builder.commit(); ``` Listening to removal of entities looks similar: ```cpp w.observer() // .event(ecs::ObserverEvent::OnDel) .no() .no() .on_each([&cnt, &isDel](ecs::Iter& it) { // Called for each entity that has Position and Velocity removed from it // ... }); // Observer will not trigger yet, on Position was removed. w.del(e); // Observer will trigger for the entity "e" now, because both Position and Velocity were removed from it w.del(e); ``` Listening to value changes uses `OnSet`: ```cpp uint32_t hits = 0; w.observer() .event(ecs::ObserverEvent::OnSet) .all() .on_each([&](ecs::Entity entity, const Position& pos) { ++hits; (void)entity; (void)pos; }); ecs::Entity e = w.add(); w.add(e, {1.0f, 2.0f, 3.0f}); // No OnSet yet. This was the initial add. w.set(e) = {4.0f, 5.0f, 6.0f}; // OnSet triggered once. w.acc_mut(e).sset({7.0f, 8.0f, 9.0f}); // Still one hit. sset is silent. w.modify(e); // OnSet triggered again. w.query().all().each([&](Position& pos) { // Still no new hit here. Query writes are deferred until the callback returns. pos = {10.0f, 11.0f, 12.0f}; }); // OnSet triggered again after the callback completed. // For query writes, OnSet is delivered once per modified matching entity. ``` #### Observers for relation pairs Observers also work with relation pairs. Use them when a relation change should run code right away. The callback is normal user code. It can update a store, rebuild an index, notify gameplay code, or save work for a later pass. Finish the observer callback before calling `update()` or `frame_cleanup()`. Debug builds assert this contract because deferred cleanup must not invalidate the mutation that triggered the observer. Use a wildcard target when every target of a relation should trigger the same code: ```cpp const ecs::Entity HomeOf = w.add(); w.observer() .event(ecs::ObserverEvent::OnAdd) .all(ecs::Pair(HomeOf, ecs::All)) .on_each([&](ecs::Iter& it) { // A source entity gained a HomeOf relation to some target. // Update derived data here, or record work for a later system. }); ``` Use `OnDel` for removals: ```cpp w.observer() .event(ecs::ObserverEvent::OnDel) .all(ecs::Pair(HomeOf, ecs::All)) .on_each([&](ecs::Iter& it) { // A source entity lost a HomeOf relation. }); ``` Use an exact pair when only one target matters: ```cpp w.observer() .event(ecs::ObserverEvent::OnAdd) .all(ecs::Pair(HomeOf, specificHome)) .on_each([&](ecs::Iter& it) { // React only to HomeOf(specificHome). }); ``` Changing a relation target is structural (changes the entity shape). Replacing `(HomeOf, A)` with `(HomeOf, B)` triggers `OnDel` for the old pair and `OnAdd` for the new pair. It does not trigger `OnSet`. If a relation pair carries a payload, writing that existing payload can trigger `OnSet`. Exact pairs and wildcard pairs such as `(HomeOf, *)`, `(*, target)`, or `(*, *)` can observe those value writes. Wildcard pair observers can match more changes than exact-pair observers. Their cost depends on how often those changes happen, which is normal for event reactions. ### Bulk editing Adding an entity to entity means it becomes a part of a new archetype. Like mentioned [previously](#implementation), becoming a part of a new archetype means that all data associated with the entity needs to be moved to a new place. The more ids in the archetype the slower the move (empty components/tags are an exception because they do not carry any data). For this reason it is not advised to perform large number of separate additions / removals per frame. Instead, when adding or removing multiple entities/components at once it is more efficient doing it via bulk operations. This way only one archetype movement is performed in total rather than one per added/removed entity. ```cpp ecs::World w; // Create an entity with Position. This is one archetype movement. ecs::Entity e = w.add(); w.add(); // Add and remove multiple components. // This does one archetype movement rather than 6 compared to doing these operations separate. w.build(e) // add Velocity to entity e .add() // remove Position from entity e .del() // add Rotation to entity e .add() // set a name for the entity if desired .name("MyEntity); ``` It is also possible to manually commit all changes by calling `ecs::EntityBuilder::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. ```cpp ecs::EntityBuilder builder = w.build(e); builder .add() .del(); if (some_condition) { builder.add(); } builder.commit(); ``` >**NOTE:**
Once `ecs::EntityBuilder::commit` is called (either manually or internally when the builder's destructor is invoked) the contents of builder are returned to its default state. ### Set or get component value ```cpp // Change Velocity's value. w.set(e) = {0, 0, 2}; // Same shape as above but silent: no world-version update, no hooks, no OnSet observers. w.sset(e) = {4, 2, 0}; ``` `w.set(entity)` returns a write-back proxy. The current value is copied out, you mutate the proxy, and the new value is written back at the end of the full expression or scope. ```cpp w.add(e, {1, 2, 3}); { auto pos = w.set(e); pos.x = 10; pos.y = 20; pos.z = 30; // The write-back did not happen yet. const auto& current = w.get(e); // current is still {1, 2, 3} } // The proxy went out of scope, so the new value is now stored. const auto& updated = w.get(e); // updated is {10, 20, 30} ``` If you need the write to happen immediately, use `acc_mut(entity).set(...)` instead of `w.set(entity)`. For runtime object/component entities, the immediate form is `acc_mut(entity).set(object, value)`. When setting multiple component values at once it is more efficient doing it via chaining: ```cpp w.acc_mut(e) // Change Velocity's value on entity "e" .set({0, 0, 2}) // Change Position's value on entity "e" .set({0, 100, 0}) // Change... .set...; ``` Similar to `ecs::EntityBuilder::build` you can also use the setter object in scenarios with complex logic. ```cpp ecs::ComponentSetter setter = w.acc_mut(e); setter.set({0, 0, 2}); if (some_condition) setter.set({0, 100, 0}); setter.set({ ... }).set({ ... }); // You can also retrieve a reference to data (for AoS) or the data accessor (for SoA) auto& vel = setter.mut(); auto& pos = setter.mut(); ``` The setter object supports the same immediate object-based form: ```cpp ecs::Entity runtimePos = w.add().entity; w.add(e, runtimePos, Position{1, 2, 3}); ecs::ComponentSetter setter = w.acc_mut(e); setter.set(runtimePos, Position{10, 20, 30}); ``` `setter.mut()` and `w.mut(e)` are silent raw write paths. If you use them and want hooks or `OnSet`, call `w.modify(e)` after finishing the write. The same pattern applies to object-based writes: ```cpp ecs::Entity runtimePos = w.add().entity; w.add(e, runtimePos, Position{1, 2, 3}); auto& pos = w.mut(e, runtimePos); pos.x = 10; pos.y = 20; pos.z = 30; w.modify(e, runtimePos); ``` Use the write path that matches the behavior you want: * `set(entity)` - writes back on scope/full-expression end and then triggers set hooks and `OnSet` * `set(entity, object)` - same as above for a specific runtime object/component entity * `acc_mut(entity).set(...)` - writes immediately and triggers set hooks and `OnSet` * `acc_mut(entity).set(object, value)` - immediate object-based write with set hooks and `OnSet` * `sset(entity)` / `mut(entity)` - silent write paths, no hooks, no `OnSet` * `sset(entity, object)` / `mut(entity, object)` - silent object-based write paths. Pair them with `modify(entity, object)` when you want set side effects. Components up to 8 bytes (including) are returned by value. Bigger components are returned by const reference. ```cpp // Read Velocity's value. As shown above Velocity is 12 bytes in size. // Therefore, it is returned by const reference. const auto& velRef = w.get(e); // However, it is easy to store a copy. auto velCopy = w.get(e); ``` Both read and write operations are also accessible via views. Check the [iteration](#iteration) sections to see how. ### Copy entity A copy of another entity can be easily created. ```cpp // Create an entity with Position and Velocity. ecs::Entity e = w.add(); w.add(e, position, Position{0, 100, 0}); w.add(e, velocity, Velocity{0, 0, 1}); // Make a copy of "e". Component values on the copied entity will match the source. // Value of Position on "e2" will be {0, 100, 0}. // Value of Velocity on "e2" will be {0, 0, 1}. ecs::Entity e2 = w.copy(e); ``` ### Entity cleanup Anything attached to an entity can be easily removed using `World::clear`. This is useful when you need to quickly reset your entity and still want to keep your Entity's id (deleting the entity would mean that as some point it could be recycled and its id could be used by some newly created entity). ```cpp ecs::Entity e = w.add(); ecs::Entity something = w.add(); // Add a Position component to our entity w.add(e, {0, 100, 0}); // Add the "something" entity to our entity w.add(e, something); // Remove anything attached to out entity w.clear(e); bool hasPosition = w.has(e); // false bool hasSomething = w.has(e, something); // false ``` ### Batched creation Another way to create entities is by creating many of them at once. This is more performant than creating entities one by one. ```cpp // 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, Velocity{0, 0, 1}); // Create 1000 more entities like "e". // Their component values are not initialized 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 // ... }); w.copy_n(e, 1000, [](ecs::CopyIter& it) { auto entityView = it.view(); // You can also access the view of components attached to the entity auto someView = it.view(); GAIA_EACH(it) { // Do something with the new entities // ... } }); // Same as copy_n, but observers are notified for the copied ids and entities. w.copy_ext_n(e, 1000); w.copy_ext_n(e, 1000, [](ecs::CopyIter& it) { auto entityView = it.view(); // You can also access the view of components attached to the copied entities auto someView = it.view(); GAIA_EACH(it) { // Do something with the new entities // ... } }); ``` ### Entity lifespan Every entity in the world is reference counted. When an entity is created, the value of this counter is 1. When `ecs::World::del` is called the value of this counter is decremented. When it reaches zero, the entity is deleted. However, the lifetime of entities can be extended. Calling `ecs::World::del` any number of times on the same entity is safe because the reference counter is decremented only on the first attempt. Any further attempts are ignored. #### SafeEntity `ecs::SafeEntity` is a wrapper above `ecs::Entity` that makes sure that an entity stays alive until the last `ecs::SafeEntity` referencing the entity goes out of scope. When the wrapper is instantiated it increments the entity's reference counter by 1. When it goes out of scope it decrements the counter by 1. In terms of functionality, this is reminiscent of a C++ smart pointer, std::shared_ptr. ```cpp ecs::World w; // Create an entity. Its reference counter is 1. ecs::Entity player = w.add(); { // Make sure the entity survives so long playerSafe exists. Reference counter is incremented to 2. auto playerSafe = ecs::SafeEntity(w, player); // Try to delete the player entity. The reference counter is decremented to 1. // It is not zero and therefore the entity player is not deleted. w.del(player); bool isValid = w.valid(player); // true // We can try deleting the entity again but the request is ignored this time. // Calling del on an entity decrements the reference counter only once. Further // calls are dropped. Hence, the reference counter remains 1. w.del(player); isValid = w.valid(player); // true } // Here, playerSafe is out of scope. Reference counter is decremented to 0. // Internally, w.del(player) is called. // ... it's not safe to use player at this point anymore. bool isValid = w.valid(player); // false ``` ecs::SafeEntity is fully compatible with ecs::Entity and can be used just like it in all scenarios. ```cpp ecs::World w; ecs::Entity player = w.add(); auto playerSafe = ecs::SafeEntity(w, player); // Add a Position component to playerSafe (player) w.add(playerSafe); // w.add(player) <-- this would do the same thing ``` #### WeakEntity `ecs::WeakEntity` is a wrapper above `ecs::Entity` that makes sure that when the entity it references is deleted, it automatically starts acting as `ecs::EntityBad`. In terms of functionality, this is reminiscent of a C++ smart pointer, std::weak_ptr. `ecs::WeakEntity` is fully compatible with `ecs::Entity` and can be used just like it in all scenarios. As a result, you have to keep in mind that it can become invalid at any point. ```cpp ecs::World w; // Create an entity. Its reference counter is 1. ecs::Entity player = w.add(); // Create a "weak reference" to the entity player auto playerSafe = ecs::WeakEntity(w, player); // Add a Position component to playerSafe (player) w.add(playerSafe); // w.add(player) <-- this would do the same thing // Calling del decrements the reference count of entity by 1. In this case, the reference counter // becomes 0 and therefore the entity is deleted. // Our playerSafe automatically becomes EntityBad. w.del(player); bool isValid; isValid = w.valid(player); // false isValid = w.valid(playerSafe); // false ``` Technically, `ecs::WeakEntity` is almost the same thing as `ecs::Entity` with one nuance difference. Because entity ids are recycled, in theory, `ecs::Entity` left lying around somewhere could end up being multiple different things over time. This is not an issue with `ecs::WeakEntity` because the moment the entity linked with it gets deleted, it is reset to `ecs::EntityBad`. This is an edge-case scenario, unlikely to happen even, but should you ever need it `ecs::WeakEntity` is there to help. If you decided to change the amount of bits allocated to `Entity::gen` to a lower number you will increase the likelihood of double-recycling happening and increase usefulness of `ecs::WeakEntity`. A more useful use case, however, would be if you need an entity identifier that gets automatically reset when the entity gets deleted without any setup necessary from your end. Certain situations can be complex and using `ecs::WeakEntity` just might be the one way for you to address them. ### Archetype lifespan Once all entities of given archetype are deleted (and as a result all chunks in the archetypes are empty), the archetype stays alive for another 127 ticks of `ecs::World::update`. However, there might be cases where this behavior is insufficient. Maybe you want the archetype deleted faster, or you want to keep it around forever. For instance, you might often end up deleting all entities of a given archetype only to create new ones seconds later. In this case, keeping the archetype around can have several performance benefits: 1) no need to recreate the archetype 2) no need to rematch queries with the archetype ```cpp ecs::World w; ecs::Entity player0 = w.add(); // player0 belongs to archetype A ecs::Entity teamA = w.add(); // teamA belongs to archetype A // Player0 becomes a part of archetype B. w.add(player0, teamA); // Archetype B is never going to be deleted. w.set_max_lifespan(player0, 0); // Archetype B is going to be deleted after 20 ticks of ecs::World::update. w.set_max_lifespan(player0, 20); // Reset maximum lifespan of the archetype B belongs to. w.set_max_lifespan(player0); ``` Note, if the entity that changed an archetype’s lifespan moves to a new archetype, the new archetype’s lifespan will not be updated. ```cpp ecs::World w; ecs::Entity player0 = w.add(); // player0 belongs to archetype A ecs::Entity teamA = w.add(); // teamA belongs to archetype A // Player0 becomes a part of archetype B. w.add(player0, teamA); // Maximum lifespan of archetype B changed to 20. w.set_max_lifespan(player0, 20); // Player0 becomes a part of archetype A again. Lifespan of B is still 20, lifespan of A is default. w.del(player0, team1); ``` In case you want to affect an archetype directly without abstracting it away you can retrieve it via the entity's container returned by World::fetch() function: ```cpp EntityContainer& ec = w.fetch(player0); // Maximum lifespan of archetype the player0 entity belongs to changed to 50. ec.pArchetype->set_max_lifespan(50); ``` ## Data processing ### Query For querying data you can use a Query. It can help you find all entities, components, or chunks matching a list of conditions and constraints and iterate them or return them as an array. You can also use them to quickly check if any entities satisfying your requirements exist or calculate how many of them there are. By default, `ecs::Query` keeps cached query state locally in the query object. If you want identical query shapes to reuse one shared cache entry across the world, opt into `QueryCacheScope::Shared`. Queries can also carry a user-owned context pointer. Gaia-ECS does not allocate, copy, or destroy this data; it only stores the pointer on the query object and exposes it to iterator-style callbacks through `ecs::Iter::ctx()`. The pointer is not part of query identity, does not affect matching, and does not invalidate cached query state. Shared-cache queries with the same shape can still keep different context pointers. ```cpp struct MoveSettings { float dt; }; MoveSettings settings{1.0f / 60.0f}; ecs::Query q = w.query() .ctx(&settings) .all() .all(); q.each([](ecs::Iter& it) { auto* settings = static_cast(it.ctx()); auto pos = it.view_mut(); auto vel = it.view(); GAIA_EACH(it) { pos[i].x += vel[i].x * settings->dt; pos[i].y += vel[i].y * settings->dt; pos[i].z += vel[i].z * settings->dt; } }); ``` Queries also expose scheduling access metadata. Positive query terms already describe component reads and writes (`all()`/`or_()` read, `all()`/`or_()` write). If the callback touches data outside the query shape, declare it explicitly with `reads(Entity)`, `writes(Entity)`, or the typed adapters `reads()` / `writes()`. Use `main_thread()` for callbacks that are not safe to run on worker threads even when component access would otherwise allow it. These declarations are metadata only: they do not change query matching, hashing, shared-cache identity, or cache invalidation. ```cpp auto move = w.query() .all() .all(); auto bounds = w.query() .all() .writes(); // WorldBounds is updated manually inside the callback. auto uiUpload = w.query() .all() .main_thread(); // The graphics API must be called from the main thread. if (!move.can_run_parallel(bounds)) { // Add a dependency or keep one of the jobs on the serial path. } ``` Two queries conflict when both access the same id and at least one side writes it. Pair query terms are treated as matching/filtering metadata and do not imply component data access; if a pair id is used as an external scheduling key, declare it explicitly with `reads(pairEntity)` or `writes(pairEntity)`. Note, the first Query invocation of a cached query is always slower than the subsequent ones because internals of the Query need to be initialized. ### Simple query ```cpp ecs::Query q = w.query(); q.all(); // Consider only entities with Position // Iterate matching entities. q.each([&](ecs::Entity entity) { ... }); // Fill the entities array with entities with a Position component. cnt::darray entities; q.arr(entities); // Fill the positions array with position data. cnt::darray positions; q.arr(positions); // Calculate the number of entities satisfying the query const auto numberOfMatches = q.count(); // Check if any entities satisfy the query. // Possibly faster than count() because it stops on the first match. const bool hasMatches = !q.empty(); ``` More complex queries can be created by combining All, Or, Any (optional), and None: ```cpp ecs::Query q = w.query(); // Take into account everything with Position and Velocity (mutable access for both)... q.all(); q.all(); // ... may have Something, or may have SomethingElse (immutable access for both, it does not matter if none is present)... q.any().any(); // ... and no Player component... (no access done for no()) q.no(); ecs::Query q2 = w.query(); // Take into account everything with Position and Velocity (mutable access for both)... q2.all(); q2.all(); // ... at least Something or SomethingElse (immutable access for both, one of them has to be present)... q2.or_().or_(); // ... and no Player component... (no access done for no()) q2.no(); ``` All Query operations can be chained and it is also possible to invoke various filters multiple times with unique components: ```cpp ecs::Query q = w.query(); // Take into account everything with Position (mutable access)... .all() // ... and at the same time everything with Velocity (mutable access)... .all() // ... at least Something or SomethingElse (immutable access)... .or_() .or_() // ... and no Player component (no access)... .no(); ``` `all(...)` requires the term, `any(...)` keeps the term optional, `or_(...)` creates an OR-chain that requires at least one OR term. ```cpp struct Cable {}; struct Device {}; struct Powered {}; ecs::World w; const ecs::Entity cablePlain = w.add(); w.add(cablePlain); const ecs::Entity cableDevice = w.add(); w.add(cableDevice); w.add(cableDevice); const ecs::Entity cablePowered = w.add(); w.add(cablePowered); w.add(cablePowered); const ecs::Entity cableBoth = w.add(); w.add(cableBoth); w.add(cableBoth); w.add(cableBoth); ecs::Query qAll = w.query().all().all(); qAll.count(); // expected: 2 (cableDevice, cableBoth) ecs::Query qAny = w.query().all().any(); qAny.count(); // expected: 4 (cablePlain, cableDevice, cablePowered, cableBoth) ecs::Query qOr = w.query().all().or_().or_(); qOr.count(); // expected: 3 (cableDevice, cablePowered, cableBoth) ecs::Query qExpr = w.query().add("Cable, Device || Powered"); qExpr.count(); // expected: 3 (cableDevice, cablePowered, cableBoth) ``` OR terms never duplicate matches. If an entity/archetype satisfies more than one OR term, it is still returned once. When no `all(...)` terms are present, chaining multiple `or_(...)` terms still means logical OR. ```cpp struct Marker {}; struct A {}; struct B {}; ecs::World w; const ecs::Entity e = w.add(); w.add(e); w.add(e); w.add(e); ecs::Query q = w.query() .all() .any() .any(); q.count(); // expected: 1 (entity `e` is matched once) ecs::Query qOr = w.query() .all() .or_() .or_(); qOr.count(); // expected: 1 ecs::Entity e1 = w.add(); ecs::Entity e2 = w.add(); w.add(e1, e1); w.add(e2, e2); ecs::Query qAny = w.query() .or_(e1) .or_(e2); qAny.count(); // expected: 2 (matches entities with e1 OR e2) ecs::Query qBad = w.query().or_(); qBad.count(); // expected (Debug): assertion failure, use all() or any() ``` ### Query traversal More advanced lookup settings are supported via `QueryTermOptions`. This includes source selection, traversal by relation (`ChildOf` by default), traversal filtering (`trav`, `trav_up`, `trav_parent`, `trav_self_parent`, `trav_down`, `trav_child`, `trav_self_down`, `trav_self_child`, `trav_depth`), and access type (read or write). ```cpp struct Position {}; struct Level { int value; }; ecs::World w; const ecs::Entity level = w.add().entity; const ecs::Entity game = w.add(); const ecs::Entity root = w.add(); const ecs::Entity parent = w.add(); const ecs::Entity scene = w.add(); w.child(parent, root); w.child(scene, parent); // Create 64 entities with Position. for (int i = 0; i < 64; ++i) { ecs::Entity e = w.add(); w.add(e); } // Fixed source lookup. Requires Level on `game`. ecs::Query qSrc = w.query() .all() .all(level, ecs::QueryTermOptions{}.src(game)); w.add(game, {1}); qSrc.count(); // expected: 64 w.del(game); qSrc.count(); // expected: 0 // Hierarchical source lookup with default traversal filter (self + all parents). ecs::Query qSelfUp = w.query() .all() .all(level, ecs::QueryTermOptions{}.src(scene).trav()); qSelfUp.count(); // expected: 0 w.add(root, {2}); qSelfUp.count(); // expected: 64 // Immediate parent only (no self, no grandparent). ecs::Query qParent = w.query() .all() .all(level, ecs::QueryTermOptions{}.src(scene).trav_parent()); qParent.count(); // expected: 0 (Level is on root, not on parent) // Self + immediate parent (no grandparent). ecs::Query qSelfParent = w.query() .all() .all(level, ecs::QueryTermOptions{}.src(scene).trav().trav_depth(1)); qSelfParent.count(); // expected: 0 (no Level on scene/parent) ``` ```cpp ecs::Query qFast = w.query() .all() .all(level, ecs::QueryTermOptions{}.src(scene).trav_up()); qFast.count(); // expected: 64, because root has Level and root is an ancestor of scene w.del(root); w.add(scene, {3}); qFast.count(); // expected: 0, because trav_up() checks ancestors only (it does not check scene itself) ecs::Query qDown = w.query() .all() .all(level, ecs::QueryTermOptions{}.src(root).trav_down()); qDown.count(); // expected: 64 if any descendant of root has Level // Depth control: 0 means unlimited traversal. ecs::Query qDownUnlimited = w.query() .all() .all(level, ecs::QueryTermOptions{}.src(root).trav_down().trav_depth(0)); ``` Query source traversal treats disabled entities as traversal barriers. Disabled sources are not matched, and descendants under a disabled source are not searched until that source is enabled again. If you know a traversed source closure is small and stable, you can opt into traversed-source snapshots explicitly: ```cpp ecs::Query qTravCached = w.query() .all() .all(level, ecs::QueryTermOptions{}.src(scene).trav()) .cache_src_trav(16); ``` This is not recommended as a blanket default. It is most useful for read-heavy queries with small traversal closures. Cached traversed-source snapshots also track hierarchy enabled-state changes, so disabled-subtree barriers invalidate the snapshot before reuse. ### Traversal order Use `order_by(relation, ecs::TravOrder::...)` when you want to reorder the current query result by a relationship traversal. It does not change what the query matches, only the order in which the current result is visited. Traversal orders are deterministic. Roots are visited by entity id. Siblings under the same target are also visited by entity id. A relation pair `Pair(Rel, X)` on entity `E` means `E` points at `X`. In the names below, `E` is the source and `X` is the target. - `TravOrder::Down`: visit targets before the sources that point at them. For `ChildOf`, this means parents before children. This is DFS preorder. `TravOrder::Preorder` is the alias. - `TravOrder::Up`: visit sources before their targets. For `ChildOf`, this means children before parents. This is DFS postorder. `TravOrder::Postorder` is the alias. - `TravOrder::ReverseDown`: exact reverse of `Down`. This is reverse DFS preorder. `TravOrder::ReversePreorder` is the alias. - `TravOrder::ReverseUp`: exact reverse of `Up`. This is reverse DFS postorder. `TravOrder::ReversePostorder` is the alias. There is no breadth-first traversal alias for `order_by(...)`. Use `depth_order(...)` for cached breadth-first top-down ordering on fragmenting acyclic relations. `order_by(...)` supports entity callbacks, typed callbacks, and regular `ecs::Iter&`. Entity and typed callbacks are the best optimized paths. The iterator-style paths can be slower on heavily reordered traversal results, because reordered entities often split the result into many small runs. Use them only for code that is not performance critical. ```cpp struct Time { int time; }; ecs::Entity buyGroceries = wld.add(); ecs::Entity boilWater = wld.add(); ecs::Entity chopVegetables = wld.add(); ecs::Entity cookDinner = wld.add(); ecs::Entity setTable = wld.add(); wld.add