mcpField
A
axiom-realitykit

axiom-realitykit

github.com/charleswiltgen開発1.2k153 回インストール今日更新
リポジトリ

axiom-realitykit について

axiom-realitykitは、RealityKitで3D、AR、空間コンピューティングアプリを構築するための開発ガイドです。エンティティ・コンポーネント・システムの考え方を説明し、SwiftUI、操作、物理、描画、マルチプレイヤー同期を扱います。

主な機能

  1. 01エンティティ・コンポーネント・システムの解説
  2. 02エンティティ、コンポーネント、システムの説明
  3. 033DコンテンツとSwiftUIの連携
  4. 04ARアンカーとインタラクションの解説
  5. 05マテリアル、描画、物理の説明
  6. 06マルチプレイヤー同期の項目

活用シーン

  1. 01RealityKitでの3Dゲームや体験の構築
  2. 02SwiftUIアプリへの3Dモデルの組み込み
  3. 03AR配置とジェスチャーの追加
  4. 04物理処理や共有AR体験の実装

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-realitykit

インストール後、Claude Code で/axiom-realitykitを入力して読み込まれたか確認します。

詳細

RealityKit 開発ガイド

目的:RealityKit のエンティティ・コンポーネント・システム(ECS)アーキテクチャを使用して、3D コンテンツ、AR エクスペリエンス、空間コンピューティングアプリを構築します iOS バージョン:iOS 13 以降(基本)、iOS 18 以降(iOS の RealityView)、visionOS 1.0 以降 Xcode:Xcode 15 以降

このスキルを使う場面

次のような場合にこのスキルを使用します:

  • 3D エクスペリエンス(AR、ゲーム、ビジュアライゼーション、空間コンピューティング)を構築する
  • 3D コンテンツを含む SwiftUI アプリ(RealityView、Model3D)を作成する
  • アンカー(ワールド、画像、顔、身体トラッキング)を使った AR を実装する
  • エンティティ・コンポーネント・システム(ECS)アーキテクチャを扱う
  • 物理演算、衝突、空間インタラクションを設定する
  • マルチプレイヤーまたは共有 AR エクスペリエンスを構築する
  • SceneKit から RealityKit に移行する
  • visionOS を対象とする

次の用途にはこのスキルを使用しないでください:

  • SceneKit の保守(axiom-scenekit を使用)
  • 2D ゲーム(axiom-spritekit を使用)
  • Metal シェーダープログラミング(axiom-metal-migration-ref を使用)
  • GPU コンピューティングのみ(Metal を直接使用)

1. メンタルモデル:ECS とシーングラフ

シーングラフ(SceneKit)

SceneKit では、ノードが自身のプロパティを所有します。ノードは、レンダリング、衝突、アニメーションが可能なオブジェクトそのものです。

エンティティ・コンポーネント・システム(RealityKit)

RealityKit では、エンティティは空のコンテナです。コンポーネントがデータを追加し、システムがそのデータを処理します。

Entity (identity + hierarchy)
  ├── TransformComponent (position, rotation, scale)
  ├── ModelComponent (mesh + materials)
  ├── CollisionComponent (collision shapes)
  ├── PhysicsBodyComponent (mass, mode)
  └── [YourCustomComponent] (game-specific data)

System (processes entities with specific components each frame)

ECS が重要な理由:

  • 継承より合成:任意のエンティティに任意のコンポーネントを組み合わせられます
  • データ指向:システムはコンポーネントの配列を効率的に処理します
  • ロジックの分離:システム同士は互いを認識しません
  • テスト可能:コンポーネントは純粋なデータであり、システムは純粋なロジックです
ECS へのメンタルシフト
シーングラフの考え方ECS の考え方
「プレイヤーノードが移動する」「移動システムが MovementComponent を持つエンティティを処理する」
「ノードのサブクラスにメソッドを追加する」「コンポーネントを追加し、システムを作成する」
「ノードの update(_:) をオーバーライドする」「コンポーネントを検索する System を登録する」
「ノードが自身の体力を把握している」「HealthComponent がデータを保持し、DamageSystem が処理する」

2. エンティティ階層

エンティティの作成
// Empty entity
let entity = Entity()
entity.name = "player"

// Entity with components
let entity = Entity()
entity.components[ModelComponent.self] = ModelComponent(
    mesh: .generateBox(size: 0.1),
    materials: [SimpleMaterial(color: .blue, isMetallic: false)]
)

// ModelEntity convenience (has ModelComponent built in)
let box = ModelEntity(
    mesh: .generateBox(size: 0.1),
    materials: [SimpleMaterial(color: .red, isMetallic: true)]
)
階層の管理
// Parent-child
parent.addChild(child)
child.removeFromParent()

// Find entities
let found = root.findEntity(named: "player")

// Enumerate
for child in entity.children {
    // Process children
}

// Clone
let clone = entity.clone(recursive: true)
トランスフォーム
// Local transform (relative to parent)
entity.position = SIMD3<Float>(0, 1, 0)
entity.orientation = simd_quatf(angle: .pi / 4, axis: SIMD3(0, 1, 0))
entity.scale = SIMD3<Float>(repeating: 2.0)

// World-space queries
let worldPos = entity.position(relativeTo: nil)
let worldTransform = entity.transform(relativeTo: nil)

// Set world-space transform
entity.setPosition(SIMD3(1, 0, 0), relativeTo: nil)

// Look at a point
entity.look(at: targetPosition, from: entity.position, relativeTo: nil)

3. コンポーネント

組み込みコンポーネント
コンポーネント目的
Transform位置、回転、スケール
ModelComponentメッシュジオメトリ+マテリアル
CollisionComponent物理演算とインタラクション用の衝突形状
PhysicsBodyComponent質量、物理演算モード(動的/静的/キネマティック)
PhysicsMotionComponent線速度と角速度
AnchoringComponentAR アンカーへのアタッチ
SynchronizationComponentマルチプレイヤー同期
PerspectiveCameraComponentカメラ設定
DirectionalLightComponentディレクショナルライト
PointLightComponentポイントライト
SpotLightComponentスポットライト
CharacterControllerComponentキャラクター物理コントローラー
AudioMixGroupsComponentオーディオミキシング
SpatialAudioComponent3D 位置オーディオ
AmbientAudioComponent位置に依存しないオーディオ
ChannelAudioComponentマルチチャンネルオーディオ
OpacityComponentエンティティの透明度
GroundingShadowComponent接地シャドウ
InputTargetComponentジェスチャー入力(visionOS)
HoverEffectComponentホバー時のハイライト(visionOS)
AccessibilityComponentVoiceOver のサポート
カスタムコンポーネント
struct HealthComponent: Component {
    var current: Int
    var maximum: Int

    var percentage: Float {
        Float(current) / Float(maximum)
    }
}

// Register before use (typically in app init)
HealthComponent.registerComponent()

// Attach to entity
entity.components[HealthComponent.self] = HealthComponent(current: 100, maximum: 100)

// Read
if let health = entity.components[HealthComponent.self] {
    print(health.current)
}

// Modify
entity.components[HealthComponent.self]?.current -= 10
コンポーネントのライフサイクル

コンポーネントは値型(struct)です。コンポーネントを読み取り、変更してから書き戻すと、コンポーネント全体が置き換えられます:

// Read-modify-write pattern
var health = entity.components[HealthComponent.self]!
health.current -= damage
entity.components[HealthComponent.self] = health

アンチパターン:コンポーネントへの参照を保持し、変更が反映されると期待すること。コンポーネントは読み取り時にコピーされます。


4. システム

System プロトコル
struct DamageSystem: System {
    // Define which components this system needs
    static let query = EntityQuery(where: .has(HealthComponent.self))

    init(scene: RealityKit.Scene) {
        // One-time setup
    }

    func update(context: SceneUpdateContext) {
        for entity in context.entities(matching: Self.query,
                                        updatingSystemWhen: .rendering) {
            var health = entity.components[HealthComponent.self]!
            if health.current <= 0 {
                entity.removeFromParent()
            }
        }
    }
}

// Register system
DamageSystem.registerSystem()
システム設計のベストプラクティス
  • システムごとに責務を1つにする:MovementSystem、DamageSystem、RenderingSystem のようにし、GameLogicSystem にはしません
  • クエリでフィルタリングする:処理対象外のエンティティを処理しないよう、正確なクエリを使用します
  • 順序が重要です:システムは登録順に実行されます。依存関係のあるシステムを先に登録してください。
  • エンティティの参照を保存しない:毎フレームクエリしてください。エンティティの参照は古くなることがあります。
イベント処理
// Subscribe to collision events
scene.subscribe(to: CollisionEvents.Began.self) { event in
    let entityA = event.entityA
    let entityB = event.entityB
    // Handle collision
}

// Subscribe to scene update
scene.subscribe(to: SceneEvents.Update.self) { event in
    let deltaTime = event.deltaTime
    // Per-frame logic
}

5. SwiftUI との統合

RealityView(iOS 18 以降、visionOS 1.0 以降)
struct ContentView: View {
    var body: some View {
        RealityView { content in
            // make closure — called once
            let box = ModelEntity(
                mesh: .generateBox(size: 0.1),
                materials: [SimpleMaterial(color: .blue, isMetallic: false)]
            )
            content.add(box)

        } update: { content in
            // update closure — called when SwiftUI state changes
        }
    }
}
カメラを使用する RealityView(iOS)

iOS では、RealityView にカメラコンテンツ用のパラメーターがあり、AR カメラまたは仮想カメラを設定できます。

RealityView { content, attachments in
    // Load 3D content
    if let model = try? await ModelEntity(named: "scene") {
        content.add(model)
    }
}
コンテンツの非同期読み込み
RealityView { content in
    // Load from bundle
    if let entity = try? await Entity(named: "MyScene", in: .main) {
        content.add(entity)
    }

    // Load from URL
    if let entity = try? await Entity(contentsOf: modelURL) {
        content.add(entity)
    }
}
Model3D(シンプルな表示)
// Simple 3D model display (no interaction)
Model3D(named: "toy_robot") { model in
    model
        .resizable()
        .scaledToFit()
} placeholder: {
    ProgressView()
}
SwiftUI アタッチメント(visionOS)
RealityView { content, attachments in
    let entity = ModelEntity(mesh: .generateSphere(radius: 0.1))
    content.add(entity)

    if let label = attachments.entity(for: "priceTag") {
        label.position = SIMD3(0, 0.15, 0)
        entity.addChild(label)
    }
} attachments: {
    Attachment(id: "priceTag") {
        Text("$9.99")
            .padding()
            .glassBackgroundEffect()
    }
}
状態バインディングのパターン
struct GameView: View {
    @State private var score = 0

    var body: some View {
        VStack {
            Text("Score: \(score)")

            RealityView { content in
                let scene = try! await Entity(named: "GameScene")
                content.add(scene)
            } update: { content in
                // React to state changes
                // Note: update is called when SwiftUI state changes,
                // not every frame. Use Systems for per-frame logic.
            }
        }
    }
}

6. iOS での AR

AnchorEntity
// Horizontal plane
let anchor = AnchorEntity(.plane(.horizontal, classification: .table,
                                  minimumBounds: SIMD2(0.2, 0.2)))

// Vertical plane
let anchor = AnchorEntity(.plane(.vertical, classification: .wall,
                                  minimumBounds: SIMD2(0.5, 0.5)))

// World position
let anchor = AnchorEntity(world: SIMD3<Float>(0, 0, -1))

// Image anchor
let anchor = AnchorEntity(.image(group: "AR Resources", name: "poster"))

// Face anchor (front camera)
let anchor = AnchorEntity(.face)

// Body anchor
let anchor = AnchorEntity(.body)
SpatialTrackingSession(iOS 18 以降)
let session = SpatialTrackingSession()
let configuration = SpatialTrackingSession.Configuration(tracking: [.plane, .object])
let result = await session.run(configuration)

if let notSupported = result {
    // Handle unsupported tracking on this device
    for denied in notSupported.deniedTrackingModes {
        print("Not supported: \(denied)")
    }
}
AR のベストプラクティス
  • 安定性を高めるため、ワールド座標ではなく検出した面にエンティティをアンカーします
  • 平面の分類(.table、.floor、.wall)を使って、コンテンツを適切な場所に配置します
  • 水平面の検出から始めます。最も信頼性が高い方法です
  • 実機でテストしてください。シミュレーターでの AR には制限があります
  • 面の検出中に視覚的なフィードバック(コーチングオーバーレイ)を表示します

7. インタラクション

ManipulationComponent(iOS、visionOS)
// Enable drag, rotate, scale gestures
entity.components[ManipulationComponent.self] = ManipulationComponent(
    allowedModes: .all  // .translate, .rotate, .scale
)

// Also requires CollisionComponent for hit testing
entity.generateCollisionShapes(recursive: true)
InputTargetComponent(visionOS)
// Required for visionOS gesture input
entity.components[InputTargetComponent.self] = InputTargetComponent()
entity.components[CollisionComponent.self] = CollisionComponent(
    shapes: [.generateBox(size: SIMD3(0.1, 0.1, 0.1))]
)
SwiftUI とのジェスチャー統合
RealityView { content in
    let entity = ModelEntity(mesh: .generateBox(size: 0.1))
    entity.generateCollisionShapes(recursive: true)
    entity.components.set(InputTargetComponent())
    content.add(entity)
}
.gesture(
    TapGesture()
        .targetedToAnyEntity()
        .onEnded { value in
            let tappedEntity = value.entity
            // Handle tap
        }
)
.gesture(
    DragGesture()
        .targetedToAnyEntity()
        .onChanged { value in
            value.entity.position = value.convert(value.location3D,
                from: .local, to: .scene)
        }
)
ヒットテスト
// Ray-cast from screen point
if let result = arView.raycast(from: screenPoint,
                                allowing: .estimatedPlane,
                                alignment: .horizontal).first {
    let worldPosition = result.worldTransform.columns.3
    // Place entity at worldPosition
}

8. マテリアルとレンダリング

マテリアルの種類
マテリアル用途カスタマイズ
SimpleMaterial単色またはテクスチャ色、メタリック、粗さ
PhysicallyBasedMaterialフル PBRすべての PBR マップ(ベースカラー、法線、メタリック、粗さ、AO、発光)
UnlitMaterialライティングの影響を受けない色またはテクスチャ。常に完全に照らされます
OcclusionMaterial見えませんが、遮蔽しますAR コンテンツを実物の背後に隠します
VideoMaterial表面での動画再生AVPlayer による制御
ShaderGraphMaterialカスタムシェーダーグラフReality Composer Pro
CustomMaterialMetal シェーダー関数Metal を完全に制御
PhysicallyBasedMaterial
var material = PhysicallyBasedMaterial()
material.baseColor = .init(tint: .white,
    texture: .init(try! .load(named: "albedo")))
material.metallic = .init(floatLiteral: 0.0)
material.roughness = .init(floatLiteral: 0.5)
material.normal = .init(texture: .init(try! .load(named: "normal")))
material.ambientOcclusion = .init(texture: .init(try! .load(named: "ao")))
material.emissiveColor = .init(color: .blue)
material.emissiveIntensity = 2.0

let entity = ModelEntity(
    mesh: .generateSphere(radius: 0.1),
    materials: [material]
)
OcclusionMaterial(AR)
// Invisible plane that hides 3D content behind it
let occluder = ModelEntity(
    mesh: .generatePlane(width: 1, depth: 1),
    materials: [OcclusionMaterial()]
)
occluder.position = SIMD3(0, 0, 0)
anchor.addChild(occluder)
環境ライティング
// Image-based lighting
if let resource = try? await EnvironmentResource(named: "studio_lighting") {
    // Apply via RealityView content
}

9. 物理演算と衝突

衝突形状
// Generate from mesh (accurate but expensive)
entity.generateCollisionShapes(recursive: true)

// Manual shapes (prefer for performance)
entity.components[CollisionComponent.self] = CollisionComponent(
    shapes: [
        .generateBox(size: SIMD3(0.1, 0.2, 0.1)),     // Box
        .generateSphere(radius: 0.1),                   // Sphere
        .generateCapsule(height: 0.3, radius: 0.05)     // Capsule
    ]
)
物理ボディ
// Dynamic — physics simulation controls movement
entity.components[PhysicsBodyComponent.self] = PhysicsBodyComponent(
    massProperties: .init(mass: 1.0),
    material: .generate(staticFriction: 0.5,
                        dynamicFriction: 0.3,
                        restitution: 0.4),
    mode: .dynamic
)

// Static — immovable collision surface
ground.components[PhysicsBodyComponent.self] = PhysicsBodyComponent(
    mode: .static
)

// Kinematic — code-controlled, participates in collisions
platform.components[PhysicsBodyComponent.self] = PhysicsBodyComponent(
    mode: .kinematic
)
衝突グループとフィルター
// Define groups
let playerGroup = CollisionGroup(rawValue: 1 << 0)
let enemyGroup = CollisionGroup(rawValue: 1 << 1)
let bulletGroup = CollisionGroup(rawValue: 1 << 2)

// Filter: player collides with enemies and bullets
entity.components[CollisionComponent.self] = CollisionComponent(
    shapes: [.generateSphere(radius: 0.1)],
    filter: CollisionFilter(
        group: playerGroup,
        mask: enemyGroup | bulletGroup
    )
)
衝突イベント
// Subscribe in RealityView make closure or System
scene.subscribe(to: CollisionEvents.Began.self, on: playerEntity) { event in
    let otherEntity = event.entityA == playerEntity ? event.entityB : event.entityA
    handleCollision(with: otherEntity)
}
力の適用
if var motion = entity.components[PhysicsMotionComponent.self] {
    motion.linearVelocity = SIMD3(0, 5, 0)  // Impulse up
    entity.components[PhysicsMotionComponent.self] = motion
}

10. アニメーション

トランスフォームアニメーション
// Animate to position over duration
entity.move(
    to: Transform(
        scale: SIMD3(repeating: 1.5),
        rotation: simd_quatf(angle: .pi, axis: SIMD3(0, 1, 0)),
        translation: SIMD3(0, 2, 0)
    ),
    relativeTo: entity.parent,
    duration: 2.0,
    timingFunction: .easeInOut
)
USD アニメーションの再生
if let entity = try? await Entity(named: "character") {
    // Play all available animations
    for animation in entity.availableAnimations {
        entity.playAnimation(animation.repeat())
    }
}
アニメーション再生の制御
let controller = entity.playAnimation(animation)
controller.pause()
controller.resume()
controller.speed = 2.0      // 2x playback speed
controller.blendFactor = 0.5 // Blend with current state

11. オーディオ

空間オーディオ
// Load audio resource
let resource = try! AudioFileResource.load(named: "engine.wav",
    configuration: .init(shouldLoop: true))

// Create entity with spatial audio
let audioEntity = Entity()
audioEntity.components[SpatialAudioComponent.self] = SpatialAudioComponent()
let controller = audioEntity.playAudio(resource)

// Position the audio source in 3D space
audioEntity.position = SIMD3(2, 0, -1)
環境オーディオ
entity.components[AmbientAudioComponent.self] = AmbientAudioComponent()
entity.playAudio(backgroundMusic)

12. パフォーマンス

エンティティ数
  • 100 エンティティ未満:特に問題ありません
  • 100~1000 エンティティ:RealityKit デバッガーで監視します
  • 1000 エンティティ以上:インスタンシングと LOD 戦略を使用します
インスタンシング
// Share mesh and material across many entities
let sharedMesh = MeshResource.generateSphere(radius: 0.01)
let sharedMaterial = SimpleMaterial(color: .white, isMetallic: false)

for i in 0..<1000 {
    let entity = ModelEntity(mesh: sharedMesh, materials: [sharedMaterial])
    entity.position = randomPosition()
    parent.addChild(entity)
}

RealityKit は、メッシュとマテリアルのリソースが同一のエンティティを自動的にバッチ処理します。

コンポーネントの頻繁な変更

アンチパターン:毎フレーム、コンポーネントを作成して置き換えること。

// BAD — component allocation every frame
func update(context: SceneUpdateContext) {
    for entity in context.entities(matching: query, updatingSystemWhen: .rendering) {
        entity.components[ModelComponent.self] = ModelComponent(
            mesh: .generateBox(size: 0.1),
            materials: [newMaterial]  // New allocation every frame
        )
    }
}

// GOOD — modify existing component
func update(context: SceneUpdateContext) {
    for entity in context.entities(matching: query, updatingSystemWhen: .rendering) {
        // Only update when actually needed
        if needsUpdate {
            var model = entity.components[ModelComponent.self]!
            model.materials = [cachedMaterial]
            entity.components[ModelComponent.self] = model
        }
    }
}
衝突形状の最適化
  • メッシュベースの衝突ではなく、単純な形状(ボックス、球、カプセル)を使用します
  • generateCollisionShapes(recursive: true) は便利ですが、処理コストが高くなります
  • 静的ジオメトリの場合、セットアップ時に一度だけ形状を生成します
プロファイリング

Xcode の RealityKit デバッガーを使用します。

  • エンティティインスペクター:エンティティの階層とコンポーネントを表示します
  • 統計オーバーレイ:エンティティ数、描画呼び出し数、三角形数を表示します
  • 物理演算の可視化:衝突形状を表示します

13. マルチプレイヤー

同期の基本
// Components sync automatically if they conform to Codable
struct ScoreComponent: Component, Codable {
    var points: Int
}

// SynchronizationComponent controls what syncs
entity.components[SynchronizationComponent.self] = SynchronizationComponent()
MultipeerConnectivityService
let service = try MultipeerConnectivityService(session: mcSession)
// Entities with SynchronizationComponent auto-sync across peers
オーナーシップ
  • エンティティを変更できるのは、そのオーナーだけです
  • 共有エンティティを変更する前に、オーナーシップをリクエストします
  • Codable ではないコンポーネントデータは同期されません

14. アンチパターン

アンチパターン 1:ECS での UIKit 流の考え方

時間コスト:アーキテクチャに逆らって試行錯誤することで、何時間も無駄になります

// BAD — subclassing Entity for behavior
class PlayerEntity: Entity {
    func takeDamage(_ amount: Int) { /* logic in entity */ }
}

// GOOD — component holds data, system has logic
struct HealthComponent: Component { var hp: Int }
struct DamageSystem: System {
    static let query = EntityQuery(where: .has(HealthComponent.self))
    func update(context: SceneUpdateContext) {
        // Process damage here
    }
}
アンチパターン 2:モノリシックなエンティティ

時間コスト:テストしにくく、柔軟性のないアーキテクチャになります

ゲームロジックを 1 つのエンティティ型にすべて詰め込まないでください。組み合わせて使えるコンポーネントに分割しましょう。

アンチパターン 3:システムを使わないフレームベースの更新

時間コスト:フレーム更新が失われ、動作に一貫性がなくなります

// BAD — timer-based updates
Timer.scheduledTimer(withTimeInterval: 1/60, repeats: true) { _ in
    entity.position.x += 0.01
}

// GOOD — System update
struct MovementSystem: System {
    static let query = EntityQuery(where: .has(VelocityComponent.self))
    func update(context: SceneUpdateContext) {
        for entity in context.entities(matching: Self.query,
                                        updatingSystemWhen: .rendering) {
            let velocity = entity.components[VelocityComponent.self]!
            entity.position += velocity.value * Float(context.deltaTime)
        }
    }
}
アンチパターン 4:インタラクティブなエンティティの衝突形状を生成しない

時間コスト:「タップが反応しないのはなぜか」のデバッグに 15~30 分かかります

ジェスチャには CollisionComponent が必要です。エンティティに InputTargetComponent(visionOS)または ManipulationComponent があっても、CollisionComponent がなければ、ジェスチャは一切発生しません。

アンチパターン 5:システムにエンティティへの参照を保存する

時間コスト:古い参照によるクラッシュ

// BAD — entity might be removed between frames
struct BadSystem: System {
    var playerEntity: Entity?  // Stale reference risk

    func update(context: SceneUpdateContext) {
        playerEntity?.position.x += 0.1  // May crash
    }
}

// GOOD — query each frame
struct GoodSystem: System {
    static let query = EntityQuery(where: .has(PlayerComponent.self))

