Skip to content

Concepts

Shell ECS is built around a simple idea: composition over inheritance. Instead of building a game by subclassing a monolithic GameObject class, you assemble behavior from small, reusable pieces of data (components) and logic (systems). This separation makes code easier to reason about, test, and optimize.

This guide explains the core concepts and the design philosophy behind the engine. For concrete examples, see the Getting Started guide and the Reference documentation.

An ECS splits a game object into three distinct ideas:

  • Entity — A lightweight, unique identifier. It has no data or behavior of its own. It is simply a container that holds components.
  • Component — A plain data structure. A position, a health value, a sprite reference. Components contain no logic.
  • System — A function that processes entities with specific components. Systems contain all the logic.

This separation makes dependencies explicit. A system declares exactly which components it needs, and the engine finds every entity that matches. There are no hidden references, no deep inheritance trees, and no surprise side effects from a distant parent class.

Under the hood, Shell ECS stores component data in a Structure of Arrays (SoA) layout. Each component field lives in its own contiguous typed array (Float32Array, Int32Array, etc.) grouped by archetype. This is the most cache-friendly layout for bulk iteration because the CPU can stream through memory sequentially without chasing pointers.

Raw SoA access is fast but tedious. Most game logic does not need to squeeze every last microsecond out of the CPU — it needs to read and write component data clearly. The engine bridges this gap by offering two ways to access the same underlying data:

  • AoS-style access — Per-entity proxies that feel like plain objects. You read and write entity.transform.x and the engine routes the access to the correct typed array index. This is the default for game logic, event handling, and spawning where clarity matters more than raw throughput.
  • SoA-style access — Direct typed array batches per archetype. A tight for loop over contiguous memory with no proxies or function calls. This is for hot paths — physics, particle systems, spatial hashing — where entity counts are high and every cycle counts.

Both modes read from and write to the same underlying storage. You do not commit to one style at compile time. Use AoS-style by default, and switch to SoA-style when profiling tells you to.

The SoA layout is not the only performance-oriented decision in the engine.

  • Archetype-based storage — Entities with the same set of components live in the same archetype. When a system queries for Transform and Velocity, it only touches archetypes that contain both. This eliminates the need for per-entity bitmask checks during iteration.
  • Change detection — The engine tracks which archetypes have been modified since the last time a system ran. A system that only reacts to changes can skip entire archetypes without inspecting a single entity.
  • Batch iteration — The iter() API yields one batch per archetype with direct array access. This avoids the overhead of per-entity function calls and proxy traps.
  • Typed arrays — Numeric fields are backed by Float32Array, Int32Array, and Uint8Array. This keeps memory compact and reduces garbage collection pressure.

These features are not opt-in. They are the default architecture. You get them for free by using the engine as designed.

ECS enforces a natural boundary between data and logic. Because components are pure data and systems are pure functions, the codebase tends to organize itself:

  • No hidden state — A system can only access what it declares in its query. You cannot accidentally touch a component you did not ask for.
  • Parallelizable by default — Because systems declare their read and write dependencies explicitly, the scheduler can reason about which systems can run concurrently. Two systems that only read the same component do not conflict.
  • Testable in isolation — A system is just a function that receives a query, commands, and resources. You can unit test it by constructing a world, spawning a few entities, and running the system directly.
  • Reusable logic — A system that moves entities with Transform and Velocity does not care whether those entities are players, enemies, or projectiles. The same system works for all of them.

Almost every definition in the engine — components, systems, prefabs, plugins, events — is created through an immutable builder. Chaining methods returns a new instance rather than mutating the original.

This has a few practical benefits:

  • Safe reuse — Define a base prefab and create specialized variants without mutating the original.
  • Predictable configuration — Plugin and scene configurations are snapshots. You can pass them around without worrying about a later mutation changing behavior.
  • Type-safe composition — Builders carry type information forward. A prefab with a named child slot knows its slot names at the type level, so spawned.get('sword') is statically checked.

Runtime state — entities, archetypes, queries, and component data — is mutable. The engine is optimized for fast mutations at runtime, but the definitions that describe those mutations are stable.

Plugins are self-contained bundles of configuration. A plugin registers components, systems, and resources as a single unit. This makes it easy to share and reuse whole features — a physics plugin, a networking plugin, a rendering plugin — without manually wiring each piece together.

Plugins can declare dependencies. The engine resolves them in topological order and detects circular dependencies. Plugin groups let you bundle multiple plugins together and selectively disable or override individual ones.

This modularity is especially useful for large projects and teams. One developer can own the physics plugin, another can own the AI plugin, and both can be developed and tested in isolation before being dropped into the same world.

Scenes are child worlds that share some infrastructure with their parent while keeping their own entities and systems isolated. A scene inherits the component registry, scheduler, and ticker from its parent, but it gets its own entity registry, command buffer, and system instances.

Queries span the complete root/scene family. When a query returns an entity owned by another scene, entity-targeted commands route to that entity’s registry automatically. Spawning and UID-based commands remain local, and hierarchy or stored entity references cannot cross registries.

This is useful for:

  • Level loading — Each level is a scene with its own entities and systems.
  • UI layers — A HUD scene that runs on top of the gameplay scene without polluting the gameplay entity registry.
  • Networked rooms — Each networked room is a scene with isolated replication state.

Serialization works across the scene hierarchy automatically. A full world snapshot includes all child scenes, and loading restores them without manual bookkeeping.

The engine runs systems on a fixed schedule. There are startup schedules (run once when the world mounts), per-frame schedules (variable timestep), fixed schedules (deterministic timestep for physics), and render schedules (drawing).

Within a schedule, systems can declare ordering constraints: run before this system, run after that system. Systems can also be grouped into system sets that share ordering constraints and run conditions. The scheduler topologically sorts systems each frame based on these constraints.

This explicit ordering replaces the implicit priority systems found in many game engines. You do not guess what runs first. You declare it.

Resources are shared objects stored in the world’s IoC container. They are the escape hatch for global state that does not fit into the ECS model — a score tracker, a network socket, a random number generator.

Systems access resources through their update parameters. The container is typed, so resources.get('gameState') returns the correct type without casting. You can bind classes, factories, or plain instances.

This keeps global state explicit and testable. A system that depends on a resource declares that dependency by using it. You can mock the resource in tests by rebinding the container before the test runs.

The engine is designed with serialization in mind from the start. Components must explicitly opt in to serialization with .serializable(). Only serializable components participate in save/load and networking.

This opt-in design means you can have internal, ephemeral state — caches, transient flags, runtime handles — without worrying about it leaking into save files or network packets. The serialization system can snapshot the entire world, load a full state replacement, or apply an authoritative patch without stopping the game loop.

Shell ECS is built for projects where performance and structure matter. The SoA architecture gives you cache-friendly bulk iteration. The ECS model gives you explicit dependencies and testable logic. Immutable builders give you safe composition. Plugins and scenes give you modularity and isolation. And serialization is built in, not bolted on.