View in English

  • Apple Developer
    • Get Started

    Explore Get Started

    • Overview
    • Learn
    • Apple Developer Program

    Stay Updated

    • Latest News
    • Hello Developer
    • Platforms

    Explore Platforms

    • Apple Platforms
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    • App Store

    Featured

    • Design
    • Distribution
    • Games
    • Accessories
    • Web
    • Home
    • CarPlay
    • Technologies

    Explore Technologies

    • Overview
    • Xcode
    • Swift
    • SwiftUI

    Featured

    • Accessibility
    • App Intents
    • Apple Intelligence
    • Games
    • Machine Learning & AI
    • Security
    • Xcode Cloud
    • Community

    Explore Community

    • Overview
    • Meet with Apple events
    • Community-driven events
    • Developer Forums
    • Open Source

    Featured

    • WWDC
    • Swift Student Challenge
    • Developer Stories
    • App Store Awards
    • Apple Design Awards
    • Apple Developer Centers
    • Documentation

    Explore Documentation

    • Documentation Library
    • Technology Overviews
    • Sample Code
    • Human Interface Guidelines
    • Videos

    Release Notes

    • Featured Updates
    • iOS
    • iPadOS
    • macOS
    • watchOS
    • visionOS
    • tvOS
    • Xcode
    • Downloads

    Explore Downloads

    • All Downloads
    • Operating Systems
    • Applications
    • Design Resources

    Featured

    • Xcode
    • TestFlight
    • Fonts
    • SF Symbols
    • Icon Composer
    • Support

    Explore Support

    • Overview
    • Help Guides
    • Developer Forums
    • Feedback Assistant
    • Contact Us

    Featured

    • Account Help
    • App Review Guidelines
    • App Store Connect Help
    • Upcoming Requirements
    • Agreements and Guidelines
    • System Status
  • Quick Links

    • Events
    • News
    • Forums
    • Sample Code
    • Videos
 

Videos

Abrir menú Cerrar menú
  • Colecciones
  • Todos los videos
  • Información

