はじめに
こんにちは!システム開発部のYです。
担当した案件でEntityを常に回転させる処理が必要になりました。
タイマーを使用してEntityを回転させる方法を検討しましたが、複数のシーンで複数のEntityを回転させるケースが多く、コードが冗長になりそうでした。
調べたところ、Systemを使うことで、Entityをフレームごとに更新し、効率的に回転させるロジックを実装できることがわかりました。今回は、この方法についてまとめていきたいと思います。
環境
- Swift Version: 5.9.2
- Xcode: 15.2
- visionOS: 1.0〜1.3
- Reality Composer Pro: 1.0
- macOS: 14.5
Entity Component System (ECS) とは
Entity Component System (ECS) は、データ指向の設計アプローチの一つであり、機能とデータを分離することで効率化を図る仕組みです。
ECSは、エンティティを「Entity(物体)」「Component(要素・属性)」「System(機能・処理)」の3つに分割して設計します。
System
機能やロジックを担う部分であり、シーン内のエンティティの状態を更新するための処理を実行します。
Systemは、複数のエンティティから必要なコンポーネント(属性データ)を取得し、それらに基づいて一括で処理を行います。
この構造により、従来のように各エンティティごとに独立したメソッドを呼び出す必要がなくなり、効率的に多くのエンティティに影響を与える共通のロジックを実装できます。
実装について
固定値の回転
1. プロジェクトの作成
まず、Xcodeを開き、新規プロジェクト作成時に「visionOS」を選択します。

作成したプロジェクトをビルドして実行すると、ウィンドウが表示され、その中央に「Show Immersive Space」というボタンが配置されています。このボタンを押すと、Full Immersive Spaceに入り、2つの球体が表示されます。
2. RotationCustomComponent.swiftの作成
New File → Swift File で『RotationCustomComponent』の新規ファイルを作成します。
ComponentProtocolに準拠させます。
import RealityKit
import SwiftUI
public struct RotationCustomComponent: Component {
public var isOn: Bool = false
public init() {}
}isOnで、そのEntityを回転させるか制御します。
3. RotationSystem.swiftの作成
New File → Swift File で『RotationSystem』の新規ファイルを作成します。
SystemProtocolに準拠させます。
import Foundation
import RealityKit
import RealityKitContent
class RotationSystem: System {
required init(scene: Scene) {
}
func update(context: SceneUpdateContext) {
}
}
RotationCustomComponentを取得して操作するため、RotationCustomComponentを取得するクエリを定義します。
static let query = EntityQuery(where: .has(RotationCustomComponent.self))
回転量を初期化時に、定義させます。
import Foundation
import RealityKit
import RealityKitContent
class RotationSystem: System {
static let query = EntityQuery(where: .has(RotationCustomComponent.self))
private let degrees: Float = 0.04
private let additionalRotation: simd_quatf
required init(scene: Scene) {
let rotationAngle = degrees * .pi / 180
additionalRotation = simd_quatf(angle: rotationAngle, axis: SIMD3<Float>(0, 1, 0))
}
func update(context: SceneUpdateContext) {
}
}updateメソッドでは、毎フレーム処理が呼び出されるため、ここで回転の処理を実行します。
まず、引数であるcontext.sceneからRotationCustomComponentが付いているEntityを取得します。
let entities = context.entities(matching: Self.query, updatingSystemWhen: .rendering)
取得したEntityから対象のコンポーネントを取得し、その中でもisOnがtrueのEntityに絞り込んで処理を行います。
絞り込んだEntityに対して、初期化時に設定した回転角度を加えて回転を適用します。
func update(context: SceneUpdateContext) {
// RotationCustomComponentを付与したEntityに絞る
let entities = context.entities(matching: Self.query, updatingSystemWhen: .rendering)
for entity in entities {
guard let component = entity.components[RotationCustomComponent.self] else { continue }
guard component.isOn else { continue }
let currentRotation = entity.transform.rotation
let newRotation = currentRotation * additionalRotation
entity.transform = Transform(
scale: entity.transform.scale,
rotation: newRotation,
translation: entity.transform.translation
)
}
}以下公式にある通りupdateメソッドは毎フレーム呼び出されるため、不要な作業は行わないでください。
updateメソッドは非常に頻繁に呼び出されるため、メソッド内では不要な作業を行わないでください。
update(context:)メソッドが戻るまでに長い時間がかかる場合、アプリのフレーム レートに悪影響を与える可能性があります。
4. Reality Composer Proを変更
Reality Composer Proを開き、「Immersive.usda」を選択して編集を行います。
『Sphere_Left』と『Sphere_Right』を削除して、新たにEntityを配置します。
今回は、Earthを配置します。

5. ImmersiveView.swiftの変更
プロジェクト作成後のコードは以下になっています。
import SwiftUI
import RealityKit
import RealityKitContent
struct ImmersiveView: View {
var body: some View {
RealityView { content in
// Add the initial RealityKit content
if let immersiveContentEntity = try? await Entity(named: "Immersive", in: realityKitContentBundle) {
content.add(immersiveContentEntity)
// Add an ImageBasedLight for the immersive content
guard let resource = try? await EnvironmentResource(named: "ImageBasedLight") else { return }
let iblComponent = ImageBasedLightComponent(source: .single(resource), intensityExponent: 0.25)
immersiveContentEntity.components.set(iblComponent)
immersiveContentEntity.components.set(ImageBasedLightReceiverComponent(imageBasedLight: immersiveContentEntity))
// Put skybox here. See example in World project available at
// https://developer.apple.com/
}
}
}
}EarthのEntityを取得し、そのEntityに作成したRotationCustomComponentを設定します。
また、RotationCustomComponentのisOnをtrueにします。
import SwiftUI
import RealityKit
import RealityKitContent
struct ImmersiveView: View {
var body: some View {
RealityView { content in
// Add the initial RealityKit content
if let immersiveContentEntity = try? await Entity(named: "Immersive", in: realityKitContentBundle) {
content.add(immersiveContentEntity)
// Add an ImageBasedLight for the immersive content
guard let resource = try? await EnvironmentResource(named: "ImageBasedLight") else { return }
let iblComponent = ImageBasedLightComponent(source: .single(resource), intensityExponent: 0.25)
immersiveContentEntity.components.set(iblComponent)
immersiveContentEntity.components.set(ImageBasedLightReceiverComponent(imageBasedLight: immersiveContentEntity))
// Put skybox here. See example in World project available at
// https://developer.apple.com/
// EarthEntityを取得
if let earthEntity = immersiveContentEntity.findEntity(named: "Earth") {
// RotationCustomComponentを作成
var rotationCustomComponent = RotationCustomComponent()
// isOnをtrueに変更し、回転させる
rotationCustomComponent.isOn = true
// Componentをセット
earthEntity.components[RotationCustomComponent.self] = rotationCustomComponent
}
}
}
}
}6. App.swiftの変更
作成したComponentと、システムを使用できるように登録する必要があります。
App.swiftのinitメソッドを追加し、ComponentはregisterComponentで、SystemはregisterSystemで登録します。
import SwiftUI
import RealityKitContent
@main
struct RotationSystemProjectApp: App {
init() {
// Componentの登録
RotationCustomComponent.registerComponent()
// Systemの登録
RotationSystem.registerSystem()
}
var body: some Scene {
WindowGroup {
ContentView()
}
ImmersiveSpace(id: "ImmersiveSpace") {
ImmersiveView()
}.immersionStyle(selection: .constant(.full), in: .full)
}
}ここまででアプリをビルドして実行し、ImmersiveSpaceを開くと、Earthが表示され回転していることを確認できます。
Componentで定義した値で回転させる
1. RotationCustomComponent.swiftの変更
これまでのisOnに加え、degreesとaxisプロパティも持つようにします。degreesは1フレームでの回転角度を制御し、axisで回転軸を指定します。
import RealityKit
import SwiftUI
public struct RotationCustomComponent: Component {
public var isOn: Bool = false
public var degrees: Float = 0.04
public var axis = SIMD3<Float>(0, 1, 0)
public init() {}
}2. RotationSystem.swiftの変更
回転量を初期化時に定義するのではなく、updateメソッド内で計算するように変更しています。
それ以外の変更はありません。
import Foundation
import RealityKit
import RealityKitContent
class RotationSystem: System {
static let query = EntityQuery(where: .has(RotationCustomComponent.self))
required init(scene: Scene) {
}
func update(context: SceneUpdateContext) {
// RotationCustomComponentを付与したEntityに絞る
let entities = context.entities(matching: Self.query, updatingSystemWhen: .rendering)
for entity in entities {
guard let component = entity.components[RotationCustomComponent.self] else { continue }
guard component.isOn else { continue }
// 各Componentで定義した回転量を計算
let rotationAngle = component.degrees * .pi / 180
let additionalRotation = simd_quatf(angle: rotationAngle, axis: component.axis)
let currentRotation = entity.transform.rotation
let newRotation = currentRotation * additionalRotation
entity.transform = Transform(
scale: entity.transform.scale,
rotation: newRotation,
translation: entity.transform.translation
)
}
}
}3. Reality Composer Proを変更
Reality Composer Proを開き、「Immersive.usda」を選択して編集を行います。
Jupiterと、Moonを配置します。

