Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Give agents more agency

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    domocarroll

    realitykit-ar-companion

    domocarroll/realitykit-ar-companion
    Design

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Comprehensive RealityKit skill optimized for building AR companion experiences on iOS and visionOS, with character animation, body/hand tracking, AI integration patterns, spatial audio, and entity...

    SKILL.md

    RealityKit AR Companion Development Skill

    A production-ready skill for developing AR companion applications using Apple's RealityKit framework, specifically tailored for creating interactive, persistent virtual characters.

    Quick Start for AR Companion MVP

    Minimum Viable Setup

    import SwiftUI
    import RealityKit
    import ARKit
    
    struct ARCompanionView: View {
        var body: some View {
            ARViewContainer()
                .edgesIgnoringSafeArea(.all)
        }
    }
    
    struct ARViewContainer: UIViewRepresentable {
        func makeUIView(context: Context) -> ARView {
            let arView = ARView(frame: .zero)
    
            // Configure AR session for companion placement
            let config = ARWorldTrackingConfiguration()
            config.planeDetection = [.horizontal]
            config.environmentTexturing = .automatic
            arView.session.run(config)
    
            // Create companion anchor on horizontal surface
            let anchor = AnchorEntity(plane: .horizontal)
            arView.scene.addAnchor(anchor)
    
            // Load companion entity asynchronously
            Task {
                do {
                    let companion = try await Entity.loadAsync(named: "Companion")
                    anchor.addChild(companion)
                } catch {
                    print("Failed to load companion: \(error)")
                }
            }
    
            return arView
        }
    
        func updateUIView(_ uiView: ARView, context: Context) {}
    }
    

    Core Concepts for Companion Development

    Entity-Component-System (ECS) Architecture

    RealityKit uses an ECS architecture ideal for companion characters:

    • Entity: The base container for your companion (position, hierarchy)
    • Component: Modular behaviors attached to entities (animation, physics, audio)
    • System: Logic that processes entities with specific components each frame
    // Custom companion component
    struct CompanionComponent: Component {
        var mood: CompanionMood = .neutral
        var energy: Float = 1.0
        var isInteracting: Bool = false
    }
    
    enum CompanionMood {
        case happy, neutral, curious, tired
    }
    
    // Register component before use
    CompanionComponent.registerComponent()
    

    Custom System for Companion Behavior

    class CompanionBehaviorSystem: System {
        static let query = EntityQuery(where: .has(CompanionComponent.self))
    
        required init(scene: Scene) {}
    
        func update(context: SceneUpdateContext) {
            for entity in context.entities(matching: Self.query, updatingSystemWhen: .rendering) {
                guard var companion = entity.components[CompanionComponent.self] else { continue }
    
                // Update companion state based on time/interaction
                companion.energy -= 0.0001 * Float(context.deltaTime)
    
                if companion.energy < 0.3 {
                    companion.mood = .tired
                }
    
                entity.components[CompanionComponent.self] = companion
            }
        }
    }
    
    // Register system at app launch
    CompanionBehaviorSystem.registerSystem()
    

    Character Animation and Rigging

    Loading Animated Characters

    // Async loading of USDZ character with animations
    func loadCompanionCharacter() async throws -> Entity {
        let character = try await Entity.load(named: "CompanionCharacter")
    
        // Access available animations
        if let animationResource = character.availableAnimations.first {
            character.playAnimation(animationResource.repeat())
        }
    
        return character
    }
    

    Animation Playback Control

    class CompanionAnimator {
        var companion: Entity
        var currentController: AnimationPlaybackController?
    
        init(companion: Entity) {
            self.companion = companion
        }
    
        func playAnimation(named name: String, loop: Bool = false) {
            // Stop current animation
            currentController?.stop()
    
            // Find and play new animation
            if let animation = companion.availableAnimations.first(where: {
                $0.name?.contains(name) == true
            }) {
                let resource = loop ? animation.repeat() : animation
                currentController = companion.playAnimation(resource)
            }
        }
    
        func transitionToIdle() {
            playAnimation(named: "idle", loop: true)
        }
    
        func playReaction(type: ReactionType) {
            switch type {
            case .wave:
                playAnimation(named: "wave")
            case .jump:
                playAnimation(named: "jump")
            case .nod:
                playAnimation(named: "nod")
            }
        }
    }
    
    enum ReactionType {
        case wave, jump, nod
    }
    

    Using RealityActions for Procedural Animation

    import RealityActions
    
    // Register at app startup
    ActionManagerSystem.registerSystem()
    
    // Procedural bounce animation
    func makeCompanionBounce(_ entity: Entity) {
        entity.start(RepeatForever(
            Sequence(
                MoveBy(duration: 0.3, delta: SIMD3(0, 0.1, 0)),
                MoveBy(duration: 0.3, delta: SIMD3(0, -0.1, 0))
            )
        ))
    }
    
    // Attention-grabbing wiggle
    func wiggleForAttention(_ entity: Entity) {
        entity.start(Sequence(
            RotateBy(duration: 0.1, deltaAngles: SIMD3(0, 0.1, 0)),
            RotateBy(duration: 0.1, deltaAngles: SIMD3(0, -0.2, 0)),
            RotateBy(duration: 0.1, deltaAngles: SIMD3(0, 0.1, 0))
        ))
    }
    
    // Look at user smoothly
    func lookAtUser(_ entity: Entity, userPosition: SIMD3<Float>) async {
        let direction = normalize(userPosition - entity.position)
        let targetRotation = simd_quatf(from: SIMD3(0, 0, 1), to: direction)
    
        await entity.run(RotateTo(duration: 0.5, orientation: targetRotation))
    }
    

    Morph Target Animations (Facial Expressions)

    Using RealityMorpher for blend shape animations (iOS/macOS only, not visionOS):

    import RealityMorpher
    
    // Apply facial expression blend shapes
    func setCompanionExpression(_ entity: Entity, expression: FacialExpression) {
        // Requires model with blend shapes exported from 3D software
        // Set morph target weights for expressions
        switch expression {
        case .happy:
            // Set smile blend shape weight
            entity.setMorphWeight(named: "smile", weight: 1.0)
            entity.setMorphWeight(named: "eyeWide", weight: 0.3)
        case .sad:
            entity.setMorphWeight(named: "frown", weight: 0.8)
            entity.setMorphWeight(named: "eyeNarrow", weight: 0.4)
        case .surprised:
            entity.setMorphWeight(named: "mouthOpen", weight: 0.7)
            entity.setMorphWeight(named: "eyeWide", weight: 1.0)
        }
    }
    
    enum FacialExpression {
        case happy, sad, surprised, neutral
    }
    

    Body and Hand Tracking for Interaction

    Body Tracking Setup

    import RKLoader
    
    class BodyTrackingManager {
        var arView: ARView
        var trackedCharacter: BodyTrackedEntity?
    
        init(arView: ARView) {
            self.arView = arView
        }
    
        func startBodyTracking() async throws {
            // Load body-tracked character
            let character = try await RKLoader.loadBodyTrackedEntityAsync(named: "TrackedCompanion")
            trackedCharacter = character
    
            // Create body anchor
            let bodyAnchor = AnchorEntity(.body)
            arView.scene.addAnchor(bodyAnchor)
            bodyAnchor.addChild(character)
        }
    
        func stopBodyTracking() {
            trackedCharacter?.removeFromParent()
            trackedCharacter = nil
        }
    }
    

    Hand Tracking for visionOS

    import HandVector
    
    class HandInteractionManager: ObservableObject {
        @Published var detectedGesture: HandGesture = .none
    
        private let handTracking = HandTracking()
    
        func processHandUpdate(anchor: HandAnchor) {
            let handInfo = handTracking.generateHandInfo(from: anchor)
    
            // Check against built-in gestures
            if let similarity = handInfo?.similarity(of: .fiveFingers, to: .thumbsUp),
               similarity > 0.8 {
                detectedGesture = .thumbsUp
                return
            }
    
            if let similarity = handInfo?.similarity(of: .fiveFingers, to: .wave),
               similarity > 0.8 {
                detectedGesture = .wave
                return
            }
    
            // Check pinch gesture using FingerShape
            if let fingerShape = handInfo?.fingerShape,
               fingerShape.pinchDistance < 0.02 {
                detectedGesture = .pinch
                return
            }
    
            detectedGesture = .none
        }
    }
    
    enum HandGesture {
        case none, thumbsUp, wave, pinch, pointAt
    }
    

    Companion Response to Hand Gestures

    class GestureResponsiveCompanion {
        var companion: Entity
        var animator: CompanionAnimator
    
        func respondToGesture(_ gesture: HandGesture) {
            switch gesture {
            case .thumbsUp:
                animator.playReaction(type: .jump)
                playHappySound()
            case .wave:
                animator.playReaction(type: .wave)
            case .pinch:
                // Start interaction mode
                startInteractionMode()
            case .pointAt:
                // Look where user is pointing
                followPointDirection()
            case .none:
                break
            }
        }
    }
    

    AI Integration Patterns for Companion Behavior

    State Machine for Companion AI

    class CompanionAI {
        enum State {
            case idle
            case following
            case playing
            case resting
            case responding
        }
    
        var currentState: State = .idle
        var companion: Entity
        var animator: CompanionAnimator
    
        private var stateTimer: Double = 0
        private var attentionTarget: SIMD3<Float>?
    
        func update(deltaTime: Double, userPosition: SIMD3<Float>) {
            stateTimer += deltaTime
    
            switch currentState {
            case .idle:
                handleIdleState(userPosition: userPosition)
            case .following:
                handleFollowingState(userPosition: userPosition)
            case .playing:
                handlePlayingState()
            case .resting:
                handleRestingState()
            case .responding:
                handleRespondingState()
            }
        }
    
        private func handleIdleState(userPosition: SIMD3<Float>) {
            // Occasionally look around
            if stateTimer > 3.0 {
                lookAtRandomPoint()
                stateTimer = 0
            }
    
            // If user is far, start following
            let distance = length(userPosition - companion.position)
            if distance > 2.0 {
                transitionTo(.following)
            }
        }
    
        private func handleFollowingState(userPosition: SIMD3<Float>) {
            let distance = length(userPosition - companion.position)
    
            // Move toward user
            if distance > 1.0 {
                let direction = normalize(userPosition - companion.position)
                let targetPosition = userPosition - direction * 1.0
                moveToward(targetPosition)
            } else {
                transitionTo(.idle)
            }
        }
    
        private func transitionTo(_ newState: State) {
            currentState = newState
            stateTimer = 0
    
            switch newState {
            case .idle:
                animator.transitionToIdle()
            case .following:
                animator.playAnimation(named: "walk", loop: true)
            case .playing:
                animator.playAnimation(named: "play", loop: true)
            case .resting:
                animator.playAnimation(named: "rest", loop: true)
            case .responding:
                break // Animation set by trigger
            }
        }
    }
    

    Behavior Tree Pattern

    protocol BehaviorNode {
        func evaluate(context: CompanionContext) -> BehaviorResult
    }
    
    enum BehaviorResult {
        case success
        case failure
        case running
    }
    
    struct CompanionContext {
        var companion: Entity
        var userPosition: SIMD3<Float>
        var energy: Float
        var mood: CompanionMood
        var deltaTime: Double
    }
    
    // Sequence: Run children in order until one fails
    class SequenceNode: BehaviorNode {
        var children: [BehaviorNode]
    
        func evaluate(context: CompanionContext) -> BehaviorResult {
            for child in children {
                let result = child.evaluate(context: context)
                if result != .success {
                    return result
                }
            }
            return .success
        }
    }
    
    // Selector: Run children until one succeeds
    class SelectorNode: BehaviorNode {
        var children: [BehaviorNode]
    
        func evaluate(context: CompanionContext) -> BehaviorResult {
            for child in children {
                let result = child.evaluate(context: context)
                if result == .success {
                    return .success
                }
            }
            return .failure
        }
    }
    
    // Example leaf nodes
    class CheckEnergyNode: BehaviorNode {
        let threshold: Float
    
        func evaluate(context: CompanionContext) -> BehaviorResult {
            return context.energy > threshold ? .success : .failure
        }
    }
    
    class MoveToUserNode: BehaviorNode {
        func evaluate(context: CompanionContext) -> BehaviorResult {
            let distance = length(context.userPosition - context.companion.position)
            if distance < 1.0 {
                return .success
            }
            // Move toward user
            let direction = normalize(context.userPosition - context.companion.position)
            context.companion.position += direction * 0.01
            return .running
        }
    }
    

    Spatial Audio for Immersive Companions

    Basic Spatial Audio Setup

    func setupCompanionAudio(_ companion: Entity) {
        // Add spatial audio component
        var spatialAudio = SpatialAudioComponent()
        spatialAudio.gain = 0.8
        spatialAudio.directivity = .beam(focus: 0.5)
        companion.components.set(spatialAudio)
    }
    
    func playCompanionSound(_ companion: Entity, soundName: String) async {
        guard let audioResource = try? await AudioFileResource.load(
            named: soundName,
            in: nil,
            inputMode: .spatial,
            loadingStrategy: .preload,
            shouldLoop: false
        ) else { return }
    
        let audioController = companion.playAudio(audioResource)
    }
    

    Ambient and Reactive Audio

    class CompanionAudioManager {
        var companion: Entity
        var ambientController: AudioPlaybackController?
    
        func startAmbientSounds() async {
            guard let ambient = try? await AudioFileResource.load(
                named: "companion_ambient",
                inputMode: .spatial,
                loadingStrategy: .stream,
                shouldLoop: true
            ) else { return }
    
            ambientController = companion.playAudio(ambient)
            ambientController?.gain = 0.3
        }
    
        func playReactionSound(for mood: CompanionMood) async {
            let soundName: String
            switch mood {
            case .happy:
                soundName = "companion_happy"
            case .curious:
                soundName = "companion_curious"
            case .tired:
                soundName = "companion_yawn"
            case .neutral:
                soundName = "companion_chirp"
            }
    
            guard let sound = try? await AudioFileResource.load(
                named: soundName,
                inputMode: .spatial,
                shouldLoop: false
            ) else { return }
    
            companion.playAudio(sound)
        }
    
        func stopAmbient() {
            ambientController?.stop()
            ambientController = nil
        }
    }
    

    Entity Lifecycle Management

    Persistent Companion Manager

    @MainActor
    class CompanionLifecycleManager: ObservableObject {
        @Published var companion: Entity?
        @Published var isLoaded: Bool = false
        @Published var companionState: CompanionSaveState?
    
        private var arView: ARView?
        private var anchor: AnchorEntity?
    
        // MARK: - Lifecycle
    
        func initialize(arView: ARView) async throws {
            self.arView = arView
    
            // Create anchor for companion
            anchor = AnchorEntity(plane: .horizontal)
            arView.scene.addAnchor(anchor!)
    
            // Load companion
            let loadedCompanion = try await Entity.load(named: "Companion")
            companion = loadedCompanion
            anchor?.addChild(loadedCompanion)
    
            // Restore saved state if available
            if let savedState = loadSavedState() {
                applyState(savedState)
            }
    
            isLoaded = true
        }
    
        func suspend() {
            // Save state before suspending
            saveCurrentState()
    
            // Pause animations
            companion?.availableAnimations.forEach { animation in
                companion?.stopAllAnimations()
            }
        }
    
        func resume() {
            // Resume idle animation
            if let idle = companion?.availableAnimations.first(where: {
                $0.name?.contains("idle") == true
            }) {
                companion?.playAnimation(idle.repeat())
            }
        }
    
        func cleanup() {
            saveCurrentState()
            companion?.removeFromParent()
            anchor?.removeFromParent()
            companion = nil
            anchor = nil
            isLoaded = false
        }
    
        // MARK: - Persistence
    
        func saveCurrentState() {
            guard let companion = companion else { return }
    
            let state = CompanionSaveState(
                position: companion.position,
                orientation: companion.orientation,
                energy: companion.components[CompanionComponent.self]?.energy ?? 1.0,
                mood: companion.components[CompanionComponent.self]?.mood ?? .neutral,
                lastInteraction: Date()
            )
    
            if let encoded = try? JSONEncoder().encode(state) {
                UserDefaults.standard.set(encoded, forKey: "companionState")
            }
        }
    
        func loadSavedState() -> CompanionSaveState? {
            guard let data = UserDefaults.standard.data(forKey: "companionState"),
                  let state = try? JSONDecoder().decode(CompanionSaveState.self, from: data) else {
                return nil
            }
            return state
        }
    
        func applyState(_ state: CompanionSaveState) {
            companion?.position = state.position
            companion?.orientation = state.orientation
    
            var component = CompanionComponent()
            component.energy = state.energy
            component.mood = state.mood
            companion?.components.set(component)
    
            companionState = state
        }
    }
    
    struct CompanionSaveState: Codable {
        var position: SIMD3<Float>
        var orientation: simd_quatf
        var energy: Float
        var mood: CompanionMood
        var lastInteraction: Date
    }
    
    extension CompanionMood: Codable {}
    extension SIMD3: Codable where Scalar: Codable {}
    extension simd_quatf: Codable {}
    

    Scene Lifecycle Integration

    import SwiftUI
    
    struct ARCompanionApp: App {
        @StateObject var companionManager = CompanionLifecycleManager()
        @Environment(\.scenePhase) var scenePhase
    
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .environmentObject(companionManager)
                    .onChange(of: scenePhase) { oldPhase, newPhase in
                        switch newPhase {
                        case .active:
                            companionManager.resume()
                        case .inactive:
                            companionManager.suspend()
                        case .background:
                            companionManager.saveCurrentState()
                        @unknown default:
                            break
                        }
                    }
            }
        }
    }
    

    Gesture and Touch Handling

    Touch-Based Interaction

    class CompanionInteractionHandler {
        var arView: ARView
        var companion: Entity?
    
        init(arView: ARView) {
            self.arView = arView
            setupGestures()
        }
    
        private func setupGestures() {
            let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
            arView.addGestureRecognizer(tapGesture)
    
            let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
            arView.addGestureRecognizer(panGesture)
        }
    
        @objc func handleTap(_ gesture: UITapGestureRecognizer) {
            let location = gesture.location(in: arView)
    
            // Check if tapped on companion
            if let hitEntity = arView.entity(at: location),
               hitEntity == companion || hitEntity.isDescendant(of: companion!) {
                onCompanionTapped()
            } else {
                // Raycast to find new position for companion
                if let result = arView.raycast(from: location,
                                               allowing: .estimatedPlane,
                                               alignment: .horizontal).first {
                    moveCompanionTo(result.worldTransform.translation)
                }
            }
        }
    
        @objc func handlePan(_ gesture: UIPanGestureRecognizer) {
            guard let companion = companion else { return }
    
            let location = gesture.location(in: arView)
    
            switch gesture.state {
            case .changed:
                if let result = arView.raycast(from: location,
                                               allowing: .estimatedPlane,
                                               alignment: .horizontal).first {
                    companion.position = result.worldTransform.translation
                }
            case .ended:
                // Companion reached destination
                onCompanionMoved()
            default:
                break
            }
        }
    
        func onCompanionTapped() {
            // Trigger interaction response
            companion?.components[CompanionComponent.self]?.isInteracting = true
        }
    
        func moveCompanionTo(_ position: SIMD3<Float>) {
            // Animate companion to new position
            companion?.move(to: Transform(translation: position),
                           relativeTo: companion?.parent,
                           duration: 1.0)
        }
    
        func onCompanionMoved() {
            // Play arrival animation
        }
    }
    
    // Helper extension
    extension simd_float4x4 {
        var translation: SIMD3<Float> {
            SIMD3(columns.3.x, columns.3.y, columns.3.z)
        }
    }
    

    RealityUI Integration for 3D Controls

    import RealityUI
    
    class CompanionUIManager {
        var arView: ARView
        var companion: Entity?
    
        init(arView: ARView) {
            self.arView = arView
            RealityUI.registerComponents()
            RealityUI.enableGestures(.all, on: arView)
        }
    
        func addInteractionControls() {
            guard let companion = companion else { return }
    
            // Add a toggle for companion following mode
            let followToggle = RUISwitch()
            followToggle.position = SIMD3(0.3, 0.5, 0)
            followToggle.changedCallback = { [weak self] toggle in
                self?.setFollowMode(toggle.isOn)
            }
            companion.addChild(followToggle)
    
            // Add mood slider
            let moodSlider = RUISlider(length: 0.5, start: 0.5)
            moodSlider.position = SIMD3(-0.3, 0.5, 0)
            moodSlider.updateCallback = { [weak self] slider, state in
                self?.updateCompanionMood(value: slider.value)
            }
            companion.addChild(moodSlider)
        }
    
        func setFollowMode(_ enabled: Bool) {
            // Toggle following behavior
        }
    
        func updateCompanionMood(value: Float) {
            // Map slider value to mood
        }
    }
    

    ARView Configuration

    Optimal Configuration for Companions

    func configureARViewForCompanion(_ arView: ARView) {
        // Enable environment texturing for realistic lighting
        let config = ARWorldTrackingConfiguration()
        config.planeDetection = [.horizontal]
        config.environmentTexturing = .automatic
        config.frameSemantics.insert(.personSegmentationWithDepth)
    
        // Enable people occlusion (companion appears behind real people)
        if ARWorldTrackingConfiguration.supportsFrameSemantics(.personSegmentationWithDepth) {
            arView.environment.sceneUnderstanding.options.insert(.occlusion)
        }
    
        // Configure rendering
        arView.renderOptions = [
            .disablePersonOcclusion, // Remove if you want occlusion
            .disableMotionBlur
        ]
    
        // Enable camera exposure adaptation
        arView.environment.lighting.intensityExponent = 1.0
    
        arView.session.run(config)
    }
    

    Debug Visualization

    func enableDebugVisualization(_ arView: ARView, enabled: Bool) {
        if enabled {
            arView.debugOptions = [
                .showFeaturePoints,
                .showAnchorOrigins,
                .showPhysics,
                .showSceneUnderstanding
            ]
        } else {
            arView.debugOptions = []
        }
    }
    

    Troubleshooting Guide

    Common Issues and Solutions

    Companion Not Appearing

    // Ensure anchor is properly configured
    let anchor = AnchorEntity(plane: .horizontal, minimumBounds: [0.2, 0.2])
    
    // Check if plane detection is working
    arView.session.delegate = self
    
    extension YourClass: ARSessionDelegate {
        func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
            for anchor in anchors {
                if let planeAnchor = anchor as? ARPlaneAnchor {
                    print("Plane detected: \(planeAnchor.extent)")
                }
            }
        }
    }
    

    Animation Not Playing

    // Check available animations
    print("Available animations: \(entity.availableAnimations.map { $0.name })")
    
    // Ensure animation resource is valid
    guard !entity.availableAnimations.isEmpty else {
        print("No animations found in entity")
        return
    }
    

    Performance Issues

    // Reduce draw calls
    entity.generateCollisionShapes(recursive: true) // Do once, not per frame
    
    // Use simpler collision shapes
    let simpleShape = ShapeResource.generateSphere(radius: 0.5)
    entity.collision = CollisionComponent(shapes: [simpleShape])
    
    // Limit update frequency for AI
    class ThrottledCompanionAI {
        var updateInterval: Double = 0.1 // 10 updates per second
        var timeSinceUpdate: Double = 0
    
        func update(deltaTime: Double) {
            timeSinceUpdate += deltaTime
            if timeSinceUpdate >= updateInterval {
                performUpdate()
                timeSinceUpdate = 0
            }
        }
    }
    

    Companion Drifts or Moves Unexpectedly

    // Use world tracking quality checks
    extension YourClass: ARSessionDelegate {
        func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) {
            switch camera.trackingState {
            case .limited(let reason):
                handleLimitedTracking(reason)
            case .normal:
                resumeNormalOperation()
            case .notAvailable:
                showTrackingUnavailableUI()
            }
        }
    }
    

    Best Practices Summary

    1. Always use async loading for companion models to prevent UI freezes
    2. Register custom components and systems at app launch, before any RealityKit usage
    3. Save companion state on app suspension for continuity
    4. Throttle AI updates to 10-30fps to balance responsiveness and performance
    5. Use spatial audio to make companions feel present in the space
    6. Handle tracking state changes gracefully with user feedback
    7. Test on real devices - simulator limitations affect gesture and tracking accuracy
    8. Implement accessibility features for inclusive AR experiences

    Resource Links

    Official Apple Documentation

    • RealityKit Overview
    • RealityKit Documentation
    • Understanding RealityKit's Modular Architecture
    • Entity Component System

    Essential WWDC Sessions

    • Building Apps with RealityKit (2019)
    • Dive into RealityKit 2 (2021)
    • Build spatial experiences with RealityKit (2023)

    Community Packages

    • RealityActions - Procedural animation framework
    • BodyTracking - Body tracking utilities
    • FocusEntity - AR placement indicator
    • RealityUI - 3D UI components
    • HandVector - visionOS hand tracking
    • GoncharKit - visionOS helper utilities
    • RealityMorpher - Blend shape animations

    Sample Projects

    • RealityKit-Sampler - Comprehensive feature samples
    • ImmersiveMoveAndRotate - Gesture handling example
    • SwiftStrike - Apple's multiplayer game sample

    Skill Version: 2.0.0-enhanced Focus: AR Companion Development Platforms: iOS 15+, visionOS 1.0+ Generated by: Metatron (Enhanced from Skill Seeker output)

    Recommended Servers
    Excalidraw
    Excalidraw
    Thoughtbox
    Thoughtbox
    Nimble MCP Server
    Nimble MCP Server
    Repository
    domocarroll/wayfinding-companion
    Files