Systems contain the logic that brings your spatial applications to life. While components store data about what entities are and have, systems define what entities do. They run every tick, finding entities with specific component combinations and processing their data to create behaviors like movement, animation, user interaction, and physics simulation.
What systems do
Systems define what entities do by continuously processing entities that have specific component combinations:
Movement systems read Transform components to update positions
Rendering systems use Mesh and Material components to draw 3D objects
Input systems handle Grabbable components for user interaction
How systems work
Every system follows the same fundamental pattern:
Query: Find entities with the components you need.
Process: Read component data and apply your logic.
Update: Save changes back to the entities.
This pattern ensures data consistency and enables the ECS architecture’s performance benefits.
System and component processing lifecycle
Understanding the component lifecycle is crucial:
Retrieve: Get components with entity.getComponent<ComponentType>().
Modify: Change component data directly.
Persist: Save changes with entity.setComponent(component).
Always retrieve the latest component state, modify it, and save it back to ensure thread safety and data consistency.
Components provide the data
First, entities need the right components to store behavioral data. Let’s use the same UpAndDown component from the Components documentation that stores animation parameters:
// An entity with both positional and animation data
val animatedCube = Entity.create(listOf(
Transform(Pose(Vector3(0f, 1.5f, -2f))), // Position in 3D space
Mesh(Uri.parse("apk:///assets/cube.glxf")), // Visual representation
UpAndDown(
amount = 0.1f, // Animation distance (matches XML default)
speed = 0.5f, // Animation speed (matches XML default)
offset = 0.0f, // Current animation offset
startPosition = Vector3(0f, 1.5f, -2f) // Base position
)
))
Components store what the entity has: position data (Transform), animation configuration (UpAndDown), and visual representation (Mesh).
Systems provide the behavior
Now create an UpAndDownSystem that processes the UpAndDown component data to animate entities smoothly up and down. This example comes directly from the PhysicsSample:
import com.meta.spatial.core.Entity
import com.meta.spatial.core.Query
import com.meta.spatial.core.SystemBase
import com.meta.spatial.toolkit.Mesh
import com.meta.spatial.toolkit.Transform
import kotlin.math.sin
class UpAndDownSystem() : SystemBase() {
private var lastTime = System.currentTimeMillis()
private var entities = mutableListOf<Entity>()
public fun resetEntities() {
entities.clear()
}
private fun findNewObjects() {
val q = Query.where { changed(Mesh.id) and has(UpAndDown.id, Transform.id) }
for (entity in q.eval()) {
if (entities.contains(entity)) {
continue
}
val upAndDown = entity.getComponent<UpAndDown>()
val transform = entity.getComponent<Transform>()
upAndDown.startPosition = transform.transform.t
entity.setComponent(upAndDown)
entities += entity
}
}
private fun animate(deltaTimeSeconds: Float) {
for (entity in entities) {
val transform = entity.getComponent<Transform>()
val upAndDown = entity.getComponent<UpAndDown>()
// Calculate new y offset based on speed and time passed.
upAndDown.offset += (upAndDown.speed * deltaTimeSeconds)
upAndDown.offset %= 1f
// Update the y position set on the Transform component.
transform.transform.t.y =
upAndDown.startPosition.y - (sin(upAndDown.offset * Math.PI.toFloat()) * upAndDown.amount)
// update the components on the entity
entity.setComponent(transform)
entity.setComponent(upAndDown)
}
}
// Systems are run by calling the execute() function
override fun execute() {
val currentTime = System.currentTimeMillis()
// clamp the max dt if the interpolation is too large
val deltaTimeSeconds = Math.min((currentTime - lastTime) / 1000f, 0.1f)
findNewObjects()
animate(deltaTimeSeconds)
lastTime = currentTime
}
}
This system demonstrates the core pattern adapted from the PhysicsSample:
Query discovers new entities with changed(Mesh.id) and required components, then caches them for efficient processing.
Process uses frame-based delta timing to create smooth sine wave animation that’s independent of frame rate.
Update directly modifies component data and saves changes back to entities.
The system specializes in one behavior: creating smooth up-and-down motion for entities. The implementation uses several important patterns:
Entity caching: Avoids expensive queries every frame by tracking discovered entities.
Delta time animation: Ensures consistent animation speed regardless of frame rate.
Direct transform manipulation: Modifies transform.transform.t.y directly for performance.
Offset accumulation: Uses the offset component field to track animation progress over time.
Registering systems and components
Before systems can process components, both must be registered with your application. This typically happens in your activity’s onCreate() method:
class MyActivity : AppSystemActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Register custom components first
componentManager.registerComponent<UpAndDown>(UpAndDown.Companion)
// Then register systems that use those components
systemManager.registerSystem(UpAndDownSystem())
}
}
Components must be registered before the systems that use them, as systems need to know about component types during initialization.
Key design principles
These principles help you create maintainable and performant systems:
Single responsibility: Each system should handle one specific behavior or concern. The UpAndDownSystem only handles vertical animation, not collision detection or user input. This makes systems easier to understand, test, and modify.
Use component data: Systems should be stateless and work only with component data. Instead of storing object positions inside a system, read and write Transform components. This keeps data centralized and accessible to other systems.
Work with others: Systems coordinate by sharing component data, not by calling each other directly. This loose coupling makes systems more flexible and reusable.
These patterns make your spatial applications more modular and easier to extend with new behaviors.
Next steps
Learn how to create custom systems and how queries work: