MIT License

项目介绍

richardbiely/gaia-ecs 是一个托管在 GitHub 上的开源项目,主要使用 C++ 编写,采用 MIT License 许可证,目前获得 135 个 Star、7 次 Fork。

仓库全名
richardbiely/gaia-ecs
开源许可证
MIT License
Star 数
135
Fork 数
7
未关闭 Issue
5

README 摘要

[version]: https://github.com/richardbiely/gaia-ecs/releases [license]: https://en.wikipedia.org/wiki/MITLicense [language]: https://en.wikipedia.org/wiki/C%2B%2B17 [discord]: https://discord.gg/wJjK72yze2 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,…

完整内容见 GitHub 原始 README 或本页 。

常见问题

gaia-ecs 用什么语言开发?

主要使用 C++ 编写,采用 MIT License 许可证,目前有 135 个 Star。

gaia-ecs 可以免费商用吗?

仓库采用 MIT License 许可证,可免费使用与修改,商业使用请遵守许可证条款(例如保留版权声明与许可声明)。

如何获取 gaia-ecs 的源码?

可以直接克隆仓库:git clone https://github.com/richardbiely/gaia-ecs.git,默认分支为 main;安装与运行步骤见仓库 README。

gaia-ecs 最近还在维护吗?

仓库最近一次更新为 2 天前,当前有 5 个未关闭 Issue;Star 数为 135,可作为社区活跃度的参考。

关于

GitHub 原始描述(英文)

A simple and powerful entity component system (ECS) written in C++17

描述取自 GitHub 官方 API,保持仓库作者书写的原文。

语言占比

Batchfile
C
C++
CMake
Dockerfile
HTML
Makefile
PowerShell
Python
Shell

Release

README

gaia-ecs

Version license language discord codacy

Documentation

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, wildcards, hierarchy traversal (DFS/BFS), variables
  • Per-component AoS or SoA data layout with minimal code changes
  • Integrated compile-time and runtime serialization
  • Multithreading with job dependencies, including parallel ECS iteration
  • No external dependencies, no STL strings or containers
  • Single-header option — drop it in and go
  • Compiles fast, runs on all major compilers, even in web browser thanks to Emscripten

Quality:

  • Thoroughly documented — both public API and internals
  • Correctness ensured by thousands of unit tests, 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

ECS

Entity-Component-System (ECS) is an architectural pattern that organizes code around data rather than objects, following the principle of 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 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 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 hash-map.

Usage

Minimum requirements

#include <gaia.h>

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.

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.

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.

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.

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.

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.

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. 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.

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<gameplay_render::Position>().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:

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<gameplay_types::Position>();
  // the registered symbol is "gameplay_types::Position"
  // the scoped path is "gameplay.Position"

  w.scope(render, [&] {
    w.add<render_types::Position>();
    // 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.

namespace gameplay_render {
  struct Position {};
  struct Velocity {};
}

ecs::World w;

const ecs::Entity renderModule = w.module("gameplay.render");

w.scope(renderModule, [&] {
  w.add<gameplay_render::Position>();
  w.add<gameplay_render::Velocity>();
});

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:

struct Cooldown {
  GAIA_STORAGE(Sparse);
  float value = 0.0f;
};

ecs::World w;
auto e = w.add();
w.add<Cooldown>(e);
auto cooldownValue = w.set<Cooldown>(e);
cooldownValue.value = 1.5f;

GAIA_STORAGE(Table) is the default and usually does not need to be written. A typed component’s declared payload storage is authoritative for data-bearing structs: adding ecs::Sparse or ecs::DontFragment to a typed table payload such as Position does not change its storage. Empty tags are the exception. They can latch ecs::DontFragment after registration and before any instances exist. 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.

Empty tags do not need Sparse. Declare GAIA_STORAGE(DontFragment), or call world.add(componentEntity, ecs::DontFragment) immediately after registration and before any instances exist. Data-bearing AoS structs that should stay outside archetype identity declare GAIA_STORAGE(DontFragment) (sparse payload plus non-fragmenting membership) or GAIA_STORAGE(Sparse) plus a runtime DontFragment latch. Table payloads still require an archetype column, so a typed table data component cannot become non-fragmenting at runtime.

struct Pending {
  GAIA_STORAGE(DontFragment);
};

struct Cooldown {
  GAIA_STORAGE(DontFragment);
  float value = 0.0f;
};

ecs::World w;
auto e = w.add();
w.add<Pending>(e);
w.add<Cooldown>(e);
auto cooldownValue = w.set<Cooldown>(e);
cooldownValue.value = 1.5f;

An empty tag without a compile-time declaration can still latch before instances exist:

struct Pending {};

ecs::World w;
const auto& pending = w.add<Pending>();
w.add(pending.entity, ecs::DontFragment);

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
GAIA_STORAGE(DontFragment) empty tag Table (size 0) Outside archetype No
empty tag + runtime DontFragment before instances Table (size 0) Outside archetype No
GAIA_STORAGE(DontFragment) AoS payload Sparse Outside archetype No

Data-bearing relationships use the same payload/membership split. When the payload type of an exact (relation, target) pair uses sparse storage, Gaia keeps one separate sparse payload store for that exact pair. The pair id still participates in the entity’s archetype identity. This preserves normal relationship query matching without putting the payload in a chunk column. For an Exclusive relation marked DontFragment, both pair membership and its sparse payload stay outside the archetype, so changing the target does not move the source entity.

Empty Exclusive+DontFragment ids such as Parent and OnInstantiate remain relation traits. They do not take the empty-tag membership path.

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(DontFragment) for empty optional markers and for typed optional AoS state such as cooldowns, temporary status effects, or runtime tool state.
  • Queries whose required terms include a DontFragment id use entity-seed matching, same as other non-fragmenting ids. Queries that only exclude a DontFragment id still walk the table neighbor and apply per-entity filters. Adding the tag is cheap because the archetype does not change. Iterating or counting those tags is slower than iterating an archetype-resident empty tag. count(), arr(), typed each(), and Iter& each() all use the same filters, so it.size() and chunk views cover only matching rows. changed() on an empty DontFragment tag uses the same chunk-granular entity-order signal as other non-fragmenting ids.
  • GAIA_STORAGE(Sparse) plus a runtime DontFragment latch is equivalent for data-bearing AoS payloads.
  • 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:
Component layout and storage follow the same rules for ordinary components and relationship payloads. Table storage supports AoS and SoA layouts. Sparse storage is intended for individually addressed payloads and supports only plain AoS components. GAIA_STORAGE(Sparse), GAIA_STORAGE(DontFragment), and ecs::Sparse cannot be combined with a SoA layout.

NOTE:
Runtime component storage and fragmentation traits must be set before the component has instances attached to entities. They do not override a typed data component’s GAIA_STORAGE policy. Empty tags may still latch ecs::DontFragment before instances exist.

NOTE:
Once a component is marked ecs::Sparse or ecs::DontFragment, it stays that way. Both entities carry the ecs::Requires trait.

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.

// Check if entity e has Velocity (via world).
const bool hasVelocity = w.has<Velocity>(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<Position>().any<Velocity>(); 
q.each([&](ecs::Iter& it) {
  const bool hasPosition = it.has<Position>();
  const bool hasVelocity = it.has<Velocity>();
  ...
});

Providing entities is supported as well.

auto p = w.add<Position>().entity;
auto v = w.add<Velocity>().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<T> 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.

struct Position {
  float x, y, z;
};
const ecs::ComponentCacheItem& cci = w.add<Position>();
ecs::Entity position_entity = cci.entity;

Because components are entities as well, adding them is very similar to what we have seen previously.

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<Position>(e, {0, 100, 0});
w.add<Velocity>(e, {0, 0, 1});

// Remove Velocity from the entity.
w.del<Velocity>(e);

This also means the code above could be rewritten as following:

// Create Position and Velocity entities
ecs::Entity position = w.add<Position>().entity;
ecs::Entity velocity = w.add<Velocity>().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 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.

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.

ecs::World w;
const ecs::ComponentCacheItem& pos_item = w.add<Position>();
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<Position>(e);

Hooks can easily be removed:

const ecs::ComponentCacheItem& pos_item = w.add<Position>();
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<Position>(e);

It is also possible to set up a “set” hook. For explicit setter APIs such as w.set<T>(e) = ... and w.acc_mut(e).set<T>(...), the hook runs after the new value has been written back.

ecs::World w;
const ecs::ComponentCacheItem& pos_item = w.add<Position>();
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<Position>(e); // Don't trigger the set hook, yet
w.set<Position>(e) = {}; // Trigger the set hook
w.acc_mut(e).set<Position>({}); // Trigger the set hook
w.acc_mut(e).sset<Position>({}); // 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 - observe the query’s add-side event. A negative term maps removal of its excluded id to this event
  • OnDel - observe the query’s delete-side event. A negative term maps addition of its excluded id to this event
  • OnSet - a value of an already present component was explicitly written

Use .monitor() when the callback must run only when the complete query changes from unmatched to matched or from matched to unmatched. The callback reads the actual transition from it.event(): OnAdd means the entity entered the result and OnDel means it left. Monitoring observers evaluate membership before and after a relevant structural change, so they cost more than ordinary event observers.

w.observer()
  .monitor()
  .all<Position>()
  .no<Frozen>()
  .on_each([](ecs::Iter& it) {
    if (it.event() == ecs::ObserverEvent::OnAdd) {
      // The entity now matches both terms.
    } else {
      // The entity no longer matches both terms.
    }
  });

OnSet is triggered by APIs such as set<T>(entity), set<T>(entity, object), acc_mut(entity).set<T>(...), modify<T, true>(entity), and modify<T, true>(entity, object). It is not triggered by sset(...), modify<T, false>(...), or by the initial add<T>(entity, value) that creates the component.

set<T>(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<Position>(), 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.

ecs::World w;
w.observer()
  .event(ObserverEvent::OnAdd)
  .all<Position>()
  .all<Velocity>()
  .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<Position>(e);
// Observer will trigger for the entity "e" now, because both Position and Velocity were added to it
w.add<Velocity>(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<ecs::Entity>();
  // You can also access the view of components attached to the copied entities
  auto someView = it.view<SomeComponent>();
  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<Velocity>()
  .add<Position>();
// Commit changes. The observer will triggers now.
builder.commit();

Listening to removal of entities looks similar:

w.observer() //
  .event(ecs::ObserverEvent::OnDel)
  .no<Position>()
  .no<Acceleration>()
  .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<Position>(e);
// Observer will trigger for the entity "e" now, because both Position and Velocity were removed from it
w.del<Velocity>(e);

Listening to value changes uses OnSet:

uint32_t hits = 0;

w.observer()
  .event(ecs::ObserverEvent::OnSet)
  .all<Position>()
  .on_each([&](ecs::Entity entity, const Position& pos) {
    ++hits;
    (void)entity;
    (void)pos;
  });

ecs::Entity e = w.add();
w.add<Position>(e, {1.0f, 2.0f, 3.0f});
// No OnSet yet. This was the initial add.

w.set<Position>(e) = {4.0f, 5.0f, 6.0f};
// OnSet triggered once.

w.acc_mut(e).sset<Position>({7.0f, 8.0f, 9.0f});
// Still one hit. sset is silent.

w.modify<Position, true>(e);
// OnSet triggered again.

w.query().all<Position&>().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:

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:

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:

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, 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.

ecs::World w;

// Create an entity with Position. This is one archetype movement.
ecs::Entity e = w.add();
w.add<Position>();

// 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<Velocity>()
 // remove Position from entity e
 .del<Position>()
 // add Rotation to entity e
 .add<Rotation>()
 // 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.

ecs::EntityBuilder builder = w.build(e);
builder
  .add<Velocity>()
  .del<Position>();
if (some_condition) {
  builder.add<Rotation>();
}
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

// Change Velocity's value.
w.set<Velocity>(e) = {0, 0, 2};
// Same shape as above but silent: no world-version update, no hooks, no OnSet observers.
w.sset<Velocity>(e) = {4, 2, 0};

w.set<T>(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.

w.add<Position>(e, {1, 2, 3});

{
  auto pos = w.set<Position>(e);
  pos.x = 10;
  pos.y = 20;
  pos.z = 30;

  // The write-back did not happen yet.
  const auto& current = w.get<Position>(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<Position>(e);
// updated is {10, 20, 30}

If you need the write to happen immediately, use acc_mut(entity).set<T>(...) instead of w.set<T>(entity). For runtime object/component entities, the immediate form is acc_mut(entity).set<T>(object, value). Typed entity accessors also support GAIA_STORAGE(Sparse) and GAIA_STORAGE(DontFragment) payloads: acc(entity).get<T>() reads their sparse storage, and acc_mut(entity).set<T>(value) writes immediately, updates changed-query tracking, and emits OnSet after the write. mut<T>(), smut<T>(), and sset<T>(value) remain silent, matching the component-ID accessor overloads.

When setting multiple component values at once it is more efficient doing it via chaining:

w.acc_mut(e)
// Change Velocity's value on entity "e"
  .set<Velocity>({0, 0, 2})
// Change Position's value on entity "e"
  .set<Position>({0, 100, 0})
// Change...
  .set...;

Similar to ecs::EntityBuilder::build you can also use the setter object in scenarios with complex logic.

ecs::ComponentSetter setter = w.acc_mut(e);
setter.set<Velocity>({0, 0, 2});
if (some_condition)
  setter.set<Position>({0, 100, 0});
setter.set<Something>({ ... }).set<Else>({ ... });

// You can also retrieve a reference to data (for AoS) or the data accessor (for SoA)
auto& vel = setter.mut<Velocity>();
auto& pos = setter.mut<Position>();

The setter object supports the same immediate object-based form:

ecs::Entity runtimePos = w.add<Position>().entity;
w.add(e, runtimePos, Position{1, 2, 3});

ecs::ComponentSetter setter = w.acc_mut(e);
setter.set<Position>(runtimePos, Position{10, 20, 30});

setter.mut<T>() and w.mut<T>(e) are silent raw write paths. If you use them and want hooks or OnSet, call w.modify<T, true>(e) after finishing the write.

The same pattern applies to object-based writes:

ecs::Entity runtimePos = w.add<Position>().entity;
w.add(e, runtimePos, Position{1, 2, 3});

auto& pos = w.mut<Position>(e, runtimePos);
pos.x = 10;
pos.y = 20;
pos.z = 30;
w.modify<Position, true>(e, runtimePos);

Use the write path that matches the behavior you want:

  • set<T>(entity) - writes back on scope/full-expression end and then triggers set hooks and OnSet
  • set<T>(entity, object) - same as above for a specific runtime object/component entity
  • acc_mut(entity).set<T>(...) - writes immediately and triggers set hooks and OnSet
  • acc_mut(entity).set<T>(object, value) - immediate object-based write with set hooks and OnSet
  • sset<T>(entity) / mut<T>(entity) - silent write paths, no hooks, no OnSet
  • sset<T>(entity, object) / mut<T>(entity, object) - silent object-based write paths. Pair them with modify<T, true>(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.

// 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<Velocity>(e);
// However, it is easy to store a copy.
auto velCopy = w.get<Velocity>(e);

Both read and write operations are also accessible via views. Check the iteration sections to see how.

Copy entity

A copy of another entity can be easily created.

// 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);

copy, copy_n, copy_ext, and copy_ext_n clone into new ordinary entities. An exact pair record can be the source. The destination receives the pair’s components, sparse payloads, and non-fragmenting relation pairs, but not the pair identity.

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).

ecs::Entity e = w.add();
ecs::Entity something = w.add();
// Add a Position component to our entity
w.add<Position>(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<Position>(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.

// 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".
// Table component values are not copied. Non-fragmenting ids (DontFragment tags,
// DontFragment payloads, and relation pairs such as ChildOf) are copied.
// Fragmenting sparse ids receive a default payload. Names are not copied.
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<ecs::Entity>();
  // You can also access the view of components attached to the entity
  auto someView = it.view<SomeComponent>();
  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<ecs::Entity>();
  // You can also access the view of components attached to the copied entities
  auto someView = it.view<SomeComponent>();
  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.

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

A cleanup rule that deletes the entity along with something else, such as its ChildOf parent, deletes its components even while an ecs::SafeEntity references it. The entity is no longer valid, but its record stays reserved until the last ecs::SafeEntity goes out of scope, so the wrapper never touches a recycled entity.

An ecs::SafeEntity on a pair also holds its relation and its target. A pair id names its endpoints by id only, so the pair can only stay valid while both endpoints do, and while their ids cannot name anything else.

ecs::World w;
ecs::Entity likes = w.add();
ecs::Entity apples = w.add();
ecs::Entity player = w.add();
w.add(player, ecs::Pair(likes, apples));
{
  auto pairSafe = ecs::SafeEntity(w, ecs::Pair(likes, apples));
  // Deleting an endpoint waits for pairSafe, the same way deleting the pair would.
  w.del(likes);
  bool isValid = w.valid(likes); // true
  bool hasPair = w.has(player, ecs::Pair(likes, apples)); // true
}
// pairSafe released its references. likes is deleted, and the pair with it.
bool isValid = w.valid(likes); // false

A cleanup rule that deletes an endpoint deletes the pair as well, removing it from every entity that has it. As with an entity, the endpoint’s and the pair’s records stay reserved until the last ecs::SafeEntity goes out of scope, so no new entity reusing the endpoint’s id can match the old pair.

Just like an entity holding an ecs::SafeEntity on itself, an entity holding an ecs::SafeEntity on a pair that names it keeps itself alive against del(). Only a cleanup rule, or releasing the handle, deletes it.

ecs::SafeEntity can live inside a component. Cleaning up or destroying the world drops such references without deleting anything. When destroying such a component releases the last reference, for example because its entity is deleted or the component is removed, the entity it held is deleted once the operation that destroyed the component is done: right after the outermost del() returns, otherwise at the next update(). The same applies to a reference released inside query iteration.

Saving a world does not save ecs::SafeEntity references. A handle stored in a component loads empty. After loading, an entity can be deleted normally whatever held it before, an entity whose deletion waited for handles loads as a live entity, and one a cleanup rule already deleted finishes deleting at the next update(). An ecs::SafeEntity held outside the world must go out of scope before the world is cleaned up or destroyed. Unlike ecs::WeakEntity, it is not tracked, so it cannot be reset and would release a reference on whatever entity later reuses its id, or on a world that no longer exists.

ecs::SafeEntity is fully compatible with ecs::Entity and can be used just like it in all scenarios.

ecs::World w;
ecs::Entity player = w.add();
auto playerSafe = ecs::SafeEntity(w, player);
// Add a Position component to playerSafe (player)
w.add<Position>(playerSafe);
// w.add<Position>(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.

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<Position>(playerSafe);
// w.add<Position>(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.

ecs::WeakEntity can live inside a component, including a component that references its own entity. Deleting that entity destroys the component and resets every other ecs::WeakEntity pointing at it in the same step.

Cleaning up or destroying the world resets every ecs::WeakEntity that points into it, including ones held outside the world, so none of them can later match a recycled entity.

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
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 to the world's default.
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.

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:

EntityContainer& ec = w.fetch(player0);
// Maximum lifespan of archetype the player0 entity belongs to changed to 50.
ec.pArchetype->set_max_lifespan(50);

New archetypes start with the world’s default lifespan, 127 ticks unless you change it. Change it once when entities keep moving between a bounded set of archetypes, so that each one is created only once. Archetypes that already exist keep their lifespan, and ecs::World::cleanup keeps the setting.

ecs::World w;
// Archetypes created from now on stay, however often they empty.
w.set_default_max_lifespan(0);

Deletion outweighs the lifespan. An archetype goes once its rows are deleted by a cleanup rule, or once it is left empty because an entity among its ids was deleted, even when it was meant to stay. Nothing can use it again.

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.

struct MoveSettings {
  float dt;
};

MoveSettings settings{1.0f / 60.0f};

ecs::Query q = w.query()
  .ctx(&settings)
  .all<Position&>()
  .all<Velocity>();

q.each([](ecs::Iter& it) {
  auto* settings = static_cast<MoveSettings*>(it.ctx());
  auto pos = it.view_mut<Position>();
  auto vel = it.view<Velocity>();

  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. all(), or_(), and any() terms with payload infer read access from value or const types and write access from mutable references. Optional terms declare access even when no matching entity currently has the component. If the callback touches data outside the query shape, declare it explicitly with reads(Entity), writes(Entity), or the typed adapters reads<T>() / writes<T>(). Use main_thread() for callbacks that are not safe to run on worker threads even when component access would otherwise allow it. The reads(), writes(), and main_thread() declarations only affect scheduling metadata. They do not change query matching, hashing, shared-cache identity, or cache invalidation.

auto move = w.query()
  .all<Position&>()
  .all<const Velocity>();

auto bounds = w.query()
  .all<const Position>()
  .writes<WorldBounds>(); // WorldBounds is updated manually inside the callback.

auto uiUpload = w.query()
  .all<const MeshUpload>()
  .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 component or pair and at least one side writes it. Empty tags, entity IDs used as tags, and exact pairs without payload automatically use presence-only access. Pairs with payload infer read/write access like components. Wildcard and variable pairs keep their declared access because unresolved endpoints may carry data. Their scheduling conflicts include any overlapping pair access.

Use .no_access() after an all(), or_(), or any() term to match a component without reading or writing its data. It sets only the preceding term’s access to QueryAccess::Match and leaves matching unchanged. The term declares no data access, so it does not conflict with a writer of the component. System builders support the same modifier.

auto move = w.query()
  .all<Position&>() // Read and write Position.
  .all<const Acceleration>().no_access() // Require Acceleration without reading its data.
  .all<const Velocity>(); // Read Velocity.

move.each([](Position& pos, const Velocity& vel) {
  pos.x += vel.x;
});

struct Selected {};
struct Highlighted {};

auto selectedOrHighlighted = w.query()
  .or_<Selected>() // Empty tags automatically use presence-only access.
  .or_<Highlighted>();

Apply the modifier while building the query, bef</ecs::entity></ecs::entity></ecs::entity></gameplay_render::velocity></gameplay_render::position></render_types::position></gameplay_types::position></gameplay_render::position>

README 内容过长,已截取前 64 KB。完整内容请前往 GitHub 仓库查看。

评论

暂无评论