Lab 043 – Visualize the Entity Spawner

Visualize and test the spawn shapes.

Continuing the work from Labs 016 and 042, I set up a way to visualize the spawn shapes.

Video demo of several volume-shaped entity spawners.

Lab Code

This lab loads some resources from a Reality Composer Pro scene, then uses them to construct these examples.

struct Lab043: View {

    init() {
        EntitySpawnerComponent.registerComponent()
        EntitySpawnerSystem.registerSystem()
    }

    @State var collisionEvent: EventSubscription?

    var body: some View {
        RealityView { content in
            guard let scene = try? await Entity(named: "SpawnerLabResource", in: realityKitContentBundle)  else { return }
            content.add(scene)

            guard let baseTemplate = scene.findEntity(named: "Base"),
                  let shapeVisTemplate = scene.findEntity(named: "ShapeVis"),
                  let domeVis = scene.findEntity(named: "DomeVis"),
                  let subject = scene.findEntity(named: "Subject"),
                  let floor = scene.findEntity(named: "Floor")
            else { return }

            guard let baseMaterial = baseTemplate.components[ModelComponent.self]?.materials.first as? PhysicallyBasedMaterial,
                  let shapeVisMaterial = shapeVisTemplate.components[ModelComponent.self]?.materials.first as? PhysicallyBasedMaterial
            else { return }

            let boxSpawner = createSpawnerSetup(
                position: [-2, 1, 2],
                baseMaterial: baseMaterial,
                shapeVisMaterial: shapeVisMaterial,
                spawnShape: .box,
                visualizationMesh: .generateBox(width: 1.1, height: 1.1, depth: 1.1)
            )
            scene.addChild(boxSpawner)

            let planeSpawner = createSpawnerSetup(
                position: [0, 1, 2],
                baseMaterial: baseMaterial,
                shapeVisMaterial: shapeVisMaterial,
                spawnShape: .plane,
                visualizationMesh: .generatePlane(width: 1.1, depth: 1.1)
            )
            scene.addChild(planeSpawner)

            let circleSpawner = createSpawnerSetup(
                position: [2, 1, 2],
                baseMaterial: baseMaterial,
                shapeVisMaterial: shapeVisMaterial,
                spawnShape: .circle,
                visualizationMesh: .generateCylinder(height: 0.01, radius: 0.55)
            )
            scene.addChild(circleSpawner)

            let sphereSpawner = createSpawnerSetup(
                position: [-2, 1, -2],
                baseMaterial: baseMaterial,
                shapeVisMaterial: shapeVisMaterial,
                spawnShape: .sphere,
                visualizationMesh: .generateSphere(radius: 0.55)
            )
            scene.addChild(sphereSpawner)

            let upperDomeSpawner = createSpawnerSetup(
                position: [0, 1, -2],
                baseMaterial: baseMaterial,
                shapeVisMaterial: shapeVisMaterial,
                spawnShape: .domeUpper,
                visualizationEntity: domeVis.clone(recursive: true)
            )
            scene.addChild(upperDomeSpawner)

            let lowerDomeSpawner = createSpawnerSetup(
                position: [2, 1, -2],
                baseMaterial: baseMaterial,
                shapeVisMaterial: shapeVisMaterial,
                spawnShape: .domeLower,
                visualizationEntity: domeVis.clone(recursive: true),
                rotateVisualization: true
            )
            scene.addChild(lowerDomeSpawner)

            // Disable any spheres that reach the floor
            collisionEvent = content
                .subscribe(to: CollisionEvents.Began.self, on: floor)  { event in
                    event.entityB.isEnabled = false
                    event.entityB.components.set(PhysicsMotionComponent())
                }

            // Disable all template entities after using them
            baseTemplate.isEnabled = false
            shapeVisTemplate.isEnabled = false
            domeVis.isEnabled = false
            subject.isEnabled = false

        }
        .gesture(tap)
        .modifier(DragGestureImproved())
    }

