axiom-realitykit について
axiom-realitykitは、RealityKitで3D、AR、空間コンピューティングアプリを構築するための開発ガイドです。エンティティ・コンポーネント・システムの考え方を説明し、SwiftUI、操作、物理、描画、マルチプレイヤー同期を扱います。
主な機能
- 01エンティティ・コンポーネント・システムの解説
- 02エンティティ、コンポーネント、システムの説明
- 033DコンテンツとSwiftUIの連携
- 04ARアンカーとインタラクションの解説
- 05マテリアル、描画、物理の説明
- 06マルチプレイヤー同期の項目
活用シーン
- 01RealityKitでの3Dゲームや体験の構築
- 02SwiftUIアプリへの3Dモデルの組み込み
- 03AR配置とジェスチャーの追加
- 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 | 線速度と角速度 |
AnchoringComponent | AR アンカーへのアタッチ |
SynchronizationComponent | マルチプレイヤー同期 |
PerspectiveCameraComponent | カメラ設定 |
DirectionalLightComponent | ディレクショナルライト |
PointLightComponent | ポイントライト |
SpotLightComponent | スポットライト |
CharacterControllerComponent | キャラクター物理コントローラー |
AudioMixGroupsComponent | オーディオミキシング |
SpatialAudioComponent | 3D 位置オーディオ |
AmbientAudioComponent | 位置に依存しないオーディオ |
ChannelAudioComponent | マルチチャンネルオーディオ |
OpacityComponent | エンティティの透明度 |
GroundingShadowComponent | 接地シャドウ |
InputTargetComponent | ジェスチャー入力(visionOS) |
HoverEffectComponent | ホバー時のハイライト(visionOS) |
AccessibilityComponent | VoiceOver のサポート |
カスタムコンポーネント
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 |
CustomMaterial | Metal シェーダー関数 | 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