【Swift】visionOSで鬼をカメラの方向へ動かす
はじめに
前回は【USDZ】AIを使ってモデリングするで鬼を作成しました。
今回は、作成した鬼をvisionOSにインポートし、カメラの方向へ動かすようにしたいと思います。
その中でRealityKitの「ECS」[1]という概念について学びました。
つくったもの
環境
- Xcode Version 15.3
- visionOS 1.1
前提条件
- アプリは
FullSpace
に入る必要があります。 - 一部の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が作成されます。
let oni = try? Entity.load(named: "oni")
簡単に表示できましたね!
さあ、ここから鬼を動かしていきましょう!
Componentとは
Component[4]とは動作や外観を表現するためのオブジェクトです。Entityのインスタンスにコンポーネントを追加することで、特定の要素を組み立てます。
例えば、ModelComponentは視覚的な外観を提供し、TransformComponentは空間内の位置を定義します。
鬼にコンポーネントを追加する
鬼にコンポーネントを追加していきます。アニメーションは作成したUSDZに組み込まれているので、availableAnimations
でアニメーションが再生できることをチェックしてから属性やコンポーネントを追加しています。
ここでは衝突判定に必要なCollisionComponent
と、鬼をカメラの方向へ動かすのに必要なMovingComponent
というカスタムコンポーネントを追加しました。
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
では動作速度を定義しました。
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
を使って、鬼がカメラの方向へ移動する機能を実装します。
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:)
メソッドを用います。
func update(context: SceneUpdateContext) {
// この中に書いていく
}
次にSceneからEntityを効率的に取得するため、EntityQuery
を使用します。これを使用すると、すべてのEntity、またはシステムに関連するEntityのサブセットを取得することができます。
update
メソッドのcontext.scene
からMovingComponent
がついているEntityを取得します。
let entities = context.scene.performQuery(EntityQuery(where: .has(MovingComponent.self)))
そして取得したentitiesに対して処理を実行していきます。
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のイニシャライザでregisterComponent
とregisterSystem
を実行しましょう。
RealityKitがアクティブなシーンごとにインスタンスを自動的に作成し、フレームごとに update(context:)
メソッドを繰り返し呼び出してくれます。
init() {
MovingComponent.registerComponent()
MovingSystem.registerSystem()
}
カメラの位置を取得する
カメラの位置はARKitSession
とWorldTrackingProvider
から取得します。
MovingSystem
のイニシャライザでARKitSessionを実行し、update
メソッドでDeviceAnchor
を取得しました。
required init(scene: Scene) {
try await arkitSession.run([worldTrackingProvider])
}
update
メソッドにcamaraPosition
を取得する処理を追加します。
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について発信していきますので、この記事が参考になったと思ったらぜひ♡をお願いします。
追記(2024/05/27)
今回の記事で紹介している空間シューティングゲーム「妖怪バスターZ」を日本発売前の2024年5月3日にリリースすることができました!
ぜひインストールして遊んでみてください。
これからもvisionOSを学びながらこちらのアプリをアップデートしていくのでフィードバックもお待ちしております。
参考
-
https://developer.apple.com/documentation/realitykit/realitykit-systems ↩︎
-
https://developer.apple.com/documentation/realitykit/entity/ ↩︎
-
https://developer.apple.com/documentation/realitykit/loading-entities-from-a-file#Load-an-Entity-Hierarchy-Synchronously ↩︎
-
https://developer.apple.com/documentation/realitykit/component ↩︎
-
https://developer.apple.com/documentation/realitykit/implementing-systems-for-entities-in-a-scene/ ↩︎
Discussion