This page walks you through building the LookAtSystem from the CustomComponentsSample that creates smooth look-at behavior for objects to face a target in your spatial scene. The system enables entities to smoothly rotate to face either the viewer’s head or another entity target. You’ll learn the practical steps of system implementation, from file creation to registration.
The LookAtSystem works with a LookAt component that defines look-at behavior parameters. This component acts as both a tag to identify entities for look-at behavior and a data container for properties like target entity and whether to look at the viewer’s head.
The component contains these essential fields:
target: Entity - The entity to look at (alternative to lookAtHead)
lookAtHead: Boolean - Whether to look at the viewer’s head (default: false)
The LookAtSystem teaches several essential concepts:
Component queries: How to find entities that have specific components.
Transform manipulation: How to modify object rotations in 3D space using quaternions.
Component lifecycle: The pattern of read → process → write that most systems follow.
Look-at mathematics: How to calculate rotations to face targets using quaternion operations.
Create a system
1. Create a new system file
Right-click on \app\src\main\java\your\project\folder.
Select New > File.
Create a new Kotlin file named LookAtSystem.kt.
2. Create the system class
Here’s the basic structure for a Spatial SDK system:
class CustomSystem : SystemBase() {
override fun execute() {
// System logic executes every frame
}
}
Key points:
Always extend SystemBase from com.meta.spatial.core.SystemBase.
Import all necessary components and utilities.
Use proper package naming conventions.
Override the execute() method for frame-by-frame logic.
3. Implement the core system logic
Let’s build a simplified LookAtSystem step-by-step. This simplified implementation creates instant rotation. Entities snap immediately to face their targets each frame. More advanced versions (like the full CustomComponentsSample) include smooth interpolation using spherical linear interpolation (slerp) for natural, animated transitions between rotations.
Start with the complete implementation:
import com.meta.spatial.core.Pose
import com.meta.spatial.core.Quaternion
import com.meta.spatial.core.Query
import com.meta.spatial.core.SystemBase
import com.meta.spatial.toolkit.Transform
class LookAtSystem : SystemBase() {
override fun execute() {
// 1. Query: Find entities that need to look at something
val query = Query.where { has(LookAt.id, Transform.id) }
// 2. Process: Calculate new rotation for each entity
for (entity in query.eval()) {
val lookAt = entity.getComponent<LookAt>()
val transform = entity.getComponent<Transform>()
// Get target position (could be head or another entity)
var targetPose: Pose = if (lookAt.lookAtHead) {
getScene()!!.getViewerPose()
} else {
lookAt.target.getComponent<Transform>().transform
}
// Calculate rotation to look toward target
var eyePose: Pose = transform.transform
val direction = (targetPose.t - eyePose.t)
val newRotation = Quaternion.lookRotation(direction)
// Note: This calculation creates a vector pointing toward the target.
// Use rotational offsets (like Vector3(0f, 180f, 0f)) to achieve the desired facing direction.
// See CustomComponentsSample for examples of using offsets to control final orientation.
// Apply the new rotation
eyePose.q = newRotation
// 3. Update: Save changes back to entity
entity.setComponent(Transform(eyePose))
}
}
}
Key concepts:
Query pattern: Uses Query.where { has(LookAt.id, Transform.id) } to find all entities with both components needed for look-at behavior.
Component retrieval: Gets the current LookAt and Transform components from each entity using getComponent<ComponentType>().
Target determination: Chooses between looking at the viewer’s head (getScene()!!.getViewerPose()) or another entity (lookAt.target.getComponent<Transform>().transform).
Direction calculation: Computes the direction vector by subtracting target position from entity position: (eyePose.t - targetPose.t). This creates a vector pointing away from the target, which is why rotational offsets are often needed.
Rotation calculation: Uses Quaternion.lookRotation(direction) to convert the direction vector into a rotation quaternion.
Transform update: Saves the new rotation back to the entity with entity.setComponent(Transform(eyePose)).
4. Register the system in main activity
Add your system to your main activity’s onCreate() method:
class MainActivity : AppSystemActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Register custom components
componentManager.registerComponent<LookAt>(LookAt.Companion)
// Register custom systems
systemManager.registerSystem(LookAtSystem())
}
}
Registration order:
Register components first.
Register systems second.
Load and setup scenes last.
This order ensures all dependencies are available when systems start executing.
Advanced features
The LookAt component and system shown here are simplified for learning. The full implementation in CustomComponentsSample includes additional features:
Smooth rotation: Uses interpolation for natural movement instead of instant snapping
Axis constraints: Can restrict rotation to specific axes (like Y-axis only for characters)
Rotation offsets: Applies directional corrections to achieve proper facing orientation
Speed control: Configurable rotation speed for different entity types
These advanced features follow the same Query → Process → Update pattern, just with more sophisticated processing logic.
Next steps
Learn about queries and bundling ECS modules into SpatialFeatures: