Develop
Develop
Select your platform

Entity-Component-System (ECS)

Updated: Sep 15, 2025

Overview

The Entity-Component-System (ECS) architecture pattern breaks down every object in your spatial scene into three simple parts: entities (objects in your application that group related components together), components (data like position, appearance, or behavior settings), and systems (code that processes entities with specific components).
Here is an example of a drone entity that makes sounds while moving:

Video: ECS creating drone behavior

Its simplified ECS composition is represented by this diagram:
An example of how the ECS pattern operates to create behavior for a drone entity

An example of how the ECS pattern operates to create behavior for a drone entity

ECS in Spatial SDK

ECS architecture is the foundation of Spatial SDK. This approach makes your spatial applications more flexible, maintainable, and performant by letting you compose objects from small, reusable components instead of inheriting from multiple classes. This is especially powerful in spatial applications where objects often need diverse, changing behaviors.

Entities: unique identifiers

Entities represent the objects in your application. They are lightweight containers that group related components together. They don’t contain data themselves. They’re organizational labels that group related components together.
val droneEntity = Entity.create()       // Gets unique ID like 100001
val playerEntity = Entity.create()      // Gets unique ID like 100002
val environmentEntity = Entity.create() // Gets unique ID like 100003
Think of entities like dishes on a restaurant menu: “Chicken Parmesan”, “Greek Salad”, or “Chocolate Cake”. Each dish has a name and identity, but what makes it that specific dish is the combination of ingredients that go into it. The objects in your scene are entities.

Components: data containers

Components store data and define what an entity is or has, but not what it does. Each component focuses on one specific aspect. This component code matches to the diagram above:
// Each component represents one aspect of the drone
val droneEntity = Entity.create(listOf(
    Transform(Pose(Vector3(0f, 2f, 0f))),           // Position in space
    Mesh(Uri.parse("apk:///assets/drone.glxf")),    // 3D appearance
    Grabbable(),                                    // Can be grabbed
    Audio(Uri.parse("apk:///assets/hum.wav"))       // Sound data
))
Components are like ingredients in your kitchen. Flour has properties like being powdery and providing structure, but it doesn’t become bread by itself. Lettuce is crunchy and fresh, but it doesn’t make a salad on its own. Each ingredient has specific properties that cooking processes can work with to create the entity.

Systems: behavior processors

Systems contain all the logic. They query for entities with specific component combinations and process their data:
class MovementSystem : SystemBase() {
    override fun execute() {
        // Find entities that can move (have both Transform and Animated)
        Query.where { has(Transform.id, Animated.id) }.eval().forEach { entity ->
            // Process movement logic here
        }
    }
}
Systems are like the cooking processes: baking, sautéing, chopping, or seasoning. The movement system knows how to work with position and animation ingredients to create smooth motion. The rendering system knows how to combine mesh and material ingredients to display objects visually. Each system applies specific techniques to transform the right components into working functionality.

Benefits for spatial development

  • Flexibility and reusability: Components can be mixed and matched freely. A Transform component works the same whether it’s on a drone, a UI panel, or a 3D model. A Grabbable component can make any entity interactive.
  • Performance optimization: Systems only process entities that have the components they need. A physics system ignores entities without physics components, making large scenes efficient.
  • Easy debugging and testing: Each component and system has a single responsibility, making it easier to isolate and fix issues. You can test movement logic separately from rendering logic.
  • Dynamic composition: Add or remove components at runtime to change entity behavior. Turn a static object into a moving one by adding a Physics component.

Inspecting ECS with Data Model Inspector

The Data Model Inspector (DMI) provides a real-time view of your ECS architecture in action. Through Android Studio, you can see all entities in your scene, examine their components and attribute values, and watch them update as your systems process them. DMI lets you temporarily modify component attributes without rebuilding, making it an essential tool for understanding and debugging ECS behavior. You can select entities either by clicking them in your 3D scene or in the inspector table, search and filter by component types or values, and see exactly how your entity-component relationships work in practice.

Thinking with ECS

When designing your spatial application, think in terms of composition:
  1. Identify your objects: What things exist in your scene? (entities)
  2. Break down their properties: What data does each object need? (components)
  3. Define their behaviors: What should these objects do? (systems)
Start simple with basic entities and gradually add complexity. The modular nature of ECS makes it easy to iterate and expand your application over time.

Composition over inheritance

Instead of creating complex class hierarchies, compose entities from simple components:
// Traditional OOP approach - becomes difficult to maintain
// This hypothetical example shows what NOT to do
class InteractivePhysicsBall : MovableObject, TriggerHandler, InputListener, CollisionDetector {
    // Complex inheritance hierarchy with potential conflicts
    // Difficult to add new behaviors or remove existing ones
    // Hard to reuse individual behaviors in other objects
}
// ECS approach: Simple composition from reusable components
// From StarterSample: Simple composition approach
Entity.create(
    listOf(
        Mesh(Uri.parse("mesh://skybox"), hittable = MeshCollision.NoCollision),
        Material().apply {
            baseTextureAndroidResourceId = R.drawable.skydome
            unlit = true // Prevent scene lighting from affecting the skybox
        },
        Transform(Pose(Vector3(x = 0f, y = 0f, z = 0f)))))
// Avoid: Complex inheritance hierarchies
class SkyboxMeshMaterialTransform : Renderable, Positioned, Textured {

}

Single responsibility

Each component should represent one concept, and each system should handle one behavior:
<!-- From PhysicsSample: Simple component with a single responsibility -->
<ComponentSchema packageName="com.meta.spatial.samples.physicssample">
  <Component name="Spinner" description="A Component that spins an object.">
    <FloatAttribute name="speed" defaultValue="50.0f" />
  </Component>
</ComponentSchema>
<!-- Avoid: Component that handles multiple concerns -->
<Component name="Player">
  <IntAttribute name="health" />        <!-- Game state -->
  <Vector3Attribute name="position" />  <!-- Positioning -->
  <StringAttribute name="weaponType" /> <!-- Equipment -->
  <IntAttribute name="experience" />    <!-- Progression -->
</Component>

Data-driven design

Store configuration in components, not in system code:
// From CustomComponentsSample
val robot = composition.getNodeByName("robot").entity
val basketBall = composition.getNodeByName("basketBall").entity

// add the LookAt component to the robot so it points at the basketBall
robot.setComponent(LookAt(basketBall, offset = Vector3(x = 0f, y = 180f, z = 0f)))
This example shows how configuration (the target entity and offset) is passed through component data rather than hard-coded in the system. The LookAt component’s properties like speed and offset are defined in the component schema:
<?xml version="1.0" ?>
<ComponentSchema packageName="com.meta.spatial.samples.customcomponentssample">
  <Enum name="LookAtAxis">
    <EnumValue value="ALL" />
    <EnumValue value="Y" />
  </Enum>
  <Component name="LookAt">
    <EntityAttribute name="target" />
    <BooleanAttribute name="lookAtHead" defaultValue="false" />
    <EnumAttribute name="axis" defaultValue="LookAtAxis.ALL" />
    <FloatAttribute name="speed" defaultValue="0.15f" />
    <Vector3Attribute name="offset" defaultValue="0.0f, 0.0f, 0.0f" />
  </Component>
</ComponentSchema>

SpatialFeatures: organizing ECS functionality

While ECS provides the architectural foundation, SpatialFeatures offer a higher-level organizational pattern that builds directly on ECS concepts. Rather than manually registering individual components and systems, SpatialFeatures let you package complete ECS-based functionality into reusable modules.

When to use ECS directly vs. SpatialFeatures

Use ECS directly when:
  • Creating simple, app-specific components and systems
  • Building custom behavior unique to your application
  • Learning the fundamentals of entity-component architecture
  • Fine-tuning performance for specific use cases
Use SpatialFeatures when:
  • Building complete functional modules (like physics, networking, or input handling)
  • You want to share functionality across multiple projects
  • You need bundled components, systems, and lifecycle management
  • Working with complex feature sets that require coordinated initialization
This architectural pattern transforms the granular ECS approach into organized, shareable features that maintain all the flexibility and performance benefits of the underlying entity-component-system structure.

Next steps

Now that you understand the basics of ECS architecture, learn more about each piece:
Did you find this page helpful?
Thumbs up icon
Thumbs down icon