Getting Started: Jolt Physics in an XR Project
UntoldJoltPhysics installs
Jolt Physics behind the engine's
pluggable Physics Backend
seam. Rigid bodies described with the engine-owned RigidBodyComponent /
ColliderComponent are simulated by Jolt instead of the built-in kinetics
integrator — this gives you real collision detection, contacts, triggers and
a character controller, which matters most for XR scenes where hands and
real-world surfaces need to collide with gameplay objects.
This is a getting-started tutorial, not a full reference. See Physics System for the engine-owned component vocabulary and the UntoldJoltPhysics README for the complete backend reference.
1. Add the Dependency
// Package.swift
dependencies: [
.package(url: "https://github.com/untoldengine/UntoldEngine.git", branch: "develop"),
.package(url: "https://github.com/untoldengine/UntoldJoltPhysics.git", branch: "develop"),
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "UntoldEngine", package: "UntoldEngine"),
.product(name: "UntoldJoltPhysics", package: "UntoldJoltPhysics"),
]
),
]
Jolt is compiled from source (no binaries) and supports macOS 14+, iOS 17+ and visionOS 2+, device and simulator.
2. Register the Backend Before the Renderer
import UntoldJoltPhysics
// Must run before the renderer is created — the registry locks on the
// first simulated substep, so a backend can't be swapped in mid-run.
let jolt = registerJoltPhysics()
If an immersive space is closed and reopened, the registry is already locked from the previous run. Check for an existing backend first instead of registering again:
if let active = PhysicsBackendRegistry.shared.activeBackend() {
// Reuse it.
} else {
registerJoltPhysics()
}
3. Give Entities Physics Components
Jolt doesn't introduce new component types — it simulates the engine's own
RigidBodyComponent and ColliderComponent:
let ball = createEntity()
setEntityMeshAsync(entityId: ball, filename: "ball", withExtension: "untold")
registerComponent(entityId: ball, componentType: ColliderComponent.self)
registerComponent(entityId: ball, componentType: RigidBodyComponent.self)
if let collider = scene.get(component: ColliderComponent.self, for: ball) {
collider.shape = .sphere(radius: 0.12)
collider.restitution = 0.78
}
if let body = scene.get(component: RigidBodyComponent.self, for: ball) {
body.motionType = .dynamic
body.mass = 0.62
}
Size colliders to match the mesh's real-world scale — a size-7 basketball is about 0.12 m in radius. When a collider sits on a visible surface (a rim, a rope), give it a slightly larger radius than the visual geometry so contacts read as solid instead of grazing.
Watch for a mismatch between the collider's radius and the mesh's authored scale — a soccer ball model exported at 1 m diameter still needs its node scaled down to match an 0.11 m-radius collider, or the visual and the physics will disagree about where the ball's surface is:
4. XR Hands as Kinematic Bodies
Represent tracked hands as kinematic spheres so dynamic bodies collide against them, and park them far away when tracking is lost so they don't sweep through the scene on reacquisition:
let hand = createEntity()
registerComponent(entityId: hand, componentType: ColliderComponent.self)
registerComponent(entityId: hand, componentType: RigidBodyComponent.self)
if let collider = scene.get(component: ColliderComponent.self, for: hand) {
collider.shape = .sphere(radius: 0.07)
}
if let body = scene.get(component: RigidBodyComponent.self, for: hand) {
body.motionType = .kinematic
}
// Each frame:
translateTo(entityId: hand, position: trackedPosition ?? SIMD3<Float>(0, -100, 0))
JoltWorldSettings.maxKinematicStep / maxKinematicSpeed guard against a
hand that reappears far from where it was — without them, a tracking glitch
can look like a swat at full speed.
5. Stepping Happens Automatically
Once registered, Jolt steps every fixed update as part of the engine's
physics loop — there's no step() call to make yourself.
JoltWorldSettings.collisionSteps controls how many Jolt sub-steps run per
engine step; the default of 1 is fine for most scenes, and a higher value
helps fast-moving bodies or cloth stay stable.
6. Resetting a Body
Removing and re-adding RigidBodyComponent/ColliderComponent within the
same frame is invisible to the coordinator's per-substep diff, so it isn't a
reliable way to reset a body's pose. Use the backend's own reset instead:
if let jolt = PhysicsBackendRegistry.shared.activeBackend() as? JoltPhysicsBackend {
jolt.resetBody(entity: ball, position: spawnPoint, velocity: .zero)
}
This is the right tool for teleporting an entity back into play — e.g. a ball that fell out of bounds.
7. A Note on Damping
RigidBodyComponent has no damping field, and the Jolt backend does not
currently wire per-body linear/angular damping through for rigid bodies
(only soft bodies expose it). A Jolt-backed rigid body uses Jolt's own
default linear damping (0.05 in Jolt 5.6) with no way to configure or query
it through the engine seam yet — if you need exact ballistic trajectories,
you have to account for that decay yourself. If you need the kind of
damping control described in
Physics System
(applyLinearDamping, applyAngularDamping), note that those helpers act on
the built-in kinetics integrator and have no effect on entities simulated by
Jolt.
Next Steps
For a complete, working visionOS example — a basketball demo with hand tracking, a trigger-based scoring volume, real-world surface detection, and backend switching at launch — see CoolBasket, which runs on either its own built-in backend or UntoldJoltPhysics interchangeably.
For everything else the backend supports — layers, contact/trigger events, raycasts, the character controller and soft bodies — see the UntoldJoltPhysics README.