Lab 010 – Learning the basics of Systems

Creating a custom component and system that will add a breathing effect to entities based on the duration entered in the component.

Updated on November 4, 2024 – Register the component and system in the lab instead of the app file. Save accumulated time directly on the component.

I’ve made it over a year in visionOS development without needing to create my own components or systems. RealityKit does a pretty good job of covering most of my use cases. Still, I wanted to learn how to create components and systems in case the need arises.

Lab 010 itself doesn’t contain much code. It just loads a scene from Reality Composer Pro.

struct Lab010: View {

    init() {
        BreathComponent.registerComponent()
        BreathSystem.registerSystem()
    }
    
    var body: some View {
        RealityView { content in
            // Load the scene from the Reality Kit bindle
            if let scene = try? await Entity(named: "Lab010Scene", in: realityKitContentBundle) {
                content.add(scene)

                // The scene contains three entities with the new component, all with different duration values.
            }
        }
    }
}

Creating the Component

Inside the RealityKitContent bundle I created a new file called BreathComponent. I added a duration value just to have something to play with in the system and in the inspector in Reality Composer Pro.

import Foundation
import RealityKit

public struct BreathComponent: Component, Codable {

    /// The time it will take for a full cycle of the breath animation
    public var duration: Float = 4.0

    /// Store accumation time used for the breath animation
    public var accumulatedTime: Float = 0

    public init() {

    }
}

With this simple code I could add the new components to entities. I set a different duration each one.

A screenshot of Reality Composer Pro showing the custom component attached to an entity.

Creating the System

I created a new BreathSystem file in the main project. This system will get all entities with a BreathComponent and will animate their scale between 1x and 2x over the specified duration. I’ll admit, this is a bit contrived, but I needed do to something in this system just to learn the basics. There are better ways to do animations like this in SwiftUI and RealityKit.

import Foundation
import RealityKit
import RealityKitContent

// This was used by Lab 010 to learn my way around components and systems
public class BreathSystem: System {

    // Define a query to return all entities with a BreathComponent.
    private static let query = EntityQuery(where: .has(BreathComponent.self))

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

    public func update(context: SceneUpdateContext) {
        for entity in context.entities(
            matching: Self.query,
            updatingSystemWhen: .rendering
        ) {

            // Get the component
            guard var breath = entity.components[BreathComponent.self] else { continue }

            let duration = breath.duration

            // Accumulate time for this entity and set the new value on the component
            breath.accumulatedTime += Float(context.deltaTime)

            // Calculate the phase of the sine wave (0 to 2Ï€), wrapping by duration
            let phase = (breath.accumulatedTime / duration) * 2.0 * .pi

            // Compute the scale to smoothly oscillate between 1.0 and 2.0
            let scale = 1.5 + 0.5 * sin(phase)

            // Apply the scale to the entity
            entity.transform.scale = .init(repeating: scale)

            // Reset accumulated time if a full cycle has passed
            if breath.accumulatedTime >= duration {
                breath.accumulatedTime = 0.0
            }

            entity.components[BreathComponent.self] = breath

        }
    }

}

Finally, I registered the component and the system in the initializer for the lab. If you want to use these many times in your project, it may be best to register them at the app level instead of in a view.

struct Lab010: View {

    init() {
        BreathComponent.registerComponent()
        BreathSystem.registerSystem()
    }
    
    var body: some View {
        RealityView { content in ...}
    }
}

Video Demo

A video showing three spheres playing the breathing animation at different speeds.

Helpful docs

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?