Más videos

  • Información
  • Código
  • Break into the RealityKit debugger

    Meet the RealityKit debugger and discover how this new tool lets you inspect the entity hierarchy of spatial apps, debug rogue transformations, find missing entities, and detect which parts of your code are causing problems for your systems.

    Capítulos

    • 0:00 - Introduction
    • 0:23 - Agenda
    • 0:53 - Prepare for the Journey
    • 1:41 - Meet the RealityKit debugger
    • 2:40 - Transform the BOTanist
    • 4:02 - Traverse hierarchy issues
    • 7:20 - Address bad behaviors
    • 10:52 - Find what's missing
    • 18:44 - Embrace uniqueness
    • 22:34 - Wrap up

    Recursos

    • Forum: Developer Tools & Services
      • Video HD
      • Video SD

    Videos relacionados

    WWDC24

    • Discover RealityKit APIs for iOS, macOS, and visionOS
  • Buscar este video…
    • 2:45 - ClubView

      /*
      Abstract:
      The full club patch. SwiftUI view, state, extensions and helpers.
      */
      
      import SwiftUI
      import RealityKit
      import OSLog
      import BOTanistAssets
      import Combine
      import Charts
      
      struct ClubView: View {
          @State var state = ClubViewState()
          
          var body: some View {
              ZStack {
                  RealityView { content in
                      state.loadEnvironment()
                      
                      state.rootEntity.scale = SIMD3<Float>(repeating: 0.5)
                      
                      content.add(state.rootEntity)
                  } update: { updateContent in
                      if !state.doorSupervisor.doorsOpen {
                          state.transformIntoClub(content: updateContent)
                      }
                  }
              }
          }
      }
      
      @Observable
      @MainActor
      final public class ClubViewState: Sendable {
          let rootEntity = Entity()
          
          private var loadedEnvironmentRoot: Entity?
          private var robotRevolutionController: Entity?
          private var host: Entity?
          
          private(set) var doorSupervisor: DoorSupervisor {
              get {
                  rootEntity.components[DoorSupervisor.self]!
              } set {
                  rootEntity.components[DoorSupervisor.self] = newValue
              }
          }
          
          init() {
              RevolvingSystem.registerSystem()
              HoverSystem.registerSystem()
              TeleportationSystem.registerSystem()
              DanceMotivationSystem.registerSystem()
              
              rootEntity.name = "The B0T Club"
              rootEntity.components[DoorSupervisor.self] = DoorSupervisor(capacity: 9)
          }
          
          /// Load the existing garden assets
          func loadEnvironment() {
              guard loadedEnvironmentRoot == nil else {
                  return
              }
              
              if let environment = try? Entity.load(named: "scenes/volume", in: BOTanistAssetsBundle) {
                  environment.name = "Environment"
                  self.loadedEnvironmentRoot = environment
                  
                  rootEntity.addChild(environment)
              }
          }
          
          /// Renovate the loaded environment to build our club
          func transformIntoClub(content: RealityViewContent) {
              guard !doorSupervisor.doorsOpen else {
                  return
              }
              
              // Build a teleportation center and use it to spawn robots
              addTeleportationCenterToTheClub()
              
              // Haphazardly clean up the space by hiding anything un-club-like
              hideStuffInTheEnvironment()
              
              // Polish that floor and add some spin
              addRevolvingDanceFloorToTheClub()
              
              // Keep the robots moving in an orderly fashion
              addRobotRevolutionControllerToTheClub()
              
              // Install some attractors to entice robots to the dance floor
              addDanceFloorAttractors()
              
              // Set the mood
              addSpotlightsToTheClub()
              
              // Stock up on oil to keep the moves smooth
              addCounterToTheClub()
              
              // And add a huge Disco Ball, because...
              addDiscoBallToTheClub()
              
              // Let the party begin
              openDoors()
          }
          
          /// Construct a Teleportation Center and add it to the Club's root entity
          private func addTeleportationCenterToTheClub() {
              let teleportationCenter = Entity()
              teleportationCenter.name = "Teleportation Center"
              rootEntity.addChild(teleportationCenter)
              
              // Liven up the planters to look more like teleporters
              let positions: [SIMD3<Float>] = [[0.128, 0, 0.14], [-0.255, 0, 0.23], [0.05, 0, -0.17]]
              let colors: [(UIColor, UIColor)] = [(.green, .yellow), (.magenta, .purple), (.cyan, .blue)]
              for index in 0...2 {
                  if let teleporter = rejigPlanter(identifier: String(index + 1), position: positions[index], colors: colors[index]) {
                      teleportationCenter.addChild(teleporter)
                  }
              }
              
              // Create a Control Center and provide a closure to handle robot spawning
              let teleportationControlCenter = ControlCenterComponent(
                  initialValue: 10,
                  interval: 5,
                  rootEntity: rootEntity) { teleporter in
                      self.spawnRobot(from: teleporter)
                      self.countVisitor()
                      
                      // Have the host say hello
                      if let hostCharacter = self.host?.components[AutomatonControl.self]?.character {
                          hostCharacter.transitionToAndPlayAnimation(.idle)
                          hostCharacter.transitionToAndPlayAnimation(.wave)
                      }
              }
              
              // Assign the new control center component to the teleportation center entity
              teleportationCenter.components[ControlCenterComponent.self] = teleportationControlCenter
          }
          
          /// Transforms the visuals of the planters to look more teleporter-y
          private func rejigPlanter(identifier: String, position: SIMD3<Float>, colors: (UIColor, UIColor)) -> Entity? {
              if let rim = rootEntity.findEntity(named: "heroPlanter_rim_\(identifier)"),
                 let dirt = rootEntity.findEntity(named: "dirt_hero_\(identifier)"),
                 let rimModelComponent = rim.components[ModelComponent.self],
                 var dirtModelComponent = dirt.components[ModelComponent.self] {
                  // Apply the luminous material from the rims to the dirt (trust me it will look cool).
                  dirtModelComponent.materials = rimModelComponent.materials
                  dirt.components[OpacityComponent.self] = OpacityComponent(opacity: 0.7)
                  dirt.components[ModelComponent.self] = dirtModelComponent
              }
              
              // Make a teleporter container entity
              let teleporter = Entity()
              teleporter.name = "Teleporter-T\(identifier)"
              teleporter.position = position
              teleporter.components[TeleporterComponent.self] = TeleporterComponent()
              
              // Add a particle emitter
              let radius: Float = 0.035
              var particleEmitter = ParticleEmitterComponent.Presets.teleporter
              particleEmitter.emitterShapeSize = .init(repeating: radius)
              particleEmitter.mainEmitter.color = .constant(.random(a: colors.0, b: colors.1))
              
              let particleEntity = Entity()
              particleEntity.orientation = .init(angle: -.pi / 2, axis: [1, 0, 0])
              particleEntity.components[ParticleEmitterComponent.self] = particleEmitter
              particleEntity.name = "Photons"
              particleEntity.scale = .init(repeating: 1)
              teleporter.addChild(particleEntity)
              
      #if DEBUG
              // Add a debug marker in case we want to visually inspect this in the RealityKit Debugger
              teleporter.addDebugMarker(radius: radius, color: colors.0)
      #endif
              
              return teleporter
          }
          
          /// adds a random robot to the club root, positioned at the provided point
          private func spawnRobot(from spawnPoint: Entity) {
              guard let robotCharacter = randomRobot() else {
                  logger.error("Robot creation malfunction 🤖💥")
                  return
              }
              
              let guest = Entity()
              
              guest.addChild(robotCharacter.characterParent)
              guest.position = spawnPoint.position(relativeTo: rootEntity)
              guest.components[Newcomer.self] = Newcomer()
              guest.components[AutomatonControl.self] = AutomatonControl(character: robotCharacter)
              
              rootEntity.addChild(guest)
              
              // Play a little flashy burst on the particle emitter
              if let particles = spawnPoint.findEntity(named: "Photons") {
                  var component = particles.components[ParticleEmitterComponent.self]
                  component?.burst()
                  particles.components[ParticleEmitterComponent.self] = component
              }
          }
          
          /// misuses AppState as a robot factory - don't try this at home, or do, but don't ship it!
          private func randomRobot() -> RobotCharacter? {
              let robotMaker = AppState()
              
              // Use offsets from the loaded animation rig, with some random parts
              guard let skeleton = robotMaker.robotData.meshes[.body]?.findEntity(named: "rig_grp") as? ModelEntity else {
                  logger.error("Failed to find a robot animation rig... all dancing in cancelled ❌🕺")
                  return nil
              }
              
              robotMaker.randomizeSelectedRobot()
              
              guard let head = robotMaker.robotData.meshes[.head]?.clone(recursive: true),
                    let body = robotMaker.robotData.meshes[.body]?.clone(recursive: true),
                    let backpack = robotMaker.robotData.meshes[.backpack]?.clone(recursive: true) else {
                  fatalError()
              }
              
              let robotCharacter = RobotCharacter(
                  head: head,
                  body: body,
                  backpack: backpack,
                  appState: robotMaker,
                  headOffset: skeleton.pins["head"]?.position,
                  backpackOffset: skeleton.pins["backpack"]?.position
              )
              
              // Pick a random robot name from the sequence
              robotCharacter.characterParent.name = RobotNames.next
              
              // Remove the character controller and animation state, as we'll manually control these
              robotCharacter.characterParent.components[CharacterControllerComponent.self] = nil
              AnimationState.handlers.removeAll()
              
              // The robots are here to chill, so actually, let's put their backpacks in the cloakroom
              backpack.removeFromParent()
              
              // Say Hi
              robotCharacter.transitionToAndPlayAnimation(.wave)
              
              return robotCharacter
          }
          
          /// Update capacity when we have a visitor
          private func countVisitor() {
              var management = self.doorSupervisor
              management.visitorCount += 1
              self.doorSupervisor = management
          }
          
          /// Find and hide a bunch of stuff in the loaded environment
          private func hideStuffInTheEnvironment() {
              // We used the RealityKit Debugger to identify the names of things we want to hide in the club
              ["setDressing", "MovementBoundaries", "planter_side", "planter_Hero", "planter_Hero_1", "planter_Hero_2", "PlantLightGroup",
               "PlantLightGroup_1", "PlantLightGroup_2", "SidePlanterLights", "pipe_2", "pipe_3", "dirt_coffeeBerry_1", "dirt_coffeeBerry_2",
               "dirt_coffeeBerry_3", "dirt_side"].forEach { name in
                  if let entity = rootEntity.findEntity(named: name) {
                      entity.removeFromParent()
                  }
              }
          }
          
          /// Repurpose some existing bits in the environment to create a makeshift revolving dance floor - if it looks like dirt, that's because it is
          private func addRevolvingDanceFloorToTheClub() {
              guard let dirtFloor = loadedEnvironmentRoot?.findEntity(named: "dirt_end") else {
                  return
              }
              
              // Add a revolving container entity
              let revolvingDanceFloor = Entity()
              revolvingDanceFloor.name = "Revolving Dance Floor"
              revolvingDanceFloor.scale = [1, 1, 1]
              revolvingDanceFloor.position = [0, 0.181, 0]
              revolvingDanceFloor.components[RevolvingComponent.self] = RevolvingComponent(relativeTo: rootEntity)
              
              // Polish up the dirt floor
              let geometry = dirtFloor.clone(recursive: false)
              geometry.name = "Dirt Floor"
              geometry.transform = .identity
              geometry.position = [0, 0, 0]
              geometry.scale = dirtFloor.scale(relativeTo: rootEntity)
              
              let polish = geometry.clone(recursive: false)
              polish.name = "Polish Layer"
              polish.position = [0, 0.0004, 0]
              
              if var modelComponent = geometry.components[ModelComponent.self] {
                  var polishedFloorMaterial = PhysicallyBasedMaterial()
                  
                  polishedFloorMaterial.baseColor = .init(tint: .gray)
                  polishedFloorMaterial.roughness = .init(floatLiteral: 0.2)
                  polishedFloorMaterial.metallic = .init(floatLiteral: 0.8)
                  polishedFloorMaterial.blending = .transparent(opacity: .init(floatLiteral: 0.5))
                  polishedFloorMaterial.clearcoat = .init(floatLiteral: 0.4)
                  
                  modelComponent.materials = [polishedFloorMaterial]
                  
                  polish.components[ModelComponent.self] = modelComponent
              }
              
              // Add it to the revolving container
              revolvingDanceFloor.addChild(geometry)
              revolvingDanceFloor.addChild(polish)
              
              rootEntity.addChild(revolvingDanceFloor)
          }
          
          /// Creates a revolving container entity to keep robots moving in sync with the dance floor
          private func addRobotRevolutionControllerToTheClub() {
              let robotRevolutionController = Entity()
              robotRevolutionController.name = "Robot Revolution Controller"
              robotRevolutionController.components[RevolvingComponent.self] = RevolvingComponent(relativeTo: rootEntity)
              
              rootEntity.addChild(robotRevolutionController)
              
              self.robotRevolutionController = robotRevolutionController
          }
          
          /// Add invisible attractors to the dance floor to position and control robots
          private func addDanceFloorAttractors() {
              guard let robotRevolutionController else {
                  logger.error("The Robot Revolution Controller is missing 😱")
                  return
              }
              
              // Add a few dance spots on the outside of the club that we know don't obstruct the furniture
              let staticAttractors = Entity()
              staticAttractors.name = "Static Attractors"
              
              let placementRadius: Float = 0.25
              let outerRadius = placementRadius * 0.8
              addDanceFloorAttractor(to: staticAttractors, angle: Angle2D(degrees: 10), placementRadius: outerRadius, name: "Static-A1", variation: 0)
              addDanceFloorAttractor(to: staticAttractors, angle: Angle2D(degrees: 90), placementRadius: outerRadius, name: "Static-A2", variation: 0)
              addDanceFloorAttractor(to: staticAttractors, angle: Angle2D(degrees: 130), placementRadius: outerRadius, name: "Static-A3", variation: 0)
              addDanceFloorAttractor(to: staticAttractors, angle: Angle2D(degrees: 240), placementRadius: outerRadius, name: "Static-A4", variation: 0)
              addDanceFloorAttractor(to: staticAttractors, angle: Angle2D(degrees: 325), placementRadius: outerRadius, name: "Static-A5", variation: 0)
              
              rootEntity.addChild(staticAttractors)
              
              // The remaining center attractors are on the revolving dance floor and can be more randomly positioned
              let innerRingCapacity = doorSupervisor.capacity - 5
              
              let revolvingAttractors = Entity()
              revolvingAttractors.name = "Revolving Attractors"
              
              addDanceFloorAttractors(to: revolvingAttractors, count: innerRingCapacity, placementRadius: placementRadius * 0.3, namePrefix: "Revolving")
              
              robotRevolutionController.addChild(revolvingAttractors)
              
      #if DEBUG
              // Add some debug visualizations
              let debugRoot = Entity()
              debugRoot.name = "[Debug] Dance System"
              debugRoot.isEnabled = false
              debugRoot.components[DanceSystemDebugComponent.self] = DanceSystemDebugComponent()
              
              rootEntity.addChild(debugRoot)
              
              let allAttractors = Array(staticAttractors.children) + Array(revolvingAttractors.children)
              
              // Create a new visualization for each attractor
              allAttractors.forEach { attractor in
                  if let visualization = Entity.makeDebugMarker(height: 0.08, radius: 0.03, enabled: true) {
                      guard let attractorComponent = attractor.components[AttractorComponent.self] else {
                          return
                      }
                      
                      let debugComponent = AttractorDebugComponent(state: attractorComponent.state, attractor: attractor)
                      
                      visualization.position = [0, 0.04, 0]
                      visualization.components[AttractorDebugComponent.self] = debugComponent
                      debugRoot.addChild(visualization)
                  }
              }
      #endif
          }
          
          /// Add multiple dance floor attractors along the circumference of a circle with the specified placementRadius
          private func addDanceFloorAttractors(to danceFloor: Entity, count: Int, placementRadius: Float, namePrefix: String, variation: Float = 0.005) {
              let angleIncrements = 360 / count
              
              for offset in 0..<count {
                  let angle = Angle2D(degrees: Double(angleIncrements * offset))
                  let name = "\(namePrefix)-A\(offset + 1)"
                  addDanceFloorAttractor(to: danceFloor, angle: angle, placementRadius: placementRadius, name: name, variation: variation)
              }
          }
          
          /// Adds a single dance floor attractor at a point on the circumference of a circle with the specified placementRadius
          private func addDanceFloorAttractor(to danceFloor: Entity, angle: Angle2D, placementRadius: Float, name: String, variation: Float = 0.005) {
              let attractor = Entity()
              attractor.name = name
              attractor.components[AttractorComponent.self] = AttractorComponent(club: rootEntity)
              attractor.position = pointOnCircumference(angle: angle, radius: placementRadius, variation: variation)
              danceFloor.addChild(attractor)
          }
          
          /// Adds some revolving spot lights to the club
          private func addSpotlightsToTheClub() {
              let placementRadius: Float = 0.5
              let lightsWrapper = Entity()
              lightsWrapper.name = "Light Rig"
              
              let magentaLight = SpotLight()
              magentaLight.light.color = .magenta
              magentaLight.light.intensity = 500
              var lightPosition = pointOnCircumference(angle: Angle2D(degrees: 0), radius: placementRadius, y: 0.5)
              magentaLight.look(at: .zero, from: lightPosition, relativeTo: rootEntity)
              lightsWrapper.addChild(magentaLight)
              
              let greenLight = magentaLight.clone(recursive: true)
              greenLight.light.color = .green
              lightPosition = pointOnCircumference(angle: Angle2D(degrees: 120), radius: placementRadius, y: 0.5)
              greenLight.look(at: .zero, from: lightPosition, relativeTo: rootEntity)
              lightsWrapper.addChild(greenLight)
              
              let cyanLight = magentaLight.clone(recursive: true)
              cyanLight.light.color = .cyan
              lightPosition = pointOnCircumference(angle: Angle2D(degrees: 240), radius: placementRadius, y: 0.5)
              cyanLight.look(at: .zero, from: lightPosition, relativeTo: rootEntity)
              lightsWrapper.addChild(cyanLight)
              
              lightsWrapper.components[RevolvingComponent.self] = RevolvingComponent(speed: -0.2, relativeTo: rootEntity)
              
              rootEntity.addChild(lightsWrapper)
          }
          
          /// Repurpose some planters to make a counter and stocks with a premium aged oil, and a friendly host
          private func addCounterToTheClub() {
              guard let planter = rootEntity.findEntity(named: "planter_big"),
                    let dirt = rootEntity.findEntity(named: "dirt_big") else {
                  logger.error("Making the counter failed... too much dancing may now cause rust 🤖")
                  return
              }
              
              // Group into a container entity
              let counter = Entity()
              counter.name = "Counter"
              counter.position = [0.333, 0.05, -0.09]
              rootEntity.addChild(counter)
              
              // Repurpose existing assets
              let counterGeometry = Entity()
              counterGeometry.name = "Counter Geometry"
              counterGeometry.addChild(planter, preservingWorldTransform: true)
              counterGeometry.addChild(dirt, preservingWorldTransform: true)
              counterGeometry.scale = [2, 6, 2]
              counterGeometry.position = [-0.3335, -0.15, 0.09]
              counter.addChild(counterGeometry)
              
              var counterTopMaterial = PhysicallyBasedMaterial()
              counterTopMaterial.baseColor = .init(tint: .white)
              counterTopMaterial.roughness = .init(floatLiteral: 0)
              counterTopMaterial.metallic = .init(floatLiteral: 1)
              
              dirt.components[ModelComponent.self]?.materials = [counterTopMaterial]
              dirt.position += [0, 0.001, 0]
              
              // Add a fancy hover rail
              if let rim = rootEntity.findEntity(named: "bottom_rim_1") {
                  let hoverRailing = rim.clone(recursive: true)
                  hoverRailing.name = "Hover Railing"
                  hoverRailing.position = [0, 0.1, 0]
                  hoverRailing.scale = rim.scale(relativeTo: rootEntity) * 0.5
                  hoverRailing.components[HoverComponent.self] = HoverComponent(from: hoverRailing.position, to: hoverRailing.position + [0, -0.03, 0])
                  counter.addChild(hoverRailing)
              }
              
              // Add some bottles to the counter
              let bottles = stockBottles(placementRadius: 0.045)
              counter.addChild(bottles)
              
              // Hide any out of stock items
              for bottle in bottles.children {
                  bottle.isEnabled = bottle.components[OutOfStockComponent.self] == nil
              }
              
              // Add a friendly host
              addHostToTheCounter(counter)
          }
          
          /// Adds 9 green bottles of the finest aged oil to the counter (assuming we have them in stock)
          private func stockBottles(placementRadius: Float) -> Entity {
              let bottleRadius: Float = 0.003
              let bottleHeight: Float = 0.022
              let angleIncrement: Float = -12
              let outOfStockBrands: Set = [3]
              
              // Make a wrapper entity
              let bottleGroup = Entity()
              bottleGroup.name = "Bottle Group"
              bottleGroup.position = [0, 0.04, 0]
              bottleGroup.orientation = .init(angle: 180 * (.pi / 180), axis: [0, 1, 0])
              
              // Make a nice green material
              var bottleMaterial = PhysicallyBasedMaterial()
              bottleMaterial.baseColor = .init(tint: .green)
              bottleMaterial.blending = .transparent(opacity: .init(floatLiteral: 0.5))
              
              // A simple cylinder mesh
              let bottleMesh = MeshResource.generateCylinder(height: bottleHeight, radius: bottleRadius)
              
              // Error 1: Content occluded
              let bottle1 = Entity()
              bottle1.name = "BT1"
              bottle1.position = pointOnCircumference(angle: .zero, radius: placementRadius, y: -0.03)
              bottle1.components[ModelComponent.self] = ModelComponent(mesh: bottleMesh, materials: [bottleMaterial])
              bottleGroup.addChild(bottle1)
              
              // Error 2: Content clipped
              let bottle2 = Entity()
              bottle2.name = "BT2"
              bottle2.position = pointOnCircumference(angle: Angle2D(degrees: angleIncrement), radius: 1.6, y: bottleHeight / 2)
              bottle2.components[ModelComponent.self] = ModelComponent(mesh: bottleMesh, materials: [bottleMaterial])
              bottleGroup.addChild(bottle2)
              
              // Error 3: Content inside out
              let bottle3 = Entity()
              bottle3.name = "BT3"
              bottle3.position = pointOnCircumference(angle: Angle2D(degrees: 2 * angleIncrement), radius: placementRadius, y: bottleHeight / 2)
              bottle3.scale = .init(repeating: 650)
              bottle3.components[ModelComponent.self] = ModelComponent(mesh: bottleMesh, materials: [bottleMaterial])
              bottleGroup.addChild(bottle3)
              
              // Error 4: Content not enabled
              let bottle4 = Entity()
              bottle4.name = "BT4"
              bottle4.position = pointOnCircumference(angle: Angle2D(degrees: 3 * angleIncrement), radius: placementRadius, y: bottleHeight / 2)
              bottle4.components[ModelComponent.self] = ModelComponent(mesh: bottleMesh, materials: [bottleMaterial])
              bottle4.components[OutOfStockComponent.self] = OutOfStockComponent()
              bottleGroup.addChild(bottle4)
              
              // Error 5: Content not anchored
              let bottle5 = Entity()
              bottle5.name = "BT5"
              bottle5.position = pointOnCircumference(angle: Angle2D(degrees: 4 * angleIncrement), radius: placementRadius, y: bottleHeight / 2)
              bottle5.components[AnchoringComponent.self] = AnchoringComponent(.plane(.horizontal, classification: .table, minimumBounds: .zero))
              bottle5.components[ModelComponent.self] = ModelComponent(mesh: bottleMesh, materials: [bottleMaterial])
              bottleGroup.addChild(bottle5)
              
              // Error 6: Content missing a mesh
              let bottle6 = Entity()
              bottle6.name = "BT6"
              bottle6.position = pointOnCircumference(angle: Angle2D(degrees: 5 * angleIncrement), radius: placementRadius, y: bottleHeight / 2)
              bottle5.components[ModelComponent.self] = ModelComponent(mesh: bottleMesh, materials: [bottleMaterial])
              bottleGroup.addChild(bottle6)
              
              // Error 7: Content's material misconfigured
              let bottle7 = Entity()
              bottle7.name = "BT7"
              bottle7.position = pointOnCircumference(angle: Angle2D(degrees: 6 * angleIncrement), radius: placementRadius, y: bottleHeight / 2)
              
              var simplifiedBottleMaterial = UnlitMaterial(color: .green.withAlphaComponent(0.5))
              simplifiedBottleMaterial.opacityThreshold = 1
              
              bottle7.components[ModelComponent.self] = ModelComponent(mesh: bottleMesh, materials: [simplifiedBottleMaterial])
              bottleGroup.addChild(bottle7)
              
              // Error 8: Content has a broken mesh
              let alternativeMesh = MeshResource.generateAbnormalCylinder(height: bottleHeight, radius: bottleRadius)
              let bottle8 = Entity()
              bottle8.name = "BT8"
              bottle8.position = pointOnCircumference(angle: Angle2D(degrees: 7 * angleIncrement), radius: placementRadius, y: bottleHeight / 2)
              bottle8.scale = [bottle8.scale.x, bottle8.scale.y, -bottle8.scale.z]
              bottleMaterial.opacityThreshold = 0
              bottle8.components[ModelComponent.self] = ModelComponent(mesh: alternativeMesh, materials: [bottleMaterial])
              bottleGroup.addChild(bottle8)
              
              // Error 9: Content not added to the scene hierarchy
              let bottle9 = Entity()
              bottle9.name = "BT9"
              bottle9.position = pointOnCircumference(angle: Angle2D(degrees: 8 * angleIncrement), radius: placementRadius, y: bottleHeight / 2)
              bottle9.components[ModelComponent.self] = ModelComponent(mesh: bottleMesh, materials: [bottleMaterial])
              bottleGroup.addChild(bottle8)
              
              // FIXME: Bottles are missing from the counter
              
              return bottleGroup
          }
          
          /// Add a host robot to the counter
          private func addHostToTheCounter(_ counter: Entity) {
              // Make a clone of our hero BOTanist
              let robotMaker = AppState()
              
              guard let skeleton = robotMaker.robotData.meshes[.body]?.findEntity(named: "rig_grp") as? ModelEntity else {
                  fatalError()
              }
              
              // But use the hover body to best complement the counter
              robotMaker.setMesh(part: .body, name: "body3")
              
              guard let head = robotMaker.robotData.meshes[.head]?.clone(recursive: true),
                    let body = robotMaker.robotData.meshes[.body]?.clone(recursive: true),
                    let backpack = robotMaker.robotData.meshes[.backpack]?.clone(recursive: true) else {
                  fatalError()
              }
              
              let robotCharacter = RobotCharacter(
                  head: head,
                  body: body,
                  backpack: backpack,
                  appState: robotMaker,
                  headOffset: skeleton.pins["head"]?.position,
                  backpackOffset: skeleton.pins["backpack"]?.position
              )
              
              // Remove the character controller and animation state, as we'll manually control these
              AnimationState.handlers.removeAll()
              robotCharacter.characterParent.components[CharacterControllerComponent.self] = nil
              
              // Take off that heavy backpack
              backpack.removeFromParent()
              
              // Setup our host using the character and add it to the counter
              let host = Entity()
              host.name = "Host"
              host.orientation = .init(angle: 300 * (.pi / 180), axis: [0, 1, 0])
              host.position = [0, 0.005, 0]
              host.components[AutomatonControl.self] = AutomatonControl(character: robotCharacter)
              host.addChild(robotCharacter.characterParent)
              counter.addChild(host)
              
              // Have them say Hi
              robotCharacter.transitionToAndPlayAnimation(.wave)
              
              // Save a reference so they can wave later when other bots enter
              self.host = host
          }
          
          /// Generates a disco ball looking entity, makes it revolve and hover, and adds it to the club
          private func addDiscoBallToTheClub() {
              // Add the top level revolving, hovering disco ball entity
              let discoBall = Entity()
              discoBall.name = "Disco Ball"
              discoBall.position = [-0.305, 0.17, 0.02]
              discoBall.components[RevolvingComponent.self] = RevolvingComponent(speed: -0.02, relativeTo: rootEntity)
              discoBall.components[HoverComponent.self] = HoverComponent(from: discoBall.position, to: discoBall.position + [0, 0.02, 0])
              
              rootEntity.addChild(discoBall)
              
              // Add a support beam to hold the disco ball
              var supportMaterial = PhysicallyBasedMaterial()
              supportMaterial.baseColor = .init(tint: .lightGray)
              supportMaterial.roughness = .init(floatLiteral: 0.8)
              supportMaterial.metallic = .init(floatLiteral: 0.8)
              
              let support = ModelEntity(mesh: .generateCylinder(height: 0.01, radius: 0.01), materials: [supportMaterial])
              support.scale = [0.2, 1.8, 0.2]
              support.position = [0, 0.05, 0]
              support.name = "Support"
              
              discoBall.addChild(support)
              
              // Add the shiny ball that is the base of our disco ball
              var backgroundMaterial = PhysicallyBasedMaterial()
              backgroundMaterial.baseColor = .init(tint: .lightGray)
              backgroundMaterial.roughness = .init(floatLiteral: 0)
              backgroundMaterial.metallic = .init(floatLiteral: 1)
              
              let background = ModelEntity(mesh: .generateSphere(radius: 0.05), materials: [backgroundMaterial])
              background.name = "Background"
              
              // FIXME: Unintentionally inheriting an ancestor's transformation
              support.addChild(background)
              
              // Add some detailed lines on top of the background
              var lineMaterial = PhysicallyBasedMaterial()
              lineMaterial.baseColor = .init(tint: .lightGray)
              lineMaterial.sheen = .init(tint: .lightGray)
              lineMaterial.emissiveColor = .init(color: .lightGray)
              lineMaterial.emissiveIntensity = 1
              lineMaterial.triangleFillMode = .lines
              
              let ballOutline = ModelEntity(mesh: .generateSphere(radius: 0.0505), materials: [lineMaterial])
              ballOutline.name = "Outline"
              
              background.addChild(ballOutline)
          }
          
          /// Marks the club as ready
          private func openDoors() {
              var management = self.doorSupervisor
              management.doorsOpen = true
              self.doorSupervisor = management
          }
          
          /// finds a point along the edge of a circle on an XZ-plane, given a radius and y value. Optionally applies some variance.
          private func pointOnCircumference(angle: Angle2D, radius: Float, variation: Float = 0, y: Float = 0) -> SIMD3<Float> {
              .init(
                  x: (Float(cos(angle)) * radius) + .random(in: -variation...variation),
                  y: y,
                  z: (Float(sin(angle)) * radius) + .random(in: -variation...variation)
              )
          }
      }
      
      // MARK: Club Management
      
      /// Manages club capacity and ready state
      struct DoorSupervisor: Component {
          let capacity: Int
          var doorsOpen = false
          var visitorCount = 0
          
          var hasCapacity: Bool {
              visitorCount < capacity
          }
      }
      
      /// Tag to indicate if a retail item is in stock
      struct OutOfStockComponent: Component {}
      
      // MARK: Revolution Control
      
      /// Works with the RevolvingSystem to apply a continuous rotation to an entity
      struct RevolvingComponent: Component {
          var speed: Float
          var angle: Float
          var axis: SIMD3<Float>
          var relativeTo: Entity?
          
          init(speed: Float = 0.05, initialAngle: Float = 0, axis: SIMD3<Float> = [0, 1, 0], relativeTo: Entity? = nil) {
              self.speed = speed
              self.angle = initialAngle
              self.axis = axis
              self.relativeTo = relativeTo
          }
      }
      
      /// Works with the RevolvingComponent to apply a continuous rotation to an entity
      @MainActor
      class RevolvingSystem: System {
          private static let query = EntityQuery(where: .has(RevolvingComponent.self))
          
          required init(scene: RealityKit.Scene) {}
          
          func update(context: SceneUpdateContext) {
              for entity in context.entities(matching: Self.query, updatingSystemWhen: .rendering) {
                  if var revolvingComponent = entity.components[RevolvingComponent.self] {
                      let relativeTo = revolvingComponent.relativeTo
                      
                      revolvingComponent.angle += .pi * Float(context.deltaTime) * revolvingComponent.speed
                      entity.setOrientation(.init(angle: revolvingComponent.angle, axis: revolvingComponent.axis), relativeTo: relativeTo)
                      
                      entity.components[RevolvingComponent.self] = revolvingComponent
                  }
              }
          }
      }
      
      // MARK: Hover Control
      
      /// Works with the HoverSystem to apply a continuous levitation like bounce to an entity
      struct HoverComponent: Component {
          var speed: Float
          var angle: Float
          var from: SIMD3<Float>
          var to: SIMD3<Float>
          
          init(speed: Float = 0.06, angle: Float = 0, from: SIMD3<Float>, to: SIMD3<Float>) {
              self.speed = speed
              self.angle = angle
              self.from = from
              self.to = to
          }
      }
      
      /// Works with the HoverComponent to apply a continuous levitation like bounce to an entity
      @MainActor
      class HoverSystem: System {
          private static let query = EntityQuery(where: .has(HoverComponent.self))
          
          required init(scene: RealityKit.Scene) {}
          
          func update(context: SceneUpdateContext) {
              for entity in context.entities(matching: Self.query, updatingSystemWhen: .rendering) {
                  if var hoverComponent = entity.components[HoverComponent.self] {
                      
                      hoverComponent.angle += .pi * Float(context.deltaTime) * hoverComponent.speed
                      
                      let range = hoverComponent.to - hoverComponent.from
                      let proportion = (sin(hoverComponent.angle) + 1) / 2
                      
                      entity.position = hoverComponent.from + (proportion * range)
                      
                      entity.components[HoverComponent.self] = hoverComponent
                  }
              }
          }
      }
      
      // MARK: Robot Parts
      
      /// A wrapper around a Robot Character that is actually used as an Automaton
      struct AutomatonControl: Component {
          var character: RobotCharacter
      }
      
      extension RobotCharacter {
          /// manually control the animation transition of a single robot instance
          func transitionToAndPlayAnimation(_ animationState: AnimationState) {
              if self.animationState.transition(to: animationState) {
                  playAnimation(animationState)
              }
          }
      }
      
      /// A collection of shuffled robot names for our Automatons
      @MainActor
      enum RobotNames {
          static var count: Int = 0
          static var next: String {
              count += 1
              
              return "Robo-v\(count)"
          }
      }
      
      // MARK: Teleportation
      
      /// Works with the TeleportationSystem to control spawning across all teleporters
      struct ControlCenterComponent: Component {
          typealias SpawnHandler = (Entity) -> Void
          
          var initialValue: TimeInterval
          var interval: TimeInterval
          var countdown: TimeInterval
          var rootEntity: Entity
          var _spawnHandler: SpawnHandler
          
          init(initialValue: TimeInterval, interval: TimeInterval, rootEntity: Entity, spawnHandler: @escaping SpawnHandler) {
              self.initialValue = initialValue
              self.interval = interval
              self.countdown = initialValue
              self.rootEntity = rootEntity
              self._spawnHandler = spawnHandler
          }
      }
      
      /// Represents a single Teleporter in the TeleportationSystem
      struct TeleporterComponent: Component {}
      
      /// Works with the ControlCenterComponent to control spawning across all teleporters
      @MainActor
      class TeleportationSystem: System {
          private static let controlCenterQuery = EntityQuery(where: .has(ControlCenterComponent.self))
          private static let teleporterQuery = EntityQuery(where: .has(TeleporterComponent.self))
          private static let robotQuery = EntityQuery(where: .has(AutomatonControl.self))
          
          required init(scene: RealityKit.Scene) {}
          
          func update(context: SceneUpdateContext) {
              for entity in context.entities(matching: Self.controlCenterQuery, updatingSystemWhen: .rendering) {
                  update(controlCenter: entity, context: context)
              }
          }
          
          private func safeToUse(teleporter: Entity, context: SceneUpdateContext) -> Bool {
              let someBotIsStandingToClose = context.entities(matching: Self.robotQuery, updatingSystemWhen: .rendering)
                  .contains { entity in
                      distance(entity.position(relativeTo: nil), teleporter.position(relativeTo: nil)) < 0.02
                  }
              
              return  !someBotIsStandingToClose
          }
          
          private func update(controlCenter controlCenterEntity: Entity, context: SceneUpdateContext) {
              guard var controlCenter = controlCenterEntity.components[ControlCenterComponent.self],
                    let clubManager = controlCenter.rootEntity.components[DoorSupervisor.self],
                    clubManager.hasCapacity else {
                  return
              }
              
              // 1. Decrease countdown, and activate if it reaches zero
              controlCenter.countdown -= context.deltaTime
              if controlCenter.countdown <= 0 {
                  
                  // 2. Find all the active teleporters and pick a random one
                  if let teleporter = context.entities(matching: Self.teleporterQuery, updatingSystemWhen: .rendering).shuffled().first {
                      
                      // 3. If no other robots are in the way, pass it to the designated spawn method
                      if safeToUse(teleporter: teleporter, context: context) {
                          controlCenter._spawnHandler(teleporter)
                      }
                  }
                  
                  // 4. Set the delay till the next spawn event
                  controlCenter.countdown = controlCenter.interval
              }
              
              // FIXME: Control Center is not being updated
          }
      }
      
      extension ParticleEmitterComponent.Presets {
          /// Makes a particle emitter component that looks like a teleporter
          fileprivate static var teleporter: ParticleEmitterComponent {
              var particleEmitter = ParticleEmitterComponent.Presets.rain
              
              particleEmitter.birthLocation = .surface
              particleEmitter.emitterShape = .torus
              particleEmitter.particlesInheritTransform = false
              particleEmitter.fieldSimulationSpace = .global
              particleEmitter.speed = 0.07
              particleEmitter.speedVariation = 0.03
              particleEmitter.radialAmount = 360
              particleEmitter.torusInnerRadius = 0.001
              particleEmitter.emissionDirection = [0, 1, 0]
              particleEmitter.spawnedEmitter = nil
              particleEmitter.burstCount = 5000
              particleEmitter.mainEmitter.opacityCurve = .linearFadeOut
              particleEmitter.mainEmitter.birthRate = 50
              particleEmitter.mainEmitter.birthRateVariation = 10
              particleEmitter.mainEmitter.lifeSpan = 0.5
              particleEmitter.mainEmitter.lifeSpanVariation = 0.01
              particleEmitter.mainEmitter.size = 0.001
              particleEmitter.mainEmitter.sizeVariation = 0.0005
              particleEmitter.mainEmitter.sizeMultiplierAtEndOfLifespan = 0.01
              particleEmitter.mainEmitter.stretchFactor = 10
              particleEmitter.mainEmitter.noiseStrength = 0
              particleEmitter.mainEmitter.spreadingAngle = 0
              particleEmitter.mainEmitter.angle = 0
              
              particleEmitter.spawnedEmitter = nil
              
              return particleEmitter
          }
      }
      
      // MARK: Dancing
      
      /// Represents a single Attractor in the DanceMotivationSystem
      struct AttractorComponent: Component {
          enum State {
              case vacant
              case attracting
              case motivating
          }
          
          private(set) var state: State = .vacant
          
          var target: Entity?
          var walkSpeed: Float = 0.1
          var interval: TimeInterval = 5
          var countdown: TimeInterval = 5
          var club: Entity?
          
          var isVacant: Bool {
              if case .vacant = state {
                  return true
              }
              return false
          }
          
          mutating func setTarget(_ target: Entity) {
              self.target = target
              self.state = .attracting
          }
          
          mutating func targetReached() {
              self.state = .motivating
          }
      }
      
      /// Represents a single Robot in the DanceMotivationSystem
      struct Newcomer: Component {}
      
      /// Works with the DanceMotivationSystem to provide additional Debug information to the RealityKit Debugger
      struct DanceSystemDebugComponent: Component {
          var states: UIImage? = nil
          var vacant: Int = 0
          var attracting: Int = 0
          var motivating: Int = 0
      }
      
      /// Provides additional Debug information about a single Attractor in the DanceMotivationSystem to the RealityKit Debugger
      struct AttractorDebugComponent: Component {
          var state: AttractorComponent.State
          var attractor: Entity
          var robot: Entity?
      }
      
      /// Manages the states of dance floor attractors, the movement of robots and the relationships between them
      @MainActor
      class DanceMotivationSystem: System {
          private static let attractorQuery = EntityQuery(where: .has(AttractorComponent.self))
          private static let targetQuery = EntityQuery(where: .has(Newcomer.self))
          private static let clubbersQuery = EntityQuery(where: .has(AutomatonControl.self))
          private static let debugRootQuery = EntityQuery(where: .has(DanceSystemDebugComponent.self))
          private static let debugVisualizationsQuery = EntityQuery(where: .has(AttractorDebugComponent.self))
          
          required init(scene: RealityKit.Scene) {}
          
          func update(context: SceneUpdateContext) {
              
              // 1. Check for newcomers at the club who could be enticed to come and dance
              for visitor in context.entities(matching: Self.targetQuery, updatingSystemWhen: .rendering) {
                  
                  // 2. Randomly pick an attractor
                  guard let attractor = context.entities(matching: Self.attractorQuery, updatingSystemWhen: .rendering)
                      .filter({ $0.components[AttractorComponent.self]?.isVacant ?? false })
                      .randomElement() else {
                      return
                  }
                  
                  // 3. Start attracting the visitor
                  var attractorComponent = attractor.components[AttractorComponent.self]!
                  attractorComponent.setTarget(visitor)
                  attractor.components[AttractorComponent.self] = attractorComponent
                  
                  // FIXME: Stop attractors competing over the same bot
              }
              
              // Let the attractors do their thing and attract visitors to come and dance
              for attractor in context.entities(matching: Self.attractorQuery, updatingSystemWhen: .rendering) {
                  guard var attractorComponent = attractor.components[AttractorComponent.self] else {
                      continue
                  }
                  
                  switch attractorComponent.state {
                  case .attracting:
                      if let updatedAttractorComponent = attractRobot(attractor: attractor, deltaTime: Float(context.deltaTime)) {
                          attractorComponent = updatedAttractorComponent
                      }
                      
                  case .motivating:
                      if let updatedAttractorComponent = motivateRobot(attractor: attractor, context: context) {
                          attractorComponent = updatedAttractorComponent
                      }
                      
                  default:
                      break
                  }
                  
                  // save changes
                  attractor.components[AttractorComponent.self] = attractorComponent
              }
              
      #if DEBUG
              updateDebugInfo(context: context)
      #endif
          }
          
          private func attractRobot(attractor: Entity, deltaTime: Float) -> AttractorComponent? {
              guard var attractorComponent = attractor.components[AttractorComponent.self],
                    case .attracting = attractorComponent.state,
                    let target = attractorComponent.target,
                    let robotCharacter = target.components[AutomatonControl.self]?.character else {
                  return nil
              }
              
              // robots wave when they first arrive, make sure that is completed first before moving
              var transitionAnimationTo: AnimationState?
              switch robotCharacter.animationState {
              case .wave: transitionAnimationTo = .idle
              case .idle: transitionAnimationTo = .walkLoop
              case .walkLoop: transitionAnimationTo = nil
              default: return attractorComponent
              }
              
              if let transitionAnimationTo {
                  if robotCharacter.animationState.transition(to: transitionAnimationTo) {
                      robotCharacter.playAnimation(robotCharacter.animationState)
                  }
              }
              
              // Convert the robot and target positions into the same coordinate system
              let targetPosition = target.position(relativeTo: attractorComponent.club)
              var danceSpotPosition = attractor.position(relativeTo: attractorComponent.club)
              danceSpotPosition.y = targetPosition.y
              
              let movementVector = danceSpotPosition - targetPosition
              let normalizedMovement = movementVector / length(movementVector)
              let move = normalizedMovement * deltaTime * attractorComponent.walkSpeed
              
              target.setPosition(targetPosition + move, relativeTo: attractorComponent.club)
              
              robotCharacter.characterModel.look(at: robotCharacter.characterModel.position - normalizedMovement,
                                                 from: robotCharacter.characterModel.position, relativeTo: robotCharacter.characterParent)
              
              // If the target is more or less in position then attach to the dance spot and change state to motivating
              if distance(danceSpotPosition, target.position(relativeTo: attractorComponent.club)) < 0.005 {
                  attractor.addChild(target, preservingWorldTransform: true)
                  
                  // Start Dancing
                  robotCharacter.transitionToAndPlayAnimation(.celebrate)
                  
                  // Update attractor state
                  attractorComponent.targetReached()
              }
              
              return attractorComponent
          }
          
          private func motivateRobot(attractor: Entity, context: SceneUpdateContext) -> AttractorComponent? {
              guard var attractorComponent = attractor.components[AttractorComponent.self],
                    case .motivating = attractorComponent.state,
                    let target = attractorComponent.target,
                    let robotCharacter = target.components[AutomatonControl.self]?.character else {
                  return nil
              }
              
              attractorComponent.countdown -= context.deltaTime
              
              if attractorComponent.countdown <= 0 {
                  // Turn to face a random fellow clubber
                  if let friend = Array(context.entities(matching: Self.clubbersQuery, updatingSystemWhen: .rendering)).randomElement() {
                      let friendsPosition = friend.position(relativeTo: robotCharacter.characterParent)
                      
                      robotCharacter.characterModel.look(at: friendsPosition,
                                                         from: robotCharacter.characterModel.position, relativeTo: robotCharacter.characterParent)
                      
                      // TODO: remove me
                      print("🔥 friendsPosition \(friendsPosition) targetPosition \(robotCharacter.characterModel.position)")
                  }
                  
                  attractorComponent.countdown = attractorComponent.interval
              }
              
              return attractorComponent
          }
          
      #if DEBUG
          let vacantColor = UnlitMaterial.BaseColor(tint: .yellow.withAlphaComponent(0.5))
          let attractingColor = UnlitMaterial.BaseColor(tint: .orange.withAlphaComponent(0.5))
          let motivatingColor = UnlitMaterial.BaseColor(tint: .red.withAlphaComponent(0.5))
          
          private func updateDebugInfo(context: SceneUpdateContext) {
              var vacantCount: Int = 0
              var attractingCount: Int = 0
              var motivatingCount: Int = 0
              
              context.entities(matching: Self.debugVisualizationsQuery, updatingSystemWhen: .rendering).forEach { visualization in
                  guard let visualizationComponent = visualization.components[AttractorDebugComponent.self],
                        let attractorComponent = visualizationComponent.attractor.components[AttractorComponent.self] else {
                      return
                  }
                  
                  updateVisualizationEntity(visualization, relativeTo: attractorComponent.club)
                  
                  switch attractorComponent.state {
                  case .vacant: vacantCount += 1
                  case .attracting: attractingCount += 1
                  case .motivating: motivatingCount += 1
                  }
              }
              
              context.entities(matching: Self.debugRootQuery, updatingSystemWhen: .rendering).forEach { debugRoot in
                  if var debugComponent = debugRoot.components[DanceSystemDebugComponent.self] {
                      debugComponent.vacant = vacantCount
                      debugComponent.attracting = attractingCount
                      debugComponent.motivating = motivatingCount
                      debugComponent.states = makeChart(vacantCount: vacantCount, attractingCount: attractingCount, motivatingCount: motivatingCount)
                      debugRoot.components[DanceSystemDebugComponent.self] = debugComponent
                  }
              }
          }
          
          private func updateVisualizationEntity(_ visualization: Entity, relativeTo root: Entity?) {
              guard var visualizationComponent = visualization.components[AttractorDebugComponent.self],
                    let attractorComponent = visualizationComponent.attractor.components[AttractorComponent.self] else {
                  return
              }
              
              // Update the position
              var position = visualizationComponent.attractor.position(relativeTo: root)
              position.y = visualization.position.y
              visualization.setPosition(position, relativeTo: root)
              
              // Update the state
              visualizationComponent.state = attractorComponent.state
              visualization.name = "[Debug] \(visualizationComponent.attractor.name) (\(attractorComponent.state))"
              
              // Update the base material color to signify the attractor state
              if var modelComponent = visualization.components[ModelComponent.self],
                 var material = modelComponent.materials.first as? UnlitMaterial {
                  
                  switch attractorComponent.state {
                  case .vacant: material.color = vacantColor
                  case .attracting: material.color = attractingColor
                  case .motivating: material.color = motivatingColor
                  }
                  
                  modelComponent.materials = [material]
                  visualization.components[ModelComponent.self] = modelComponent
              }
              
              // Update the target
              visualizationComponent.robot = attractorComponent.target
              visualization.components[AttractorDebugComponent.self] = visualizationComponent
          }
          
          private func makeChart(vacantCount: Int, attractingCount: Int, motivatingCount: Int) -> UIImage? {
              ImageRenderer(content: chartView(vacantCount: vacantCount, attractingCount: attractingCount, motivatingCount: motivatingCount)).uiImage
          }
          
          private func chartView(vacantCount: Int, attractingCount: Int, motivatingCount: Int) -> some View {
              Chart(
                  [
                      (name: "Vacant", count: vacantCount),
                      (name: "Attracting", count: attractingCount),
                      (name: "Motivating", count: motivatingCount)
                  ], id: \.name) { name, count in
                      SectorMark(
                          angle: .value("Value", count),
                          angularInset: 1.5
                      )
                      .cornerRadius(5)
                      .foregroundStyle(by: .value("Name", name))
              }
              .chartLegend(.hidden)
              .chartForegroundStyleScale(["Vacant": .yellow, "Attracting": .orange, "Motivating": .red])
              .frame(width: 1024, height: 1024)
          }
          
      #endif
      }
      
      // MARK: Debug Helpers
      
      extension Entity {
          /// creates an semi-transparent entity that can be useful in debug invisible entities in the RealityKit Debugger
          static func makeDebugMarker(name: String? = nil, height: Float, radius: Float, color: UIColor = .white, enabled: Bool = false) -> Entity? {
      #if DEBUG
              var debugMaterial = UnlitMaterial()
              debugMaterial.color = .init(tint: color)
              debugMaterial.blending = .transparent(opacity: 0.7)
              
              let marker = ModelEntity(mesh: .generateCylinder(height: height, radius: radius), materials: [debugMaterial])
              if let name {
                  marker.name = name
              }
              marker.isEnabled = enabled
              
              return marker
      #else
              return nil
      #endif
          }
          
          /// adds an semi-transparent child entity that can be useful in debug invisible entities in the RealityKit Debugger
          @discardableResult
          func addDebugMarker(name: String? = nil, height: Float? = nil, radius: Float? = nil, color: UIColor = .white, enabled: Bool = false) -> Entity? {
      #if DEBUG
              var markerRadius: Float
              if radius != nil {
                  markerRadius = radius!
              } else {
                  // If no provided radius then calculate from the visual bounds
                  let extents = visualBounds(relativeTo: nil).extents
                  let boundingXZRadius = max(extents.x, extents.z) / 2
                  
                  if boundingXZRadius.isNormal {
                      markerRadius = boundingXZRadius
                  } else {
                      // If no visual bounds then use a default radius of 1cm
                      markerRadius = 0.01 * scale(relativeTo: nil).max()
                  }
              }
              
              // If no provided height then use a default value of 10cm
              let markerHeight = height ?? 0.1 * scale(relativeTo: nil).max()
              
              let name = name ?? "[Debug] \(self.name)"
              if let marker = Entity.makeDebugMarker(name: name, height: markerHeight, radius: markerRadius, color: color, enabled: enabled) {
                  marker.position = [0, markerHeight / 2, 0]
                  addChild(marker)
                  
                  return marker
              }
      #endif
              return nil
          }
      }
      
      // MARK: Demo Helpers
      
      extension MeshResource {
          /// Generates an cylinder with all the normals facing downwards. Probably has no uses other than demo'ing a broken mesh.
          static func generateAbnormalCylinder(height: Float, radius: Float) -> MeshResource {
              let meshResource = MeshResource.generateCylinder(height: height, radius: radius)
              var contents = meshResource.contents
              let models = contents.models.map { model in
                  var model = model
                  let parts = model.parts.map { part in
                      var part = part
                      part.normals = part.normals.map { normals in
                          let transformedNormals: [SIMD3<Float>] = normals.map { _ in
                              [0, -1, 0]
                          }
                          
                          return MeshBuffer(transformedNormals)
                      }
                      
                      return part
                  }
                  model.parts = MeshPartCollection(parts)
                  
                  return model
              }
              contents.models = MeshModelCollection(models)
              try? meshResource.replace(with: contents)
              
              return meshResource
          }
      }
    • 3:02 - Add a volumetric club scene

      WindowGroup(id: "RobotClub") {
          GeometryReader3D { geometry in
              ClubView()
                  .volumeBaseplateVisibility(.visible)
                  .environment(appState)
                  .scaleEffect(geometry.size.width / initialVolumeSize.width)
          }
          .onAppear {
              dismissWindow(id: "RobotCreation")
          }
      }
      .windowStyle(.volumetric)
      .defaultWorldScaling(.dynamic)
      .defaultSize(initialVolumeSize)
    • 3:09 - Add a button to open the club

      VStack {
          Button("🪩") {
              openWindow(id: "RobotClub")
          }
          .padding()
      
          Spacer()
      }
      .padding([.trailing, .top])
    • 6:50 - FIX: Unintentionally inheriting an ancestor's transformation

      discoBall.addChild(background)
    • 10:18 - FIX: Control Center is not being updated

      // 5. Save updated component back to the entity
      controlCenterEntity.components[ControlCenterComponent.self] = controlCenter
    • 18:15 - FIX: Stocking bottles

      private func stockBottles(placementRadius: Float) -> Entity {
          let bottleRadius: Float = 0.003
          let bottleHeight: Float = 0.022
          let angleIncrement: Float = -12
          let outOfStockBrands: Set = [3]
          
          // Make a wrapper entity
          let bottleGroup = Entity()
          bottleGroup.name = "Bottle Group"
          bottleGroup.position = [0, 0.04, 0]
          bottleGroup.orientation = .init(angle: 180 * (.pi / 180), axis: [0, 1, 0])
          
          // Make a nice green material
          var bottleMaterial = PhysicallyBasedMaterial()
          bottleMaterial.baseColor = .init(tint: .green)
          bottleMaterial.blending = .transparent(opacity: .init(floatLiteral: 0.5))
          
          for i in 0..<9 {
              let angle = Angle2D(degrees: angleIncrement * Float(i))
              let bottleMesh = MeshResource.generateCylinder(height: bottleHeight, radius: bottleRadius)
              let bottle = ModelEntity(mesh: bottleMesh, materials: [bottleMaterial])
              bottle.name = "BT\(i)"
              bottle.position = pointOnCircumference(angle: angle, radius: placementRadius, y: bottleHeight / 2)
              if outOfStockBrands.contains(i) {
                  bottle.components[OutOfStockComponent.self] = OutOfStockComponent()
              }
              
              bottleGroup.addChild(bottle)
          }
          
          return bottleGroup
      }
    • 22:48 - FIX: Attractors

      // 4. Untag them as a Newcomer
      visitor.components[Newcomer.self] = nil

