Core Concepts
ECS Overview
Viber3D uses an Entity Component System (ECS) architecture via the koota library.
What is ECS?
ECS stands for Entity Component System, a data-driven architecture widely used in games:
- Entities are unique identifiers that represent in-game objects (e.g., players, enemies, bullets).
- Traits (often called components in other ECSs) hold pure data for those entities.
- Systems process matching entities to implement logic and behavior.
Koota is the ECS-based library that Viber3D uses under the hood. It’s optimized for real-time apps and XR experiences.
Why ECS?
- Flexibility
Entities can have any combination of traits—much more flexible than rigid inheritance trees. - Performance
ECS focuses on data-oriented design, allowing efficient queries and updates to large numbers of entities. - Maintainability
By separating data (traits) from logic (systems), your codebase remains organized and easy to reason about. - Composability
New features or behaviors can be added by creating new traits and systems without modifying existing code.
ECS in Viber3D
Below is a simplified example:
import { trait, createWorld } from 'koota'
// 1. Define traits (data)
export const Position = trait({ x: 0, y: 0, z: 0 })
export const Health = trait({ amount: 100 })
// 2. Create a world and spawn an entity
const world = createWorld()
world.spawn(Position({ x: 10, y: 5 }), Health({ amount: 50 }))
// 3. Query and update entities
world.query(Position, Health).updateEach(([pos, hp]) => {
// game logic that modifies position or health
})
SoA vs. AoS Storage
Koota supports two main ways of storing trait data:
- Schema-based (SoA): If you define a trait with an object literal, each property is stored in its own array. Ideal for numeric fields updated frequently (e.g., positions, velocities).
const Position = trait({ x: 0, y: 0, z: 0 }) // Internally, x, y, and z each become separate arrays
- Callback-based (AoS): If you define a trait with a callback, the entire trait is stored as a single object in an array of structures. Useful when storing complex objects (e.g.,
THREE.Mesh
, classes, or vectors).const Mesh = trait(() => new THREE.Mesh()) // Each entity that has Mesh gets its own THREE.Mesh() instance
World Traits
In Koota, your World
can also have traits (sometimes called “singleton” traits). These do not appear in standard entity queries but can be accessed with world.get(...)
and world.set(...)
. Use them for global data like time, input state, or configuration.
import { trait, createWorld } from 'koota'
const Time = trait({ delta: 0, current: 0 })
const world = createWorld()
world.add(Time)
// Now we can store time info globally
world.set(Time, (prev) => ({
current: performance.now(),
delta: performance.now() - prev.current
}))
Common Patterns
- Composition over inheritance: Build complex entities by combining small, focused traits.
- Use queries efficiently: Only query for traits you need.
- Clean up: Remove or destroy entities when they’re no longer needed (e.g., after a system determines they’re off-screen or at zero health).
Next Steps
- Dive into Entities to learn how to spawn, remove, and manage them.
- Check out Traits to see how to define data in your ECS world.
- See Systems for writing game logic.
- Explore Components for React/Three.js rendering using ECS data.
- Learn about Actions for centralizing world modifications.