Lab 023 – Anchored Bounce Box (mini-game)
A mini-game that uses a hand anchor to control a box to bounce a ball toward a target.
Overview
I made this little prototype in about two and a half hours this afternoon. The idea is to hit the cube with the ball, with as few collisions inside the chamber as possible. The twist is that the chamber orientation is controlled by an AnchorEntity attached to your index finger.
The assets for the game are primitive geometry and basic materials that I threw together in Reality Composer Pro. I use several concepts in this lab
- Spatial Tracking Session and Anchor Entity for the controls
- Loading assets from the Reality Kit Content bundle
- Basic collision tracking
- Physics on the ball and the chamber
- Reality View attachments for the menu
- SwiftUI state for the basic game loop.
Video demo
Full Lab Code
Warning: this code is kind of a mess, but you can get an idea of how things work
struct Lab023: View {
// ARKit
@State var session = SpatialTrackingSession()
@State var handControl: Entity?
// Game State
@State var gameActive = false
@State var gameWon = false
@State var score: Int = 0
// These will be replaced with entities from the RCP scene
@State var chamber = Entity()
@State var ball = Entity()
@State var box = Entity()
// Stash the collision event
@State var collisionBeganSubject: EventSubscription?
var body: some View {
RealityView { content, attachments in
if let scene = try? await Entity(named: "PhysicsPlayground", in: realityKitContentBundle) {
content.add(scene)
guard let chamber = scene.findEntity(named: "Chamber") else { return }
chamber.setPosition([0, 1.4, -2], relativeTo: nil)
chamber.setScale([0.5, 0.5, 0.5], relativeTo: nil)
self.chamber = chamber
if let ball = scene.findEntity(named: "Ball"), let box = scene.findEntity(named: "Box") {
self.ball = ball
self.box = box
collisionBeganSubject = content.subscribe(to: CollisionEvents.Began.self, on: ball) { collisionEvent in
if(collisionEvent.entityB == box) {
print("subject collision \(collisionEvent.entityB)")
gameWon = true
gameOver()
}
if(gameActive) {
score += 1
}
}
}
if let handControl = scene.findEntity(named: "HandControl") {
self.handControl = handControl
}
if let gameMenu = attachments.entity(for: "GameMenu") {
gameMenu.setPosition([0, -1.2, 1.1], relativeTo: chamber)
gameMenu.scale = [2,2,2]
content.add(gameMenu)
}
}
} update: { content, attachments in
} attachments: {
Attachment(id: "GameMenu") {
VStack {
HStack {
Button(action: {
gameWon = false
gameOver()
}, label: {
Text("Stop")
})
Button(action: {
startGame()
}, label: {
Text("Restart")
})
}
if(gameWon && score > 0) {
Text("You Won in \(score) bounces!")
} else {
Text("Collide with the box!")
}
}
.padding(10)
.frame(width: 400, height: 100)
.background(Color.black)
.clipShape(.capsule)
}
}
.persistentSystemOverlays(.hidden)
.upperLimbVisibility(.hidden)
.task {
await runTrackingSession()
}
.task {
while true {
// If the game is active, use the anchor orientation to tilt the chamber
if gameActive {
if let anchor = handControl {
let anchorTransform = Transform(matrix: anchor.transformMatrix(relativeTo: nil))
// Create a new transform that uses position and scale from the chamber, and rotation from the anchor
var transform = Transform()
transform.translation = chamber.position
transform.scale = chamber.scale
transform.rotation = anchorTransform.rotation
// Use move(to:...) to smooth out the orientation changes
chamber.move(to: transform, relativeTo: nil, duration: 0.03)
}
}
try? await Task.sleep(for: .seconds(1/30))
}
}
}
func runTrackingSession() async {
let configuration = SpatialTrackingSession.Configuration(tracking: [.hand])
await session.run(configuration)
}
func startGame() {
// Position the ball and box for another round
ball.setPosition([0, 0.5, 0], relativeTo: ball.parent)
box.setPosition([Float.random(in: -0.9...0.9), Float.random(in: -0.9...0.9), Float.random(in: -0.9...0.9)], relativeTo: box.parent)
// Reenable gravity
if var ballPhysics = ball.components[PhysicsBodyComponent.self] {
ballPhysics.isAffectedByGravity = true
ball.components.set(ballPhysics)
}
// Start the game
ball.isEnabled = true
box.isEnabled = true
gameActive = true
gameWon = false
score = 0
}
func gameOver() {
gameActive = false
// clear all velocity on the ball
if var ballMotion = ball.components[PhysicsMotionComponent.self] {
ballMotion.angularVelocity = [0,0,0]
ballMotion.linearVelocity = [0,0,0]
ball.components.set(ballMotion)
}
// disable gravity
if var ballPhyics = ball.components[PhysicsBodyComponent.self] {
ballPhyics.isAffectedByGravity = false
ball.components.set(ballPhyics)
}
// Reset the box and disable it
box.setPosition([0.25, 0.5, 0], relativeTo: box.parent)
box.isEnabled = false
ball.isEnabled = false
// Reset the chamber orientation
let resetTransform = Transform()
chamber.setOrientation(resetTransform.rotation, relativeTo: nil)
// Fire a particle burst
if(gameWon) {
if var fireworks = chamber.components[ParticleEmitterComponent.self] {
fireworks.burst()
chamber.components.set(fireworks)
}
}
}
}Support our work so we can continue to bring you new examples and articles.
Download the Xcode project with this and many more labs from Step Into Vision.

Follow Step Into Vision