Lab 016 – Entity Spawning and Pooling System

I’ve been tinkering on a custom component and system to use the in labs and I’ve coded myself into a corner.

The idea was to create a component that I could drop on an entity, then define a few parameters such as spawn shape and size, number of clones, etc. I created a system to go with this and it works well. The issue is that I’m putting this component on the entity that needs to be cloned while also using that entity as a container for the clones.

I’m going to need to rebuild this with one component for the system and one for another for the clones. For now I’m considering this lab a failure, but I still thought it was interesting enough to share. How would you go about refactoring this?

Video Demo

The Lab Code

struct Lab016: View {

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

    var body: some View {
        RealityView { content, attachments in

            if let scene = try? await Entity(named: "Lab016Scene", in: realityKitContentBundle) {
                content.add(scene)
            }

        } update: { content, attachments in

        } attachments: {
            Attachment(id: "AttachmentContent") {
                Text("wow")
            }
        }
        .gesture(tap)
        .modifier(DragGestureImproved())
        .modifier(MagnifyGestureImproved())
        .modifier(RotateGesture3DImproved())
    }

    var tap: some Gesture {
        TapGesture()
            .targetedToAnyEntity()
            .onEnded { value in
                // Skip if this is the original (spawner) entity
                if value.entity.components[EntitySpawnerComponent.self] != nil {
                    return
                }
                value.entity.isEnabled = false
            }
    }
}

Not much to see here since most of the work has been in the system.

The Component and System

public struct EntitySpawnerComponent: Component, Codable {
    public enum SpawnShape: String, Codable {
        case domeUpper
        case domeLower
        case sphere
        case box
        case plane
        case circle
    }
    
   
    /// The number of entities to manage in the pool
    public var Copies: Int = 12
    /// The shape to spawn entities in
    public var SpawnShape: SpawnShape = .domeUpper
    /// Radius for spherical shapes (dome, sphere, circle)
    public var Radius: Float = 5.0
   
    /// Dimensions for box spawning (width, height, depth)
    public var BoxDimensions: SIMD3<Float> = [2.0, 2.0, 2.0]

    /// Dimensions for plane spawning (width, depth)
    public var PlaneDimensions: SIMD2<Float> = [2.0, 2.0]
   
    /// Track if we've already spawned copies
    public var HasSpawned: Bool = false
    
    /// Track active entities for pool management
    public var ActiveEntities: Int = 0
    
    /// Whether to continuously check for disabled entities to respawn
    public var EnableRespawning: Bool = true
    
    public init() {

    }
}

public class EntitySpawnerSystem: System {
    // Define a query to return all entities with a EntitySpawnerComponent.
    private static let query = EntityQuery(where: .has(EntitySpawnerComponent.self))

    // init is required even when not used
    required public init(scene: Scene) {
        // Perform required initialization or setup.
    }

    private func positionForShape(_ shape: EntitySpawnerComponent.SpawnShape, 
                                component: EntitySpawnerComponent) -> SIMD3<Float> {
    switch shape {
  case .domeUpper:
        let distance = Float.random(in: 1...component.Radius)
        let theta = Float.random(in: 0...(2 * .pi))
        let phi = Float.random(in: 0...(Float.pi / 2))
        return SIMD3(
            distance * sin(phi) * cos(theta),
            distance * cos(phi),
            distance * sin(phi) * sin(theta)
        )
        
    case .domeLower:
        let distance = Float.random(in: 1...component.Radius)
        let theta = Float.random(in: 0...(2 * .pi))
        let phi = Float.random(in: (Float.pi / 2)...Float.pi)
        return SIMD3(
            distance * sin(phi) * cos(theta),
            distance * cos(phi),
            distance * sin(phi) * sin(theta)
        )
            
        case .sphere:
            let distance = Float.random(in: 1...component.Radius)
            let theta = Float.random(in: 0...(2 * .pi))
            let phi = Float.random(in: 0...Float.pi)
            return SIMD3(
                distance * sin(phi) * cos(theta),
                distance * cos(phi),
                distance * sin(phi) * sin(theta)
            )
            
        case .box:
            let dims = component.BoxDimensions * 0.5 // Convert to +/- dimensions
            return SIMD3(
                Float.random(in: -dims.x...dims.x),
                Float.random(in: -dims.y...dims.y),
                Float.random(in: -dims.z...dims.z)
            )
            
        case .plane:
            let dims = component.PlaneDimensions * 0.5 // Convert to +/- dimensions
            return SIMD3(
                Float.random(in: -dims.x...dims.x),
                0,
                Float.random(in: -dims.y...dims.y)
            )
            
        case .circle:
            let angle = Float.random(in: 0...(2 * .pi))
            let randomRadius = Float.random(in: 0...component.Radius)
            return SIMD3(
                randomRadius * cos(angle),
                0,
                randomRadius * sin(angle)
            )
        }
    }

    public func update(context: SceneUpdateContext) {
        for entity in context.entities(
            matching: Self.query,
            updatingSystemWhen: .rendering
        ) {
            guard var spawnerComponent = entity.components[EntitySpawnerComponent.self] else { continue }
            
            if !spawnerComponent.HasSpawned {
                // Initial spawn
                spawnInitialEntities(entity: entity, component: &spawnerComponent)
            } else if spawnerComponent.EnableRespawning {
                // Check for disabled entities to respawn
                respawnDisabledEntities(entity: entity, component: &spawnerComponent)
            }
            
            entity.components[EntitySpawnerComponent.self] = spawnerComponent
        }
    }
    
    @MainActor private func spawnInitialEntities(
        entity: Entity,
        component: inout EntitySpawnerComponent
    ) {
        for _ in 1...component.Copies {
            spawnEntity(entity: entity, component: component)
        }
        component.HasSpawned = true
        component.ActiveEntities = component.Copies
    }
    
    @MainActor private func respawnDisabledEntities(
        entity: Entity,
        component: inout EntitySpawnerComponent
    ) {
        for child in entity.children {
            if !child.isEnabled {
                child.position = positionForShape(component.SpawnShape, component: component)
                child.isEnabled = true
            }
        }
    }
    
    @MainActor private func spawnEntity(
        entity: Entity,
        component: EntitySpawnerComponent
    ) {
        let clone = entity.clone(recursive: false)
        clone.components.remove(EntitySpawnerComponent.self)
        
        let localOffset = positionForShape(component.SpawnShape, component: component)
        clone.position = localOffset 
        clone.orientation = entity.orientation
        
        entity.addChild(clone)
    }
}

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?