🥽

【Swift】visionOSで鬼をカメラの方向へ動かす

2024/04/01に公開

はじめに

前回は【USDZ】AIを使ってモデリングするで鬼を作成しました。
今回は、作成した鬼をvisionOSにインポートし、カメラの方向へ動かすようにしたいと思います。
その中でRealityKitの「ECS」[1]という概念について学びました。

つくったもの

環境

  1. Xcode Version 15.3
  2. visionOS 1.1

前提条件

  1. アプリはFullSpaceに入る必要があります。
  2. 一部のARKitデータにはアクセス権限が必要です。

ECSとは

RealityKitでは、オブジェクトのデータと動作を構造化するために「Entity」、「Component」、「System」という3つの要素が用いられます。これらの要素は、従来のオブジェクト指向プログラミングが機能と状態をカプセル化するのとは異なり、データ指向のアプローチを取り入れています。ここで、「System」は機能を、「Component」は状態を担当し、「Entity」はこれらをグループ化する識別子として機能します。RealityKitを用いたアーキテクチャでは、Entity以外に共通の祖先を持たない二つのオブジェクトであっても、同じコンポーネントを追加することで、それらに同じ動作や機能を与えることが可能になります。

Entityとは

Entity[2]とはコンテナオブジェクトです。空のEntityを作成してもEntityがレンダリングしたり動作を実行することはありません。コンポーネントをまとめるためのコンテナとなります。

鬼を表示する

鬼のUSDZファイルをRealityViewに読み込むには、load(named:in:)[3]メソッドを使用します。これでModelComponentをEntityとして追加し、ModelEntityと同じ機能を持つEntityが作成されます。

EnemyModel.swift
let oni = try? Entity.load(named: "oni")

簡単に表示できましたね!
さあ、ここから鬼を動かしていきましょう!

Componentとは

Component[4]とは動作や外観を表現するためのオブジェクトです。Entityのインスタンスにコンポーネントを追加することで、特定の要素を組み立てます。
例えば、ModelComponentは視覚的な外観を提供し、TransformComponentは空間内の位置を定義します。

鬼にコンポーネントを追加する

鬼にコンポーネントを追加していきます。アニメーションは作成したUSDZに組み込まれているので、availableAnimationsでアニメーションが再生できることをチェックしてから属性やコンポーネントを追加しています。

ここでは衝突判定に必要なCollisionComponentと、鬼をカメラの方向へ動かすのに必要なMovingComponentというカスタムコンポーネントを追加しました。

EnemyModel.swift
if let oni = try? Entity.load(named: "oni"),
   let animation = oni.availableAnimations.first {

    oni.name = "Oni"
    oni.position.z = -2.0
    oni.scale *= 2
    oni.components.set(CollisionComponent(
        shapes: [.generateCapsule(height: 1.5, radius: 0.5)],
        mode: .default
    ))
    oni.components.set(MovingComponent())

    oni.playAnimation(animation.repeat())
    
    return oni
} else { return nil }

MovingComponentでは動作速度を定義しました。

MovingComponent.swift
public struct MovingComponent: Component, Codable {
    public var moveSpeed: Float = 0.1

    public init() { }
}

これで、アニメーションが再生され、print(oni)でコンポーネントがあることも確認できました。
しかし、まだカメラの方向へ動いてくれません。そこでSystemを使います。

print(oni)の中身

▿ 'Oni' : Entity, children: 1
⟐ CollisionComponent
⟐ Transform
⟐ SynchronizationComponent
▿ 'oni_walk' : Entity, children: 4
⟐ Transform
⟐ SynchronizationComponent
▿ 'Light' : Entity
⟐ Transform
⟐ SynchronizationComponent
▿ 'Camera' : Entity
⟐ Transform
⟐ SynchronizationComponent
▿ 'geometry_0_001' : Entity
⟐ Transform
⟐ SynchronizationComponent
▿ 'mixamorig_Hips' : ModelEntity, children: 2
⟐ ModelComponent
⟐ Transform
⟐ SynchronizationComponent
▿ 'Skeleton' : Entity
⟐ Transform
⟐ SynchronizationComponent
▿ 'geometry_0_001_geometry' : Entity
⟐ Transform
⟐ SynchronizationComponent

Systemとは

System[5]とはフレーム内の複数のEntityに影響を与えるオブジェクトです。
シーン内にある様々なタイプのオブジェクトやキャラクターをフレームごとに更新し、動作やロジックを実装することができます。