4. ImmersiveView.swiftの変更
EarthのEntity取得に加え、JupiterとMoonのEntityも取得し、それぞれにRotationCustomComponentをセットします。jupiterEntityにはdegreesを1、axisをSIMD3(1, 0, 0)に設定します。moonEntityにはdegreesを0.01、axisをSIMD3(0, 0, 1)に設定します。
import SwiftUI
import RealityKit
import RealityKitContent
struct ImmersiveView: View {
var body: some View {
RealityView { content in
// Add the initial RealityKit content
if let immersiveContentEntity = try? await Entity(named: "Immersive", in: realityKitContentBundle) {
content.add(immersiveContentEntity)
// Add an ImageBasedLight for the immersive content
guard let resource = try? await EnvironmentResource(named: "ImageBasedLight") else { return }
let iblComponent = ImageBasedLightComponent(source: .single(resource), intensityExponent: 0.25)
immersiveContentEntity.components.set(iblComponent)
immersiveContentEntity.components.set(ImageBasedLightReceiverComponent(imageBasedLight: immersiveContentEntity))
// Put skybox here. See example in World project available at
// https://developer.apple.com/
// Earthを取得してくる
if let earthEntity = immersiveContentEntity.findEntity(named: "Earth") {
// RotationCustomComponentを作成
var rotationCustomComponent = RotationCustomComponent()
// isOnをtrueに変更し、回転させる
rotationCustomComponent.isOn = true
// Componentをセット
earthEntity.components[RotationCustomComponent.self] = rotationCustomComponent
}
// Jupiterを取得してくる
if let jupiterEntity = immersiveContentEntity.findEntity(named: "Jupiter") {
// RotationCustomComponentを作成
var rotationCustomComponent = RotationCustomComponent()
// isOnをtrueに変更し、回転させる
rotationCustomComponent.isOn = true
// 1フレームで回転する角度の変更
rotationCustomComponent.degrees = 1
// 回転軸を変更
rotationCustomComponent.axis = SIMD3<Float>(1, 0, 0)
// Componentをセット
jupiterEntity.components[RotationCustomComponent.self] = rotationCustomComponent
}
// Moonを取得してくる
if let moonEntity = immersiveContentEntity.findEntity(named: "Moon") {
// RotationCustomComponentを作成
var rotationCustomComponent = RotationCustomComponent()
// isOnをtrueに変更し、回転させる
rotationCustomComponent.isOn = true
// 1フレームで回転する角度の変更
rotationCustomComponent.degrees = 0.01
// 回転軸を変更
rotationCustomComponent.axis = SIMD3<Float>(0, 0, 1)
// Componentをセット
moonEntity.components[RotationCustomComponent.self] = rotationCustomComponent
}
}
}
}
}ここまででアプリをビルドして実行し、ImmersiveSpaceを開くと、Earth、Jupiter、Moonがそれぞれ異なる軸で回転していることを確認できます。
まとめ
Systemを使って回転し続けるEntityの作成方法についてまとめました。
ECSは、iOSアプリなどでは馴染みが薄く少し難しい概念ですが、visionProの開発においては強力で必要不可欠な機能だと感じました。
この記事が皆様のお役に立てれば幸いです。また、もし誤りや改善点がありましたら、ぜひ教えていただけると幸いです。
参考
- https://developer.apple.com/documentation/visionos/understanding-the-realitykit-modular-architecture
- https://developer.apple.com/documentation/realitykit/implementing-systems-for-entities-in-a-scene