Developer Footer

  • Videos
  • WWDC24
  • Break into the RealityKit debugger
  • Open Menu Close Menu
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    • App Store
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • Icon Composer
    • SF Symbols
    Open Menu Close Menu
    • Accessibility
    • Accessories
    • Apple Intelligence
    • Audio & Video
    • Augmented Reality
    • Business
    • Design
    • Distribution
    • Education
    • Games
    • Health & Fitness
    • In-App Purchase
    • Localization
    • Maps & Location
    • Machine Learning & AI
    • Security
    • Safari & Web
    Open Menu Close Menu
    • Documentation
    • Downloads
    • Sample Code
    • Videos
    Open Menu Close Menu
    • Help Guides & Articles
    • Contact Us
    • Forums
    • Feedback & Bug Reporting
    • System Status
    Open Menu Close Menu
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles
    • Feedback Assistant
    Open Menu Close Menu
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program
    • Mini Apps Partner Program
    • News Partner Program
    • Video Partner Program
    • Security Bounty Program
    • Security Research Device Program
    Open Menu Close Menu
    • Meet with Apple
    • Apple Developer Centers
    • App Store Awards
    • Apple Design Awards
    • Apple Developer Academies
    • WWDC
    Read the latest news.
    Get the Apple Developer app.
    Copyright © 2026 Apple Inc. All rights reserved.
    Terms of Use Privacy Policy Agreements and Guidelines