    private func createSpawnerSetup(
        position: SIMD3<Float>,
        baseMaterial: PhysicallyBasedMaterial,
        shapeVisMaterial: PhysicallyBasedMaterial,
        spawnShape: EntitySpawnerComponent.SpawnShape,
        visualizationMesh: MeshResource? = nil,
        visualizationEntity: Entity? = nil,
        rotateVisualization: Bool = false
    ) -> Entity {

        // Create the base platform as a parent entity
        let base = ModelEntity(
            mesh: .generateBox(width: 1.1, height: 0.1, depth: 1.1),
            materials: [baseMaterial]
        )
        base.position = position
        base.collision = CollisionComponent(shapes: [.generateBox(width: 1.1, height: 0.1, depth: 1.1)])
        base.physicsBody = PhysicsBodyComponent(
            massProperties: .init(mass: 1.0),
            material: .default,
            mode: .static
        )
        base.components.set(InputTargetComponent())

        // Create walls for the base
        let wallHeight: Float = 0.1
        let wallThickness: Float = 0.05
        let wallWidth: Float = 1.1

        let wallTemplate = Entity()
        wallTemplate.components.set(ModelComponent(
            mesh: .generateBox(width: wallWidth, height: wallHeight, depth: wallThickness),
            materials: [baseMaterial]
        ))
        wallTemplate.components.set(CollisionComponent(
            shapes: [.generateBox(width: wallWidth, height: wallHeight, depth: wallThickness)]
        ))
        wallTemplate.components.set(PhysicsBodyComponent(
            massProperties: .init(mass: 1.0),
            material: .default,
            mode: .static
        ))

        let frontWall = wallTemplate.clone(recursive: true)
        frontWall.position = [0, wallHeight/2, 0.55 - wallThickness/2]
        base.addChild(frontWall)

        let backWall = wallTemplate.clone(recursive: true)
        backWall.position = [0, wallHeight/2, -0.55 + wallThickness/2]
        base.addChild(backWall)

        let leftWall = wallTemplate.clone(recursive: true)
        leftWall.orientation = simd_quatf(angle: .pi/2, axis: [0, 1, 0])
        leftWall.position = [-0.55 + wallThickness/2, wallHeight/2, 0]
        base.addChild(leftWall)

        let rightWall = wallTemplate.clone(recursive: true)
        rightWall.orientation = simd_quatf(angle: -.pi/2, axis: [0, 1, 0])
        rightWall.position = [0.55 - wallThickness/2, wallHeight/2, 0]
        base.addChild(rightWall)

        // Create spawn volume visualization
        let shapeVis: Entity
        if let visualizationEntity = visualizationEntity {
            shapeVis = visualizationEntity
            if rotateVisualization {
                // Rotate 180 degrees for lower dome
                shapeVis.orientation = simd_quatf(angle: .pi, axis: [1, 0, 0])
            }
        } else {
            shapeVis = ModelEntity(
                mesh: visualizationMesh!,
                materials: [shapeVisMaterial]
            )
        }
        shapeVis.position = [0, 1, 0]
        shapeVis.components.remove(InputTargetComponent.self)
        base.addChild(shapeVis)

        // Create and setup spawner
        let spawner = Entity()
        spawner.position = [0, 1, 0]
        var spawnerComponent = EntitySpawnerComponent()
        spawnerComponent.SpawnShape = spawnShape

        // Set appropriate dimensions based on spawn shape
        switch spawnShape {
        case .plane:
            spawnerComponent.PlaneDimensions = [1, 1]
        case .circle:
            spawnerComponent.Radius = 0.5
        case .box:
            spawnerComponent.BoxDimensions = [1, 1, 1]
        case .sphere, .domeUpper, .domeLower:
            spawnerComponent.Radius = 0.5
        }

        spawnerComponent.Copies = 10
        spawnerComponent.TargetEntityName = "Subject"
        spawner.components[EntitySpawnerComponent.self] = spawnerComponent
        base.addChild(spawner)

        return base
    }

    var tap: some Gesture {
        TapGesture()
            .targetedToEntity(where: .has(PhysicsMotionComponent.self))
            .onEnded { value in
                value.entity.isEnabled = false
            }
    }

}

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.

Questions or feedback?