Develop

Physics sample

Updated: May 8, 2026

Overview

This sample demonstrates interactive physics-based experiences using the Meta Spatial SDK. A marble run showcases balls rolling through trigger zones and navigating moving obstacles, powered by the physics engine, custom Entity-Component-System (ECS) components and systems, and scene composition with Meta Spatial Editor.

Learning objectives

Complete this guide to learn how to:
  • Physics integration: Configure the physics engine and transition entities between STATIC, DYNAMIC, and KINEMATIC states
  • Custom components: Define reusable behaviors using XML component schemas (Button, Trigger, TriggerArea, Spinner, UpAndDown)
  • Custom systems: Extend SystemBase to implement frame-by-frame logic for input handling, spatial overlap detection, and animation
  • Entity events: Coordinate interactions between systems and activity code using registerEventListener and sendEvent with custom event payloads
  • Scene composition: Use Meta Spatial Editor to position entities and configure physics properties, then load the composition with glXFManager

Requirements

  • Meta Quest 2, Quest 3, or Quest 3S with Horizon OS
  • Android Studio (recommended) or another Kotlin-capable IDE
  • Spatial SDK development environment
For detailed setup instructions, see the sample’s README and the Spatial SDK setup guide.

Get started

Clone the Meta Spatial SDK Samples repository or download the PhysicsSample directly. Open the PhysicsSample directory in Android Studio, connect your Meta Quest device, and run the project. The sample loads a marble run scene with three colored balls positioned at the top of a ramp track. When you press the button, the balls launch and interact with ramps, trigger zones, and moving obstacles. For build configuration and dependency details, refer to the sample’s README.

Explore the sample

File / SceneWhat it demonstratesTopics covered
PhysicsSampleActivity.kt (BallRunActivity class)
Main activity orchestrating physics features, component registration, glXF scene loading, event listeners, and game state transitions
AppSystemActivity, PhysicsFeature, glXFManager.inflateGLXF(), Entity.registerEventListener(), Physics component manipulation
ButtonSystem.kt
Frame-by-frame input handling and smooth animation of a 3D button using lerp
SystemBase.execute(), Query.where, SceneObject.addInputListener(), DataModel.sendEvent()
TriggerSystem.kt
Spatial overlap detection between balls and trigger zones, dispatching custom events with direction and force values
SystemBase, AABB overlap testing, TriggerEventArgs, Quaternion.times() for direction calculation
SpinnerSystem.kt
Continuous Y-axis rotation applied to kinematic entities
SystemBase, Quaternion composition with delta time
UpAndDownSystem.kt
Sinusoidal vertical animation with staggered phase offsets for multiple platforms
SystemBase, changed(Mesh.id) query, sin() animation
Button.xml, Trigger.xml, TriggerArea.xml, Spinner.xml, UpAndDown.xml
Custom component schema definitions with typed attributes
XML component registration, StringAttribute, FloatAttribute, Vector3Attribute
TriggerEventArgs.kt
Custom event payload carrying direction and force values
Extending EventArgs with custom data
Main.metaspatial / Main.scene
Spatial Editor project and scene composition defining entity positions, physics properties, and component assignments
Scene composition, component attribute configuration in the editor
InfoPanel.kt, WelcomePanel.kt
Compose-based UI panels using the Spatial UISet theme
ComposeViewPanelRegistration, SpatialTheme, Jetpack Compose in 3D space

Runtime behavior

When you run this sample, you see a 3D marble run track inside a room environment. Three colored balls (purple, blue, green) rest at the top-left of the track. A welcome panel displays Physics Sample with a description, and below it a smaller info panel shows instructions: “Press the button to start the marbles rolling. Press it again to reset.” A physical button sits near the panel. Click the button to launch the balls. All three balls receive a forward force and roll down the ramp track under gravity. As they pass through invisible trigger zones, additional forces propel them forward and the balls visibly accelerate. A rotating block continuously spins, acting as an obstacle the balls must navigate around. Five smaller blocks oscillate up and down with staggered phases, creating moving platforms. When a ball reaches the finish zone at the bottom of the track, it freezes (transitions to KINEMATIC) and resets to the starting position. Once all three balls finish, the info panel reappears. Clicking the button again resets the run.

Key concepts

Physics states

The sample demonstrates all three physics states. Ramps and the environment use STATIC (immovable collision geometry). Balls transition between KINEMATIC (frozen) and DYNAMIC (gravity, forces, and collisions apply) as they launch and reset. The rotating block and moving platforms remain KINEMATIC throughout, so custom systems control their transforms while the physics engine still uses them for collision.
The sample launches balls by setting force and state on the Physics component:
physics.linearVelocity = Vector3(0f)
physics.state = PhysicsState.DYNAMIC
physics.applyForce = direction * Vector3(speed)
For the complete state transition logic, see PhysicsSampleActivity.kt. Learn more in the Physics documentation.

Custom components

Each custom component is defined as an XML schema with typed attributes. The UpAndDown component, for example, declares amount, speed, offset, and startPosition, which UpAndDownSystem reads each frame to animate vertical oscillation.
<Component name="UpAndDown">
  <FloatAttribute name="amount" defaultValue="0.1f" />
  <FloatAttribute name="speed" defaultValue="0.5f" />
</Component>
Components are registered in onCreate() with componentManager.registerComponent<UpAndDown>(UpAndDown.Companion). For the full set of schemas, see app/src/main/components/. The Custom components documentation uses this sample’s UpAndDown component as its primary teaching example.

Custom systems

Each system extends SystemBase and overrides execute(), which runs every frame. The sample uses Query.where to find entities with specific components, then reads and writes component data. SpinnerSystem applies a Y-axis rotation increment each frame using quaternion multiplication with delta time:
transform.transform.q =
    transform.transform.q * Quaternion(0f, spinner.speed * timeDeltaInSeconds, 0f)
entity.setComponent(transform)
For complete system implementations, see ButtonSystem.kt, TriggerSystem.kt, SpinnerSystem.kt, and UpAndDownSystem.kt. Learn more in the Systems documentation.

Entity events

The sample coordinates interactions between systems and activity code using entity events. TriggerSystem sends custom events when a ball enters a trigger zone, and the activity registers listeners on each ball to respond. TriggerEventArgs extends EventArgs to carry direction and force values as a custom payload.
The activity listens for trigger events on each ball entity:
ball.entity.registerEventListener<TriggerEventArgs>("speed_up") { entity, args ->
  shootBall(entity, args.direction, args.value)
}
TriggerSystem dispatches events via dataModel.sendEvent() when overlap is detected. For the complete event flow, see TriggerSystem.kt and PhysicsSampleActivity.kt. Learn more in the Events documentation.

Extend the sample

  • Add new obstacle types: Create a custom component and system to implement pendulums or rotating platforms with different axes
  • Experiment with physics properties: Modify restitution (bounciness) and friction in the scene file to change how balls interact with surfaces
  • Build on related concepts: Explore the MixedRealitySample to see physics combined with passthrough, or the Object3DSample for grabbable physics objects