    func update(context: SceneUpdateContext) {
        for entity in context.entities(matching: Self.query,
                                        updatingSystemWhen: .rendering) {
            entity.position.x += Float(context.deltaTime)
        }
    }
}

15. コードレビューのチェックリスト

  • カスタムコンポーネントは、使用前に registerComponent() で登録されている
  • システムは、シーンの読み込み前に registerSystem() で登録されている
  • コンポーネントはクラスではなく、値型(構造体)である
  • コンポーネントの更新には、読み取り・変更・書き込みのパターンを使用している
  • インタラクティブなエンティティに CollisionComponent がある
  • visionOS のインタラクティブなエンティティに InputTargetComponent がある
  • 可能な場合、衝突形状は単純な形状(ボックス/球/カプセル)にしている
  • システム内でフレームをまたいでエンティティへの参照を保存していない
  • 同一のエンティティ間でメッシュとマテリアルのリソースを共有している
  • コンポーネントは値が実際に変化した場合にのみ更新している
  • 3D アセットには USD/USDZ 形式を使用している(.scn ではない)
  • モデル/シーンの読み込みにはすべて非同期読み込みを使用している
  • クロージャーベースのサブスクリプションでビュー/コントローラーを保持する場合、[weak self] を使用している

16. プレッシャーのかかるシナリオ

シナリオ 1:「シンプルなアプリに ECS は大げさです」

プレッシャー:チームは ECS を学ぶのを避けたがっており、3D モデルを 1 つ表示するだけでよいと考えています

誤ったアプローチ:ECS を使わず、すべてのロジックを RealityView のクロージャーに詰め込みます。

正しいアプローチ:シンプルなアプリでも ECS のメリットがあります。RealityView 内の単一の ModelEntity も、すでに ECS を使用しています。まだカスタムコンポーネントを追加していないだけです。シンプルに始めて、複雑さが増すにつれてコンポーネントを追加しましょう。

反論のテンプレート:「すでに ECS、つまり Entity と ModelComponent を使用しています。このパターンは規模を拡大できます。動作が必要になったときにカスタムコンポーネントを追加するのは、構造体を 1 つ定義するだけで、アーキテクチャの変更ではありません。」

シナリオ 2:「使い慣れている SceneKit を使いましょう」

プレッシャー:チームは SceneKit の経験があり、RealityKit には慣れていません

誤ったアプローチ:新機能を SceneKit で構築します。

正しいアプローチ:SceneKit は実質的に非推奨です。新機能は追加されません。今すぐ RealityKit に投資しましょう。必要になれば、ECS の概念はほかのゲームエンジン(Unity、Unreal、Bevy)にも応用できます。

反論のテンプレート:「SceneKit はメンテナンスモードで、新機能は追加されず、セキュリティパッチのみが提供されます。SceneKit のコードを 1 行書くごとに、移行の負債が増えます。RealityKit の概念(Entity、Component、System)は、業界標準の ECS です。」

シナリオ 3:「衝突形状なしで動作させましょう」

プレッシャー:締め切りが迫っており、衝突形状の設定は複雑そうです

誤ったアプローチ:衝突形状を省略し、位置に基づく近接判定を使います。

正しいアプローチ:entity.generateCollisionShapes(recursive: true) を呼び出すだけで済みます。これがないと、ジェスチャは機能せず、物理演算でも衝突しません。この「近道」は、節約できる時間よりも多くのデバッグ時間を生みます。

反論のテンプレート:「ジェスチャと物理演算には衝突形状が必要です。entity.generateCollisionShapes(recursive: true) を 1 行追加するだけです。省略するとジェスチャが何も表示せずに失敗するため、原因の特定が難しくなります。」


リソース

WWDC:2019-603、2019-605、2021-10074、2022-10074、2023-10080、2023-10081、2024-10103、2024-10153

ドキュメント:/realitykit、/realitykit/entity、/realitykit/realityview、/realitykit/modelentity、/realitykit/anchorentity、/realitykit/component

スキル:axiom-realitykit-ref、axiom-realitykit-diag、axiom-scenekit、axiom-scenekit-ref

コメント