Lab 042 – Second pass at an Entity Spawner
Using Components and Systems to create an entity spawner system.
This lab builds on the work from Lab 016. I decoupled the logic a bit. In the first version, I made the mistake of attaching the component to the entity that I wanted to clone. In this revised version, I attach the component to a transform entity, then pass in a the name of the entity we want to clone and spawn. I still have a lot of clean up to do. I also want to add some position/formation logic instead of always using random positions.
Video demo of entities spawning at pseudo-random positions within a fixed volume.
Lab Code
This lab loads a scene from Reality Composer Pro. That scene has the entity with the component and the target to clone. We can tap an entity to disable it, causing it to go back into the pool for respawn.
struct Lab042: View {
init() {
EntitySpawnerComponent.registerComponent()
EntitySpawnerSystem.registerSystem()
}
var body: some View {
RealityView { content, attachments in
if let scene = try? await Entity(named: "SpawnerLab", in: realityKitContentBundle) {
content.add(scene)
print("Scene added")
}
} 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[EntitySpawnerComponentLab016.self] != nil {
return
}
value.entity.isEnabled = false
}
}
}Here is the current version of the component and system. I have a lot of work to do on this, but it’s a start. I’ll be using systems like this in Dark Spaces.
import Foundation
@preconcurrency import RealityKit
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
/// The name of the entity to clone
public var TargetEntityName: String = ""
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)
)
}
}
@MainActor private func findTargetEntity(from entity: Entity, name: String) -> Entity? {
// First find the root entity by traversing up
var root = entity
while let parent = root.parent {
root = parent
}
return root.findEntity(named: name)
}
public func update(context: SceneUpdateContext) {
for entity in context.entities(
matching: Self.query,
updatingSystemWhen: .rendering
) {
guard var spawnerComponent = entity.components[EntitySpawnerComponent.self] else { continue }
// Skip if we don't have a target name
guard !spawnerComponent.TargetEntityName.isEmpty else { continue }
// Find target entity using our spawner entity as the starting point
guard let targetEntity = findTargetEntity(from: entity, name: spawnerComponent.TargetEntityName) else { continue }
if !spawnerComponent.HasSpawned {
// Initial spawn
spawnInitialEntities(spawner: entity, target: targetEntity, component: &spawnerComponent)
} else if spawnerComponent.EnableRespawning {
// Check for disabled entities to respawn
respawnDisabledEntities(spawner: entity, component: &spawnerComponent)
}
entity.components[EntitySpawnerComponent.self] = spawnerComponent
}
}
@MainActor private func spawnInitialEntities(
spawner: Entity,
target: Entity,
component: inout EntitySpawnerComponent
) {
for _ in 1...component.Copies {
spawnEntity(spawner: spawner, target: target, component: component)
}
component.HasSpawned = true
component.ActiveEntities = component.Copies
}
@MainActor private func respawnDisabledEntities(
spawner: Entity,
component: inout EntitySpawnerComponent
) {
for child in spawner.children {
if !child.isEnabled {
// Transform the local position to spawner's space
let localOffset = positionForShape(component.SpawnShape, component: component)
child.position = localOffset
child.isEnabled = true
}
}
}
@MainActor private func spawnEntity(
spawner: Entity,
target: Entity,
component: EntitySpawnerComponent
) {
let clone = target.clone(recursive: true)
// Transform the local position to spawner's space
let localOffset = positionForShape(component.SpawnShape, component: component)
clone.position = localOffset
// Use spawner's orientation
clone.orientation = .init() // Reset to identity since we're in spawner's space
spawner.addChild(clone)
}
}Support our work so we can continue to bring you new examples and articles.

Follow Step Into Vision