例えば、MovingSystemを使って、鬼がカメラの方向へ移動する機能を実装します。

MovingSystem.swift
class MovingSystem : System {
    required init(scene: Scene) { 
        // Perform required initialization or setup.
    }
    
    func update(context: SceneUpdateContext) {
        // RealityKit automatically calls this every frame for every scene.
    }
}

鬼をカメラの方向へ動かす

まず、毎フレームごとにEntityを取得し、処理を実行したいのでSystemにあるupdate(context:)メソッドを用います。

MovingSystem.swift
func update(context: SceneUpdateContext) {
    // この中に書いていく
}

次にSceneからEntityを効率的に取得するため、EntityQueryを使用します。これを使用すると、すべてのEntity、またはシステムに関連するEntityのサブセットを取得することができます。
updateメソッドのcontext.sceneからMovingComponentがついているEntityを取得します。

MovingSystem.swift
let entities = context.scene.performQuery(EntityQuery(where: .has(MovingComponent.self)))

そして取得したentitiesに対して処理を実行していきます。

MovingSystem.swift
for entity in entities {
    guard let movingComponent = entity.components[MovingComponent.self] else { return }
    
    // カメラの方を向く
    entity.look(at: cameraPosition,
                from: entity.position(relativeTo: nil),
                relativeTo: nil,
                forward: .positiveZ)
    
    // 移動速度を設定
    let moveSpeed: Float = movingComponent.moveSpeed

    // 敵の前進方向を計算(カメラの方を向いているため、その前方向が移動方向になる)
    let forwardDirection = entity.transform.matrix.columns.2.xyz
    let newPosition = entity.position(relativeTo: nil) + (forwardDirection * moveSpeed)
    
    // 毎フレームの更新で移動
    entity.position = newPosition
}

最後にコンポーネントとシステムをRealityKitに通知しなくてはいけません。
アプリ起動時に一度だけ登録すればいいので、MyAppのイニシャライザでregisterComponentregisterSystemを実行しましょう。
RealityKitがアクティブなシーンごとにインスタンスを自動的に作成し、フレームごとに update(context:)メソッドを繰り返し呼び出してくれます。

MyApp.swift
init() {
    MovingComponent.registerComponent()
    MovingSystem.registerSystem()
}

カメラの位置を取得する

カメラの位置はARKitSessionWorldTrackingProviderから取得します。
MovingSystemのイニシャライザでARKitSessionを実行し、updateメソッドでDeviceAnchorを取得しました。

MovingSystem.swift
required init(scene: Scene) { 
    try await arkitSession.run([worldTrackingProvider])
}

updateメソッドにcamaraPositionを取得する処理を追加します。

MovingSystem.swift
guard let deviceAnchor = worldTrackingProvider.queryDeviceAnchor(atTimestamp: CACurrentMediaTime()) else { return }

let cameraTransform = Transform(matrix: deviceAnchor.originFromAnchorTransform)
// カメラとentityの高さを合わせる
let cameraPosition = SIMD3(cameraTransform.translation.x, cameraTransform.translation.y - 1.5, cameraTransform.translation.z)

これでカメラの方向に鬼が動いてくれるようになりました。

おわりに

以上、「visionOSで鬼をカメラの方向へ動かす」でした。
少し難しい概念でしたが、ゲームなどの開発を行う上で強力かつ必須な機能だと感じました。

ここからは鬼をランダムで複数体生成し、エナジーボールで衝突判定して撃退するゲームを作っていきたいと思います。
ゾンビゲームの和風版って感じですね。笑
Apple Vision Proの日本発売までにリリースしたいと思うのでご意見等いただけるとありがたいです。

今後もvisionOSについて発信していきますので、この記事が参考になったと思ったらぜひ♡をお願いします。

参考

https://developer.apple.com/videos/play/wwdc2021/10074/
https://developer.apple.com/documentation/visionos/diorama/

脚注
  1. https://developer.apple.com/documentation/realitykit/realitykit-systems ↩︎

  2. https://developer.apple.com/documentation/realitykit/entity/ ↩︎

  3. https://developer.apple.com/documentation/realitykit/loading-entities-from-a-file#Load-an-Entity-Hierarchy-Synchronously ↩︎

  4. https://developer.apple.com/documentation/realitykit/component ↩︎

  5. https://developer.apple.com/documentation/realitykit/implementing-systems-for-entities-in-a-scene/ ↩︎

Discussion