View in English

  • メニューを開く メニューを閉じる
  • Apple Developer
検索
検索を終了
  • Apple Developer
  • ニュース
  • 見つける
  • デザイン
  • 開発
  • 配信
  • サポート
  • アカウント
次の内容に検索結果を絞り込む

クイックリンク

5 クイックリンク

ビデオ

メニューを開く メニューを閉じる
  • コレクション
  • トピック
  • すべてのビデオ
  • 利用方法

WWDC24に戻る

ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。

  • 概要
  • トランスクリプト
  • コード
  • RealityKitデバッガの詳細

    RealityKitデバッガについて説明します。この新しいツールを使用して、空間アプリのエンティティ階層を検査する方法、不適切な変換をデバッグし、欠落したエンティティを特定する方法、コードのどの部分が原因でシステムに問題が生じているかを突き止める方法を確認しましょう。

    関連する章

    • 0:00 - Introduction
    • 0:23 - Agenda
    • 0:53 - Prepare for the Journey
    • 1:41 - Meet the RealityKit debugger
    • 2:40 - Transform the BOTanist
    • 4:02 - Traverse hierarchy issues
    • 7:20 - Address bad behaviors
    • 10:52 - Find what's missing
    • 18:44 - Embrace uniqueness
    • 22:34 - Wrap up

    リソース

    • Forum: Developer Tools & Services
      • HDビデオ
      • SDビデオ

    関連ビデオ

    WWDC24

    • iOS、macOS、visionOS向けRealityKit APIの紹介
  • このビデオを検索

    こんにちは Jeremiahです 空間アプリやゲームの作成を支援する デベロッパ向けツールを作っています 今回は RealityKitアプリに潜む 一般的なバグについてお話しします その中で RealityKitデバッガを紹介します バグの発見に役立つ新しいツールです まずRealityKitデバッガを 簡単に見ていきます そして このデバッガを使い アプリを調べてバグを見つけ出します エンティティ階層をたどり 予期しない変換を探します 様々なコンポーネントの誤りを明らかにして システムの不正な挙動に対処します レンダリングで陥りやすい問題に取り組み 足りないコンテンツを見つけます 最後に アプリの独自性に合わせて RealityKitデバッガを使うための ヒントとコツを紹介します 準備はいいですか? では始めましょう RealityKitを使えば 驚くような3Dアプリを作成し iOS macOS visionOSに デプロイできます その一例とも言えるのが BOT-anistサンプルという 植物の世話をするかわいいロボットです でも 大きな荷物を背負って 一日中駆け回るのは 大変な仕事です 時には ゆっくりとリラックスできる場所が ロボットにも必要です 友達と会ったり 高級オイルを楽しんだり ロボットだけにダンスをしたり とにかくストレスを発散できる空間です そこで このセッションでは チルアウトモードを BOT-anistサンプルに追加します 庭をクラブに変える プロトタイプを今作っています しかし たくさんのバグが あちこちに残っているので まだオープンできる状態ではありません RealityKitデバッガを使って バグを突き止めましょう

    RealityKitデバッガは実行中の アプリの3Dスナップショットを取り Xcodeに読み込んで 調査できるようにします 画面の下部にあるデバッグ領域で ボタンを クリックして開始します

    スナップショットの処理が完了すると キャプチャされたRealityKitシーンが 左側のデバッグナビゲータに表示されます

    シーンを選択すると 隣のアウトラインビューに そのエンティティ階層が表示されます

    また 3Dビューポートで コンテンツが再構築されます

    階層またはビューポートで エンティティを選択すると そのプロパティと 要素のプロパティが 右側のインスペクタに表示されます

    現在選択されている階層の 統計情報を表示する インスペクタもあります RealityKitデバッガは 既存のXcodeワークフローになじみ 3D開発体験を より生産的で楽しいものにする上で 役立つ新たな情報を提供します 新しいツールを手に入れたので クラブの手直しを始めましょう

    このサンプルを変えるための コードパッチはかなり大掛かりです 確かめながら進めるには ClubView.swiftファイルをダウンロードし Xcodeプロジェクトにドラッグして ターゲットに含めてください その後 小さな変更を2つ加える必要があります 1つ目に クラブの 新しいボリュメトリックシーンを定義します そして これを BOTanistAppのbodyに追加します

    2つ目に クラブを開くための ボタンが必要です これをRobotViewのbodyに追加します 既存のボタンの 「Start Planting」の横です これで アプリをビルドして visionOSシミュレータで実行できます

    アプリが起動すると ロボットビューでアプリが開きます ロボットを作るのではなく ミラーボールを クリックしてクラブに忍び込みます

    カメラ制御を使って近づくことができます

    シーンの既存のアセットに 色々と手を加えています 例えば プランターをテレポーターに変えています ミラーボールなど ゼロから作成した 新しいエンティティもあります 今は 少し変な感じに見えますね シーンを調べて原因を探ってみましょう

    Xcodeでデバッグ領域のボタンを使って RealityKitデバッガを起動します

    本題に入る前に 変換の階層がどう機能するか 少し考えてみましょう 3Dシーンにコンテンツを配置する時は 位置 向き スケールを設定します よくあるバグの1つは 設定した場所に コンテンツが表示されないことです 一般にこの問題が発生する理由は エンティティの最終的な配置が それ自体の変換とすべての祖先の変換の 組み合わせであるからです そのため 1つのエンティティにのみ 適用すべき変換が 別のエンティティにも 現れることがよくあります このミラーボールでも 同じようなことが起こっていそうです RealityKitデバッガを使って 確認してみましょう

    デバッグナビゲータで シーンラッパーを展開し RealityKitコンテンツを選択します そのシーンがデバッガで開きます

    この時点で ナビゲータとデバッグ領域を 非表示にして 作業スペースを広げることができます

    ビューポートでミラーボールを ダブルクリックして選択し中央に配置します

    メインビューポートには シーンに現れるエンティティが 祖先からの変換が 適用された状態で表示されます ビューポートでエンティティを選択すると エンティティ階層とインスペクタでも そのエンティティが選択されます 現在選択されている エンティティの名前はOutlineです このエンティティは ミラーボールの線を表示します エンティティインスペクタには 小さい副ビューポートがあります このビューポートで 変換が適用されていない エンティティの ModelComponentをプレビューできます プレビューでは Outlineエンティティは歪んでいないので 問題の原因はメッシュではありません また プレビューウィンドウの 下に表示されている このエンティティのTransform要素を見ると スケールは1に揃っています つまり エンティティが 自らを歪めているのではありません どうやら この問題は 祖先から引き継がれたようです 階層をたどって 問題のある変換を見つけましょう エンティティ階層で 親の Backgroundエンティティをクリックします インスペクタのプレビュービューポートと Transform要素を見ると このエンティティも 歪みの原因ではありません 階層でその親の Supportエンティティを クリックしてみましょう

    SupportエンティティのTransform要素を インスペクタで見ると Y軸のスケールが 大きくなっていることがわかります このエンティティを目的の形状にするために 拡大縮小すること自体は間違っていません ここでの誤りはこの拡大縮小を そのすべての子孫に 意図せず適用していることです

    SupportとBackgroundを 親子ではなく 兄弟にすると問題は解決します

    シーンではつながったままに見えますが Supportの変換は ミラーボールに影響しなくなります アプリをもう一度実行し クラブ内に移動して 結果を確認しましょう

    RealityKitデバッガを使って 階層をたどり エンティティを歪めていた不適切な変換を 見つけることができました そして親を変更して修正できました これでミラーボールができました 改装中のクラブは 体裁が整いつつあります 次はこれらのオブジェクトに 命を吹き込みましょう RealityKitではエンティティ コンポーネントシステム(ECS)を使って オブジェクトと動作を管理します エンティティを特徴づけるには データを保持できる要素を エンティティに割り当てます その後 特定の要素を持つ エンティティに対して システムで更新を実行します エンティティの要素がなかったり 正しく設定されていないと システムの動作は予測不能になります クラブに戻って例を見てみましょう

    すべてのプランターを クラブのテレポーターに変えました テレポートシステムで ロボットが出てくるはずですが 誰も現れません デバッガを開いて原因を調べてみましょう

    まずテレポートシステムの 仕組みについて説明します

    コントロールセンター要素に システムのデータが保存されます 更新のたびに カウントダウンの値が減っていきます カウントダウンの値が0になると テレポーター要素を使って シーンのエンティティがすべて特定され ランダムに1つ選択されます その位置にロボットが出てきます カウンタがリセットされ クラブが 満員になるまでこの処理が繰り返されます デバッガに切り替えて これらの要素を調べてみましょう

    まず疑わしい点はテレポーター要素を テレポーターエンティティに 追加し忘れたことです その場合は テレポーターが見つからなくなり ロボットが出てくる場所がなくなります RealityKitデバッガを使えば 簡単に確認できます エンティティ階層で BOT Clubと Teleportation Centerを順に展開し 最初のテレポーターをダブルクリックします

    エンティティインスペクタで テレポーター要素が 実際にあることを確認します 他の2つのテレポーターも 念のため確認してみましょう

    やはり テレポーター要素があります となると問題は コントロールセンターかもしれません 階層で親エンティティの Teleportation Centerを選択します

    コントロールセンター要素の プロパティを見てみましょう

    思い当たるのはカウントダウン値です 初期値と同じであることがわかります RealityKitデバッガで捕捉されるのは 一時停止した瞬間のアプリの状態なので システムが動作していれば この値は変更されているはずです コントロールセンター要素が 何らかの理由で更新されていないのです コードを調べて原因を突き止めましょう

    更新のたびに コントロールセンター要素で カウントダウン値が減るようにしています その後 更新した要素を エンティティに保存しないといけませんが その手順を忘れたようです 足りない手順を追加して アプリを再実行しましょう これはよくある間違いです 変更した要素は そのエンティティに代入し直す必要があります シミュレータに切り替えて 問題が解決したか確認してみましょう

    RealityKitデバッガを使って うまく動作していないシステムを 調査して修正しました では クラブに戻って お客さんを待ちましょう 早くお客さんが来るといいのですが 何しろここの家賃は 超高額ですから よかった 最初のお客さんがやっと来ました

    ロボットがテレポートしてくるように なりましたが まだ問題があります カウンターの上に オイルのボトルがあるはずですが 見当たりません ちゃんと補充したはずなのに 早く見つけないと ロボットたちが踊るのをやめ この場所は軋んで止まってしまいます レンダラにより 非表示になっているように思います 説明しましょう RealityKitのような3Dレンダラでは 一定のパフォーマンスを得るために レンダリングする対象を選んで 時間を節約します 例えば 遠くにあるものや 近すぎるもの 他のコンテンツに隠れているもの 不透明度の設定が低すぎるもの ARKitアンカーが必要なもの アセットが不足しているもの こうした問題がある場合 そのコンテンツはレンダリングされません そして理由の分析は たいてい消去法になります

    Reality Composer Proを使って アセットを準備 テスト パッケージ化すると このような問題を回避できます それでもコンテンツが失われる場合は RealityKitデバッガで原因を特定できます では デバッガに切り替えて ボトルが表示されない 今回のケースを解決しましょう

    ビューポートで カウンターをダブルクリックし カメラを調整して うまくクローズアップします

    よさそうです カウンターには最高級のオイルが入った 緑色のボトルが9本あるはずです しかし 今は1本しかなく それも正しくレンダリングされていません 原因を調べましょう エンティティ階層で CounterとBottle Groupを順に展開し 1本目のボトルを選択します

    選択したエンティティは それ自体が他のエンティティに隠れていても ハイライト表示されます この場合 目的のボトルがあるのを確認できますが カウンターの下にあります これをインスペクタで確認し ボトルのTransform要素を見てみると Y方向が負の値になっています この修正は簡単なので 後で行うことにします とりあえず先に進んで 次の問題を調べましょう

    階層でボトル2を選択します

    選択した要素が ビューでハイライトされていません 階層でエンティティを ダブルクリックすると それにカメラがフォーカスします

    かなり遠くにありますね とても離れていて 黄色い枠で示された シーンの境界を越えています このため レンダラでクリッピングされ 表示されることはありません 1本目のボトルと同様に この問題を解決するには変換を修正します

    次に進みましょう 階層で3本目のボトルを ダブルクリックして選択しフォーカスします

    今度は とても大きいボトルで その中にシーンがあるようです メッシュを構成する三角形は 通常は片側しか見えません そのため メッシュの内側からは まったく見えないことがよくあります このオブジェクトは 小さくすれば問題が解決します 今はカウンターからかなり離れているので 階層でボトル4を ダブルクリックして戻りましょう

    ご覧のように 階層で ボトル4の横にアイコンがあります このアイコンはエンティティが アクティブでないことを示しています アクティブでないエンティティは レンダリングされません 要素を調べると 問題を特定するのに役立ちます

    今までのボトルとは違い このボトルにはOutOfStock要素があります 在庫切れの商品については この要素でタグを付けて非表示にしています つまり レンダリングされないのは 意図した通りの動作です

    ボトル5に進みましょう

    このボトルのインスペクタで 別の予期せぬ要素が見つかりました Anchoring要素です クラブでディナーサービスを 追加するつもりだったのですが これは その初期プロトタイプの レガシーコードです その機能を削除した時に この要素を削除するのを 忘れてしまったようです 一致するARKitアンカーがシーンにない Anchoring要素を持つ エンティティについては レンダリングが停止します

    ボトル6に進みましょう

    ビューポートに選択対象の輪郭はなく 軸だけ表示されています これはエンティティに ModelComponentがないことを意味し インスペクタでもそれを確認できます 読み込みに失敗したか 間違ったエンティティに関連づけたようです ここでは判断できないので 後でコードを確認する必要があります ただし 何が問題で どこに注目すべきかはわかっています

    ボトル7に進みます

    このボトルはメインビューポートにも プレビュービューポートにも見当たりません つまり ModelComponentの 問題と考えられます メインビューポートに表示された 輪郭は正しいようです このことからメッシュは正常であり 問​​題はマテリアルにあると考えられます ModelComponentでマテリアルの プロパティを展開して調べてみましょう

    このマテリアルは 半透明に設定されていますが 同時に不透明度のしきい値が1です これは設定ミスであり 実質的に 不透明度が1未満の モデルのどの部分も レンダリングしないように指定しながら モデルのすべての部分の 不透明度を1未満に設定しています 結果的にボトル全体が見えなくなっています

    次のボトル8は実際に表示されています ただ不完全です インスペクタでは 明らかな間違いはないようです このような状況では RealityKitデバッガで 追加の表示を使うと 他の方法では見逃す可能性がある 問題を特定しやすくなります

    プレビュービューポートで 右端のドロップダウンを使って レンダリングモードを変更します 一番上の 法線を表示するオプションを選択します 各点の法線値を使って オブジェクトが色付けされます 法線値はサーフェスの向きを示し 照明とレンダリングの計算に使用されます 最初は難しく感じるかもしれませんが ボトル1のような 正常とわかっているボトルに切り替えると

    メッシュに何か問題があることがわかります

    こうしたエラーは インポートしたアセットでよく見られ 3Dコンテンツ作成ツールで 修正する必要があります

    では最後の1つです ボトル9を選択します ボトル9が見当たりませんね シーンに追加するのを忘れたのでしょう これを確認するために 階層ビューの下部にある フィルタバーを使用して 名前にBTが含まれる エンティティのみを表示します

    予想通り シーンにありません

    多くの問題を駆け足で見てきたので 簡単にまとめましょう 見当たらなかった最初の数本のボトルは 変換が原因で隠れていたり クリッピングされていたり内と外が逆でした 無効になっていたために 非表示だったボトルもあり アンカーがなかったために 非表示だったボトルもありました メッシュに問題があったボトルもあり メッシュがないものもあり マテリアルの設定ミスによって 見えなくなったボトルもありました そして単純に シーンに追加し忘れたボトルもありました コピー&ペーストによるエラーが ボトル作成コードに多数含まれていました

    そこで このコードを 1つの作成ループに置き換えるつもりです 3Dアセットをコード内で 直接配置して設定するのは難しいので できるだけ Reality Composer Proで シーンのレイアウトを 準備することをお勧めします アプリを再実行して クラブ内に移動しましょう

    デバッガの様々なツールを使って 見当たらないボトルを発見できました カウンターには今や オイルがいっぱいあります これでスムーズに進められます

    問題のある変換や 誤って設定された要素 レンダリングの落とし穴など 独自のアプリを構築する時に遭遇しやすい 多くの問題についてすでに説明しました とはいえ アプリの多くの部分は独自のものです そして複雑性が増すにつれ デバッグする際の課題も増えていきます しかし ECSの柔軟性を活用して RealityKitデバッガの 使い方をカスタマイズできます どのようなものか見てみましょう こちらはダンスシステムです ダンスフロアの周りに配置された 目に見えない 一連のアトラクターエンティティを 通じて機能します Newcomerのロボットが クラブにテレポートしてくると 空のアトラクターのターゲットになります そして更新のたびに アトラクターに徐々に引き寄せられます ロボットがアトラクターに到達すると モチベーターが作動し ロボットがダンスを始めます しかし 何かおかしいと お気づきかもしれません ロボットがテレポートしてきても その場に立っているだけで アトラクターの方へ移動していません RealityKitデバッグ機能を このシステムに組み込みましょう

    まず シーンに 基本的なモデルエンティティを追加して 見えないアトラクターを視覚化します アトラクター状態など インスペクタに渡す値を格納する カスタム要素を それぞれに指定します そしてこれらを 1つの見えないエンティティにグループ化し 実行中に何も表示されないようにします この親エンティティにも カスタム要素を指定して システム全体に関する情報を 表示するようにします

    こちらは簡略版のコードです ClubView.swiftファイルには 完全版がすでに含まれています 大掛かりに見えますが 標準的なRealityKitしか使っていません エンティティ 要素 システムです 奇妙に思われるかもしれませんが デバッグコンパイルブロック内に これらすべてが配置されています こうすると リリースされるアプリから このコードがコンパイルアウトされるので パフォーマンスへの影響を 心配する必要がなくなります 新しいデバッグシステムを 用意したのでアプリを実行し ロボットが現れるまで待ってから デバッガを作動させましょう

    「 Dance System」を 階層で見つけて選択します ご覧のように インスペクタに デバッグ要素のプロパティが表示されます RealityKitデバッガでは アプリで使うことが多い ほとんどの型を表示できます これは カウンタの表示など 簡単な方法で使えるだけでなく UIImageプロパティとして保存して Swiftのグラフを表示するなど 創造的な方法でも利用できます この新しいデバッグ要素を活用して ダンスシステムの問題を特定できます すべてのアトラクターが 引き込み中の状態になっていますが これはあり得ないことです これを観察するもう1つの方法は 視覚化を使うことです 階層内の「 Dance System」を 副ボタンでクリックして コンテキストメニューを開き 表示と非表示を切り替えます

    これにより 各アトラクターの状態が視覚化されます 実際にすべてオレンジ色です この色は引き込み中の状態を示しています 同じロボットを複数のアトラクターで ターゲットにすることはできません テレポートしてきたロボットは Newcomer要素でタグ付けされ 1つのアトラクターのターゲットになると そのタグが削除されます デバッグの視覚化を1つ選択して そのデバッグ要素を調べてみましょう

    この要素はターゲットロボットへの 参照を格納するように設定されていて RealityKitデバッガにより その値がリンクに変換されます クリックしてターゲットを見つけましょう

    ロボットの要素を調べると 問題が明らかになります Newcomer要素がまだありますが 最初にターゲットになった時に 削除されるべきだった要素です 削除されなかったので 全部のアトラクターがこのロボットを見つけ 引き込もうとしています そのため ロボットは 行き先を選べず動けなくなっています 問題を特定したので コードを確認して修正できます

    ダンスシステムではターゲットの設定時に Newcomer要素を削除する必要がありますが そうしていません そのためのコードを追加しましょう その後 アプリを再実行します

    この種のバグは簡単に修正できますが 追跡が難しくなる場合もあります システムの複雑さが増したり アプリの規模が大きくなると特にそうです しかし 同じシステムを活用して視覚化を構築し カスタム要素を使って インスペクタを追加すれば 開発体験は プレイヤーや ロボットのために構築する体験と同じくらい 楽しいものになります それでは いよいよクラブをオープンし 成功を満喫しましょう

    RealityKitデバッガを利用して エンティティ階層や要素の問題を追跡し 修正することができました また ECSの柔軟性を活用して 視覚化と カスタムインスペクタを追加しました これにより アプリの 独自の部分でも適切にデバッグできます このセッションでは 多くのことを取り上げました では ロボットの友達と同じように 私もリラックスしに行ってきます

    • 2:45 - ClubView

      /*
      Abstract:
      The full club patch. SwiftUI view, state, extensions and helpers.
      */
      
      import SwiftUI
      import RealityKit
      import OSLog
      import BOTanistAssets
      import Combine
      import Charts
      
      struct ClubView: View {
          @State var state = ClubViewState()
          
          var body: some View {
              ZStack {
                  RealityView { content in
                      state.loadEnvironment()
                      
                      state.rootEntity.scale = SIMD3<Float>(repeating: 0.5)
                      
                      content.add(state.rootEntity)
                  } update: { updateContent in
                      if !state.doorSupervisor.doorsOpen {
                          state.transformIntoClub(content: updateContent)
                      }
                  }
              }
          }
      }
      
      @Observable
      @MainActor
      final public class ClubViewState: Sendable {
          let rootEntity = Entity()
          
          private var loadedEnvironmentRoot: Entity?
          private var robotRevolutionController: Entity?
          private var host: Entity?
          
          private(set) var doorSupervisor: DoorSupervisor {
              get {
                  rootEntity.components[DoorSupervisor.self]!
              } set {
                  rootEntity.components[DoorSupervisor.self] = newValue
              }
          }
          
          init() {
              RevolvingSystem.registerSystem()
              HoverSystem.registerSystem()
              TeleportationSystem.registerSystem()
              DanceMotivationSystem.registerSystem()
              
              rootEntity.name = "The B0T Club"
              rootEntity.components[DoorSupervisor.self] = DoorSupervisor(capacity: 9)
          }
          
          /// Load the existing garden assets
          func loadEnvironment() {
              guard loadedEnvironmentRoot == nil else {
                  return
              }
              
              if let environment = try? Entity.load(named: "scenes/volume", in: BOTanistAssetsBundle) {
                  environment.name = "Environment"
                  self.loadedEnvironmentRoot = environment
                  
                  rootEntity.addChild(environment)
              }
          }
          
          /// Renovate the loaded environment to build our club
          func transformIntoClub(content: RealityViewContent) {
              guard !doorSupervisor.doorsOpen else {
                  return
              }
              
              // Build a teleportation center and use it to spawn robots
              addTeleportationCenterToTheClub()
              
              // Haphazardly clean up the space by hiding anything un-club-like
              hideStuffInTheEnvironment()
              
              // Polish that floor and add some spin
              addRevolvingDanceFloorToTheClub()
              
              // Keep the robots moving in an orderly fashion
              addRobotRevolutionControllerToTheClub()
              
              // Install some attractors to entice robots to the dance floor
              addDanceFloorAttractors()
              
              // Set the mood
              addSpotlightsToTheClub()
              
              // Stock up on oil to keep the moves smooth
              addCounterToTheClub()
              
              // And add a huge Disco Ball, because...
              addDiscoBallToTheClub()
              
              // Let the party begin
              openDoors()
          }
          
          /// Construct a Teleportation Center and add it to the Club's root entity
          private func addTeleportationCenterToTheClub() {
              let teleportationCenter = Entity()
              teleportationCenter.name = "Teleportation Center"
              rootEntity.addChild(teleportationCenter)
              
              // Liven up the planters to look more like teleporters
              let positions: [SIMD3<Float>] = [[0.128, 0, 0.14], [-0.255, 0, 0.23], [0.05, 0, -0.17]]
              let colors: [(UIColor, UIColor)] = [(.green, .yellow), (.magenta, .purple), (.cyan, .blue)]
              for index in 0...2 {
                  if let teleporter = rejigPlanter(identifier: String(index + 1), position: positions[index], colors: colors[index]) {
                      teleportationCenter.addChild(teleporter)
                  }
              }
              
              // Create a Control Center and provide a closure to handle robot spawning
              let teleportationControlCenter = ControlCenterComponent(
                  initialValue: 10,
                  interval: 5,
                  rootEntity: rootEntity) { teleporter in
                      self.spawnRobot(from: teleporter)
                      self.countVisitor()
                      
                      // Have the host say hello
                      if let hostCharacter = self.host?.components[AutomatonControl.self]?.character {
                          hostCharacter.transitionToAndPlayAnimation(.idle)
                          hostCharacter.transitionToAndPlayAnimation(.wave)
                      }
              }
              
              // Assign the new control center component to the teleportation center entity
              teleportationCenter.components[ControlCenterComponent.self] = teleportationControlCenter
          }
          
          /// Transforms the visuals of the planters to look more teleporter-y
          private func rejigPlanter(identifier: String, position: SIMD3<Float>, colors: (UIColor, UIColor)) -> Entity? {
              if let rim = rootEntity.findEntity(named: "heroPlanter_rim_\(identifier)"),
                 let dirt = rootEntity.findEntity(named: "dirt_hero_\(identifier)"),
                 let rimModelComponent = rim.components[ModelComponent.self],
                 var dirtModelComponent = dirt.components[ModelComponent.self] {
                  // Apply the luminous material from the rims to the dirt (trust me it will look cool).
                  dirtModelComponent.materials = rimModelComponent.materials
                  dirt.components[OpacityComponent.self] = OpacityComponent(opacity: 0.7)
                  dirt.components[ModelComponent.self] = dirtModelComponent
              }
              
              // Make a teleporter container entity
              let teleporter = Entity()
              teleporter.name = "Teleporter-T\(identifier)"
              teleporter.position = position
              teleporter.components[TeleporterComponent.self] = TeleporterComponent()
              
              // Add a particle emitter
              let radius: Float = 0.035
              var particleEmitter = ParticleEmitterComponent.Presets.teleporter
              particleEmitter.emitterShapeSize = .init(repeating: radius)
              particleEmitter.mainEmitter.color = .constant(.random(a: colors.0, b: colors.1))
              
              let particleEntity = Entity()
              particleEntity.orientation = .init(angle: -.pi / 2, axis: [1, 0, 0])
              particleEntity.components[ParticleEmitterComponent.self] = particleEmitter
              particleEntity.name = "Photons"
              particleEntity.scale = .init(repeating: 1)
              teleporter.addChild(particleEntity)
              
      #if DEBUG
              // Add a debug marker in case we want to visually inspect this in the RealityKit Debugger
              teleporter.addDebugMarker(radius: radius, color: colors.0)
      #endif
              
              return teleporter
          }
          
          /// adds a random robot to the club root, positioned at the provided point
          private func spawnRobot(from spawnPoint: Entity) {
              guard let robotCharacter = randomRobot() else {
                  logger.error("Robot creation malfunction 🤖💥")
                  return
              }
              
              let guest = Entity()
              
              guest.addChild(robotCharacter.characterParent)
              guest.position = spawnPoint.position(relativeTo: rootEntity)
              guest.components[Newcomer.self] = Newcomer()
              guest.components[AutomatonControl.self] = AutomatonControl(character: robotCharacter)
              
              rootEntity.addChild(guest)
              
              // Play a little flashy burst on the particle emitter
              if let particles = spawnPoint.findEntity(named: "Photons") {
                  var component = particles.components[ParticleEmitterComponent.self]
                  component?.burst()
                  particles.components[ParticleEmitterComponent.self] = component
              }
          }
          
          /// misuses AppState as a robot factory - don't try this at home, or do, but don't ship it!
          private func randomRobot() -> RobotCharacter? {
              let robotMaker = AppState()
              
              // Use offsets from the loaded animation rig, with some random parts
              guard let skeleton = robotMaker.robotData.meshes[.body]?.findEntity(named: "rig_grp") as? ModelEntity else {
                  logger.error("Failed to find a robot animation rig... all dancing in cancelled ❌🕺")
                  return nil
              }
              
              robotMaker.randomizeSelectedRobot()
              
              guard let head = robotMaker.robotData.meshes[.head]?.clone(recursive: true),
                    let body = robotMaker.robotData.meshes[.body]?.clone(recursive: true),
                    let backpack = robotMaker.robotData.meshes[.backpack]?.clone(recursive: true) else {
                  fatalError()
              }
              
              let robotCharacter = RobotCharacter(
                  head: head,
                  body: body,
                  backpack: backpack,
                  appState: robotMaker,
                  headOffset: skeleton.pins["head"]?.position,
                  backpackOffset: skeleton.pins["backpack"]?.position
              )
              
              // Pick a random robot name from the sequence
              robotCharacter.characterParent.name = RobotNames.next
              
              // Remove the character controller and animation state, as we'll manually control these
              robotCharacter.characterParent.components[CharacterControllerComponent.self] = nil
              AnimationState.handlers.removeAll()
              
              // The robots are here to chill, so actually, let's put their backpacks in the cloakroom
              backpack.removeFromParent()
              
              // Say Hi
              robotCharacter.transitionToAndPlayAnimation(.wave)
              
              return robotCharacter
          }
          
          /// Update capacity when we have a visitor
          private func countVisitor() {
              var management = self.doorSupervisor
              management.visitorCount += 1
              self.doorSupervisor = management
          }
          
          /// Find and hide a bunch of stuff in the loaded environment
          private func hideStuffInTheEnvironment() {
              // We used the RealityKit Debugger to identify the names of things we want to hide in the club
              ["setDressing", "MovementBoundaries", "planter_side", "planter_Hero", "planter_Hero_1", "planter_Hero_2", "PlantLightGroup",
               "PlantLightGroup_1", "PlantLightGroup_2", "SidePlanterLights", "pipe_2", "pipe_3", "dirt_coffeeBerry_1", "dirt_coffeeBerry_2",
               "dirt_coffeeBerry_3", "dirt_side"].forEach { name in
                  if let entity = rootEntity.findEntity(named: name) {
                      entity.removeFromParent()
                  }
              }
          }
          
          /// Repurpose some existing bits in the environment to create a makeshift revolving dance floor - if it looks like dirt, that's because it is
          private func addRevolvingDanceFloorToTheClub() {
              guard let dirtFloor = loadedEnvironmentRoot?.findEntity(named: "dirt_end") else {
                  return
              }
              
              // Add a revolving container entity
              let revolvingDanceFloor = Entity()
              revolvingDanceFloor.name = "Revolving Dance Floor"
              revolvingDanceFloor.scale = [1, 1, 1]
              revolvingDanceFloor.position = [0, 0.181, 0]
              revolvingDanceFloor.components[RevolvingComponent.self] = RevolvingComponent(relativeTo: rootEntity)
              
              // Polish up the dirt floor
              let geometry = dirtFloor.clone(recursive: false)
              geometry.name = "Dirt Floor"
              geometry.transform = .identity
              geometry.position = [0, 0, 0]
              geometry.scale = dirtFloor.scale(relativeTo: rootEntity)
              
              let polish = geometry.clone(recursive: false)
              polish.name = "Polish Layer"
              polish.position = [0, 0.0004, 0]
              
              if var modelComponent = geometry.components[ModelComponent.self] {
                  var polishedFloorMaterial = PhysicallyBasedMaterial()
                  
                  polishedFloorMaterial.baseColor = .init(tint: .gray)
                  polishedFloorMaterial.roughness = .init(floatLiteral: 0.2)
                  polishedFloorMaterial.metallic = .init(floatLiteral: 0.8)
                  polishedFloorMaterial.blending = .transparent(opacity: .init(floatLiteral: 0.5))
                  polishedFloorMaterial.clearcoat = .init(floatLiteral: 0.4)
                  
                  modelComponent.materials = [polishedFloorMaterial]
                  
                  polish.components[ModelComponent.self] = modelComponent
              }
              
              // Add it to the revolving container
              revolvingDanceFloor.addChild(geometry)
              revolvingDanceFloor.addChild(polish)
              
              rootEntity.addChild(revolvingDanceFloor)
          }
          
          /// Creates a revolving container entity to keep robots moving in sync with the dance floor
          private func addRobotRevolutionControllerToTheClub() {
              let robotRevolutionController = Entity()
              robotRevolutionController.name = "Robot Revolution Controller"
              robotRevolutionController.components[RevolvingComponent.self] = RevolvingComponent(relativeTo: rootEntity)
              
              rootEntity.addChild(robotRevolutionController)
              
              self.robotRevolutionController = robotRevolutionController
          }
          
          /// Add invisible attractors to the dance floor to position and control robots
          private func addDanceFloorAttractors() {
              guard let robotRevolutionController else {
                  logger.error("The Robot Revolution Controller is missing 😱")
                  return
              }
              
              // Add a few dance spots on the outside of the club that we know don't obstruct the furniture
              let staticAttractors = Entity()
              staticAttractors.name = "Static Attractors"
              
              let placementRadius: Float = 0.25
              let outerRadius = placementRadius * 0.8
              addDanceFloorAttractor(to: staticAttractors, angle: Angle2D(degrees: 10), placementRadius: outerRadius, name: "Static-A1", variation: 0)
              addDanceFloorAttractor(to: staticAttractors, angle: Angle2D(degrees: 90), placementRadius: outerRadius, name: "Static-A2", variation: 0)
              addDanceFloorAttractor(to: staticAttractors, angle: Angle2D(degrees: 130), placementRadius: outerRadius, name: "Static-A3", variation: 0)
              addDanceFloorAttractor(to: staticAttractors, angle: Angle2D(degrees: 240), placementRadius: outerRadius, name: "Static-A4", variation: 0)
              addDanceFloorAttractor(to: staticAttractors, angle: Angle2D(degrees: 325), placementRadius: outerRadius, name: "Static-A5", variation: 0)
              
              rootEntity.addChild(staticAttractors)
              
              // The remaining center attractors are on the revolving dance floor and can be more randomly positioned
              let innerRingCapacity = doorSupervisor.capacity - 5
              
              let revolvingAttractors = Entity()
              revolvingAttractors.name = "Revolving Attractors"
              
              addDanceFloorAttractors(to: revolvingAttractors, count: innerRingCapacity, placementRadius: placementRadius * 0.3, namePrefix: "Revolving")
              
              robotRevolutionController.addChild(revolvingAttractors)
              
      #if DEBUG
              // Add some debug visualizations
              let debugRoot = Entity()
              debugRoot.name = "[Debug] Dance System"
              debugRoot.isEnabled = false
              debugRoot.components[DanceSystemDebugComponent.self] = DanceSystemDebugComponent()
              
              rootEntity.addChild(debugRoot)
              
              let allAttractors = Array(staticAttractors.children) + Array(revolvingAttractors.children)
              
              // Create a new visualization for each attractor
              allAttractors.forEach { attractor in
                  if let visualization = Entity.makeDebugMarker(height: 0.08, radius: 0.03, enabled: true) {
                      guard let attractorComponent = attractor.components[AttractorComponent.self] else {
                          return
                      }
                      
                      let debugComponent = AttractorDebugComponent(state: attractorComponent.state, attractor: attractor)
                      
                      visualization.position = [0, 0.04, 0]
                      visualization.components[AttractorDebugComponent.self] = debugComponent
                      debugRoot.addChild(visualization)
                  }
              }
      #endif
          }
          
          /// Add multiple dance floor attractors along the circumference of a circle with the specified placementRadius
          private func addDanceFloorAttractors(to danceFloor: Entity, count: Int, placementRadius: Float, namePrefix: String, variation: Float = 0.005) {
              let angleIncrements = 360 / count
              
              for offset in 0..<count {
                  let angle = Angle2D(degrees: Double(angleIncrements * offset))
                  let name = "\(namePrefix)-A\(offset + 1)"
                  addDanceFloorAttractor(to: danceFloor, angle: angle, placementRadius: placementRadius, name: name, variation: variation)
              }
          }
          
          /// Adds a single dance floor attractor at a point on the circumference of a circle with the specified placementRadius
          private func addDanceFloorAttractor(to danceFloor: Entity, angle: Angle2D, placementRadius: Float, name: String, variation: Float = 0.005) {
              let attractor = Entity()
              attractor.name = name
              attractor.components[AttractorComponent.self] = AttractorComponent(club: rootEntity)
              attractor.position = pointOnCircumference(angle: angle, radius: placementRadius, variation: variation)
              danceFloor.addChild(attractor)
          }
          
          /// Adds some revolving spot lights to the club
          private func addSpotlightsToTheClub() {
              let placementRadius: Float = 0.5
              let lightsWrapper = Entity()
              lightsWrapper.name = "Light Rig"
              
              let magentaLight = SpotLight()
              magentaLight.light.color = .magenta
              magentaLight.light.intensity = 500
              var lightPosition = pointOnCircumference(angle: Angle2D(degrees: 0), radius: placementRadius, y: 0.5)
              magentaLight.look(at: .zero, from: lightPosition, relativeTo: rootEntity)
              lightsWrapper.addChild(magentaLight)
              
              let greenLight = magentaLight.clone(recursive: true)
              greenLight.light.color = .green
              lightPosition = pointOnCircumference(angle: Angle2D(degrees: 120), radius: placementRadius, y: 0.5)
              greenLight.look(at: .zero, from: lightPosition, relativeTo: rootEntity)
              lightsWrapper.addChild(greenLight)
              
              let cyanLight = magentaLight.clone(recursive: true)
              cyanLight.light.color = .cyan
              lightPosition = pointOnCircumference(angle: Angle2D(degrees: 240), radius: placementRadius, y: 0.5)
              cyanLight.look(at: .zero, from: lightPosition, relativeTo: rootEntity)
              lightsWrapper.addChild(cyanLight)
              
              lightsWrapper.components[RevolvingComponent.self] = RevolvingComponent(speed: -0.2, relativeTo: rootEntity)
              
              rootEntity.addChild(lightsWrapper)
          }
          
          /// Repurpose some planters to make a counter and stocks with a premium aged oil, and a friendly host
          private func addCounterToTheClub() {
              guard let planter = rootEntity.findEntity(named: "planter_big"),
                    let dirt = rootEntity.findEntity(named: "dirt_big") else {
                  logger.error("Making the counter failed... too much dancing may now cause rust 🤖")
                  return
              }
              
              // Group into a container entity
              let counter = Entity()
              counter.name = "Counter"
              counter.position = [0.333, 0.05, -0.09]
              rootEntity.addChild(counter)
              
              // Repurpose existing assets
              let counterGeometry = Entity()
              counterGeometry.name = "Counter Geometry"
              counterGeometry.addChild(planter, preservingWorldTransform: true)
              counterGeometry.addChild(dirt, preservingWorldTransform: true)
              counterGeometry.scale = [2, 6, 2]
              counterGeometry.position = [-0.3335, -0.15, 0.09]
              counter.addChild(counterGeometry)
              
              var counterTopMaterial = PhysicallyBasedMaterial()
              counterTopMaterial.baseColor = .init(tint: .white)
              counterTopMaterial.roughness = .init(floatLiteral: 0)
              counterTopMaterial.metallic = .init(floatLiteral: 1)
              
              dirt.components[ModelComponent.self]?.materials = [counterTopMaterial]
              dirt.position += [0, 0.001, 0]
              
              // Add a fancy hover rail
              if let rim = rootEntity.findEntity(named: "bottom_rim_1") {
                  let hoverRailing = rim.clone(recursive: true)
                  hoverRailing.name = "Hover Railing"
                  hoverRailing.position = [0, 0.1, 0]
                  hoverRailing.scale = rim.scale(relativeTo: rootEntity) * 0.5
                  hoverRailing.components[HoverComponent.self] = HoverComponent(from: hoverRailing.position, to: hoverRailing.position + [0, -0.03, 0])
                  counter.addChild(hoverRailing)
              }
              
              // Add some bottles to the counter
              let bottles = stockBottles(placementRadius: 0.045)
              counter.addChild(bottles)
              
              // Hide any out of stock items
              for bottle in bottles.children {
                  bottle.isEnabled = bottle.components[OutOfStockComponent.self] == nil
              }
              
              // Add a friendly host
              addHostToTheCounter(counter)
          }
          
          /// Adds 9 green bottles of the finest aged oil to the counter (assuming we have them in stock)
          private func stockBottles(placementRadius: Float) -> Entity {
              let bottleRadius: Float = 0.003
              let bottleHeight: Float = 0.022
              let angleIncrement: Float = -12
              let outOfStockBrands: Set = [3]
              
              // Make a wrapper entity
              let bottleGroup = Entity()
              bottleGroup.name = "Bottle Group"
              bottleGroup.position = [0, 0.04, 0]
              bottleGroup.orientation = .init(angle: 180 * (.pi / 180), axis: [0, 1, 0])
              
              // Make a nice green material
              var bottleMaterial = PhysicallyBasedMaterial()
              bottleMaterial.baseColor = .init(tint: .green)
              bottleMaterial.blending = .transparent(opacity: .init(floatLiteral: 0.5))
              
              // A simple cylinder mesh
              let bottleMesh = MeshResource.generateCylinder(height: bottleHeight, radius: bottleRadius)
              
              // Error 1: Content occluded
              let bottle1 = Entity()
              bottle1.name = "BT1"
              bottle1.position = pointOnCircumference(angle: .zero, radius: placementRadius, y: -0.03)
              bottle1.components[ModelComponent.self] = ModelComponent(mesh: bottleMesh, materials: [bottleMaterial])
              bottleGroup.addChild(bottle1)
              
              // Error 2: Content clipped
              let bottle2 = Entity()
              bottle2.name = "BT2"
              bottle2.position = pointOnCircumference(angle: Angle2D(degrees: angleIncrement), radius: 1.6, y: bottleHeight / 2)
              bottle2.components[ModelComponent.self] = ModelComponent(mesh: bottleMesh, materials: [bottleMaterial])
              bottleGroup.addChild(bottle2)
              
              // Error 3: Content inside out
              let bottle3 = Entity()
              bottle3.name = "BT3"
              bottle3.position = pointOnCircumference(angle: Angle2D(degrees: 2 * angleIncrement), radius: placementRadius, y: bottleHeight / 2)
              bottle3.scale = .init(repeating: 650)
              bottle3.components[ModelComponent.self] = ModelComponent(mesh: bottleMesh, materials: [bottleMaterial])
              bottleGroup.addChild(bottle3)
              
              // Error 4: Content not enabled
              let bottle4 = Entity()
              bottle4.name = "BT4"
              bottle4.position = pointOnCircumference(angle: Angle2D(degrees: 3 * angleIncrement), radius: placementRadius, y: bottleHeight / 2)
              bottle4.components[ModelComponent.self] = ModelComponent(mesh: bottleMesh, materials: [bottleMaterial])
              bottle4.components[OutOfStockComponent.self] = OutOfStockComponent()
              bottleGroup.addChild(bottle4)
              
              // Error 5: Content not anchored
              let bottle5 = Entity()
              bottle5.name = "BT5"
              bottle5.position = pointOnCircumference(angle: Angle2D(degrees: 4 * angleIncrement), radius: placementRadius, y: bottleHeight / 2)
              bottle5.components[AnchoringComponent.self] = AnchoringComponent(.plane(.horizontal, classification: .table, minimumBounds: .zero))
              bottle5.components[ModelComponent.self] = ModelComponent(mesh: bottleMesh, materials: [bottleMaterial])
              bottleGroup.addChild(bottle5)
              
              // Error 6: Content missing a mesh
              let bottle6 = Entity()
              bottle6.name = "BT6"
              bottle6.position = pointOnCircumference(angle: Angle2D(degrees: 5 * angleIncrement), radius: placementRadius, y: bottleHeight / 2)
              bottle5.components[ModelComponent.self] = ModelComponent(mesh: bottleMesh, materials: [bottleMaterial])
              bottleGroup.addChild(bottle6)
              
              // Error 7: Content's material misconfigured
              let bottle7 = Entity()
              bottle7.name = "BT7"
              bottle7.position = pointOnCircumference(angle: Angle2D(degrees: 6 * angleIncrement), radius: placementRadius, y: bottleHeight / 2)
              
              var simplifiedBottleMaterial = UnlitMaterial(color: .green.withAlphaComponent(0.5))
              simplifiedBottleMaterial.opacityThreshold = 1
              
              bottle7.components[ModelComponent.self] = ModelComponent(mesh: bottleMesh, materials: [simplifiedBottleMaterial])
              bottleGroup.addChild(bottle7)
              
              // Error 8: Content has a broken mesh
              let alternativeMesh = MeshResource.generateAbnormalCylinder(height: bottleHeight, radius: bottleRadius)
              let bottle8 = Entity()
              bottle8.name = "BT8"
              bottle8.position = pointOnCircumference(angle: Angle2D(degrees: 7 * angleIncrement), radius: placementRadius, y: bottleHeight / 2)
              bottle8.scale = [bottle8.scale.x, bottle8.scale.y, -bottle8.scale.z]
              bottleMaterial.opacityThreshold = 0
              bottle8.components[ModelComponent.self] = ModelComponent(mesh: alternativeMesh, materials: [bottleMaterial])
              bottleGroup.addChild(bottle8)
              
              // Error 9: Content not added to the scene hierarchy
              let bottle9 = Entity()
              bottle9.name = "BT9"
              bottle9.position = pointOnCircumference(angle: Angle2D(degrees: 8 * angleIncrement), radius: placementRadius, y: bottleHeight / 2)
              bottle9.components[ModelComponent.self] = ModelComponent(mesh: bottleMesh, materials: [bottleMaterial])
              bottleGroup.addChild(bottle8)
              
              // FIXME: Bottles are missing from the counter
              
              return bottleGroup
          }
          
          /// Add a host robot to the counter
          private func addHostToTheCounter(_ counter: Entity) {
              // Make a clone of our hero BOTanist
              let robotMaker = AppState()
              
              guard let skeleton = robotMaker.robotData.meshes[.body]?.findEntity(named: "rig_grp") as? ModelEntity else {
                  fatalError()
              }
              
              // But use the hover body to best complement the counter
              robotMaker.setMesh(part: .body, name: "body3")
              
              guard let head = robotMaker.robotData.meshes[.head]?.clone(recursive: true),
                    let body = robotMaker.robotData.meshes[.body]?.clone(recursive: true),
                    let backpack = robotMaker.robotData.meshes[.backpack]?.clone(recursive: true) else {
                  fatalError()
              }
              
              let robotCharacter = RobotCharacter(
                  head: head,
                  body: body,
                  backpack: backpack,
                  appState: robotMaker,
                  headOffset: skeleton.pins["head"]?.position,
                  backpackOffset: skeleton.pins["backpack"]?.position
              )
              
              // Remove the character controller and animation state, as we'll manually control these
              AnimationState.handlers.removeAll()
              robotCharacter.characterParent.components[CharacterControllerComponent.self] = nil
              
              // Take off that heavy backpack
              backpack.removeFromParent()
              
              // Setup our host using the character and add it to the counter
              let host = Entity()
              host.name = "Host"
              host.orientation = .init(angle: 300 * (.pi / 180), axis: [0, 1, 0])
              host.position = [0, 0.005, 0]
              host.components[AutomatonControl.self] = AutomatonControl(character: robotCharacter)
              host.addChild(robotCharacter.characterParent)
              counter.addChild(host)
              
              // Have them say Hi
              robotCharacter.transitionToAndPlayAnimation(.wave)
              
              // Save a reference so they can wave later when other bots enter
              self.host = host
          }
          
          /// Generates a disco ball looking entity, makes it revolve and hover, and adds it to the club
          private func addDiscoBallToTheClub() {
              // Add the top level revolving, hovering disco ball entity
              let discoBall = Entity()
              discoBall.name = "Disco Ball"
              discoBall.position = [-0.305, 0.17, 0.02]
              discoBall.components[RevolvingComponent.self] = RevolvingComponent(speed: -0.02, relativeTo: rootEntity)
              discoBall.components[HoverComponent.self] = HoverComponent(from: discoBall.position, to: discoBall.position + [0, 0.02, 0])
              
              rootEntity.addChild(discoBall)
              
              // Add a support beam to hold the disco ball
              var supportMaterial = PhysicallyBasedMaterial()
              supportMaterial.baseColor = .init(tint: .lightGray)
              supportMaterial.roughness = .init(floatLiteral: 0.8)
              supportMaterial.metallic = .init(floatLiteral: 0.8)
              
              let support = ModelEntity(mesh: .generateCylinder(height: 0.01, radius: 0.01), materials: [supportMaterial])
              support.scale = [0.2, 1.8, 0.2]
              support.position = [0, 0.05, 0]
              support.name = "Support"
              
              discoBall.addChild(support)
              
              // Add the shiny ball that is the base of our disco ball
              var backgroundMaterial = PhysicallyBasedMaterial()
              backgroundMaterial.baseColor = .init(tint: .lightGray)
              backgroundMaterial.roughness = .init(floatLiteral: 0)
              backgroundMaterial.metallic = .init(floatLiteral: 1)
              
              let background = ModelEntity(mesh: .generateSphere(radius: 0.05), materials: [backgroundMaterial])
              background.name = "Background"
              
              // FIXME: Unintentionally inheriting an ancestor's transformation
              support.addChild(background)
              
              // Add some detailed lines on top of the background
              var lineMaterial = PhysicallyBasedMaterial()
              lineMaterial.baseColor = .init(tint: .lightGray)
              lineMaterial.sheen = .init(tint: .lightGray)
              lineMaterial.emissiveColor = .init(color: .lightGray)
              lineMaterial.emissiveIntensity = 1
              lineMaterial.triangleFillMode = .lines
              
              let ballOutline = ModelEntity(mesh: .generateSphere(radius: 0.0505), materials: [lineMaterial])
              ballOutline.name = "Outline"
              
              background.addChild(ballOutline)
          }
          
          /// Marks the club as ready
          private func openDoors() {
              var management = self.doorSupervisor
              management.doorsOpen = true
              self.doorSupervisor = management
          }
          
          /// finds a point along the edge of a circle on an XZ-plane, given a radius and y value. Optionally applies some variance.
          private func pointOnCircumference(angle: Angle2D, radius: Float, variation: Float = 0, y: Float = 0) -> SIMD3<Float> {
              .init(
                  x: (Float(cos(angle)) * radius) + .random(in: -variation...variation),
                  y: y,
                  z: (Float(sin(angle)) * radius) + .random(in: -variation...variation)
              )
          }
      }
      
      // MARK: Club Management
      
      /// Manages club capacity and ready state
      struct DoorSupervisor: Component {
          let capacity: Int
          var doorsOpen = false
          var visitorCount = 0
          
          var hasCapacity: Bool {
              visitorCount < capacity
          }
      }
      
      /// Tag to indicate if a retail item is in stock
      struct OutOfStockComponent: Component {}
      
      // MARK: Revolution Control
      
      /// Works with the RevolvingSystem to apply a continuous rotation to an entity
      struct RevolvingComponent: Component {
          var speed: Float
          var angle: Float
          var axis: SIMD3<Float>
          var relativeTo: Entity?
          
          init(speed: Float = 0.05, initialAngle: Float = 0, axis: SIMD3<Float> = [0, 1, 0], relativeTo: Entity? = nil) {
              self.speed = speed
              self.angle = initialAngle
              self.axis = axis
              self.relativeTo = relativeTo
          }
      }
      
      /// Works with the RevolvingComponent to apply a continuous rotation to an entity
      @MainActor
      class RevolvingSystem: System {
          private static let query = EntityQuery(where: .has(RevolvingComponent.self))
          
          required init(scene: RealityKit.Scene) {}
          
          func update(context: SceneUpdateContext) {
              for entity in context.entities(matching: Self.query, updatingSystemWhen: .rendering) {
                  if var revolvingComponent = entity.components[RevolvingComponent.self] {
                      let relativeTo = revolvingComponent.relativeTo
                      
                      revolvingComponent.angle += .pi * Float(context.deltaTime) * revolvingComponent.speed
                      entity.setOrientation(.init(angle: revolvingComponent.angle, axis: revolvingComponent.axis), relativeTo: relativeTo)
                      
                      entity.components[RevolvingComponent.self] = revolvingComponent
                  }
              }
          }
      }
      
      // MARK: Hover Control
      
      /// Works with the HoverSystem to apply a continuous levitation like bounce to an entity
      struct HoverComponent: Component {
          var speed: Float
          var angle: Float
          var from: SIMD3<Float>
          var to: SIMD3<Float>
          
          init(speed: Float = 0.06, angle: Float = 0, from: SIMD3<Float>, to: SIMD3<Float>) {
              self.speed = speed
              self.angle = angle
              self.from = from
              self.to = to
          }
      }
      
      /// Works with the HoverComponent to apply a continuous levitation like bounce to an entity
      @MainActor
      class HoverSystem: System {
          private static let query = EntityQuery(where: .has(HoverComponent.self))
          
          required init(scene: RealityKit.Scene) {}
          
          func update(context: SceneUpdateContext) {
              for entity in context.entities(matching: Self.query, updatingSystemWhen: .rendering) {
                  if var hoverComponent = entity.components[HoverComponent.self] {
                      
                      hoverComponent.angle += .pi * Float(context.deltaTime) * hoverComponent.speed
                      
                      let range = hoverComponent.to - hoverComponent.from
                      let proportion = (sin(hoverComponent.angle) + 1) / 2
                      
                      entity.position = hoverComponent.from + (proportion * range)
                      
                      entity.components[HoverComponent.self] = hoverComponent
                  }
              }
          }
      }
      
      // MARK: Robot Parts
      
      /// A wrapper around a Robot Character that is actually used as an Automaton
      struct AutomatonControl: Component {
          var character: RobotCharacter
      }
      
      extension RobotCharacter {
          /// manually control the animation transition of a single robot instance
          func transitionToAndPlayAnimation(_ animationState: AnimationState) {
              if self.animationState.transition(to: animationState) {
                  playAnimation(animationState)
              }
          }
      }
      
      /// A collection of shuffled robot names for our Automatons
      @MainActor
      enum RobotNames {
          static var count: Int = 0
          static var next: String {
              count += 1
              
              return "Robo-v\(count)"
          }
      }
      
      // MARK: Teleportation
      
      /// Works with the TeleportationSystem to control spawning across all teleporters
      struct ControlCenterComponent: Component {
          typealias SpawnHandler = (Entity) -> Void
          
          var initialValue: TimeInterval
          var interval: TimeInterval
          var countdown: TimeInterval
          var rootEntity: Entity
          var _spawnHandler: SpawnHandler
          
          init(initialValue: TimeInterval, interval: TimeInterval, rootEntity: Entity, spawnHandler: @escaping SpawnHandler) {
              self.initialValue = initialValue
              self.interval = interval
              self.countdown = initialValue
              self.rootEntity = rootEntity
              self._spawnHandler = spawnHandler
          }
      }
      
      /// Represents a single Teleporter in the TeleportationSystem
      struct TeleporterComponent: Component {}
      
      /// Works with the ControlCenterComponent to control spawning across all teleporters
      @MainActor
      class TeleportationSystem: System {
          private static let controlCenterQuery = EntityQuery(where: .has(ControlCenterComponent.self))
          private static let teleporterQuery = EntityQuery(where: .has(TeleporterComponent.self))
          private static let robotQuery = EntityQuery(where: .has(AutomatonControl.self))
          
          required init(scene: RealityKit.Scene) {}
          
          func update(context: SceneUpdateContext) {
              for entity in context.entities(matching: Self.controlCenterQuery, updatingSystemWhen: .rendering) {
                  update(controlCenter: entity, context: context)
              }
          }
          
          private func safeToUse(teleporter: Entity, context: SceneUpdateContext) -> Bool {
              let someBotIsStandingToClose = context.entities(matching: Self.robotQuery, updatingSystemWhen: .rendering)
                  .contains { entity in
                      distance(entity.position(relativeTo: nil), teleporter.position(relativeTo: nil)) < 0.02
                  }
              
              return  !someBotIsStandingToClose
          }
          
          private func update(controlCenter controlCenterEntity: Entity, context: SceneUpdateContext) {
              guard var controlCenter = controlCenterEntity.components[ControlCenterComponent.self],
                    let clubManager = controlCenter.rootEntity.components[DoorSupervisor.self],
                    clubManager.hasCapacity else {
                  return
              }
              
              // 1. Decrease countdown, and activate if it reaches zero
              controlCenter.countdown -= context.deltaTime
              if controlCenter.countdown <= 0 {
                  
                  // 2. Find all the active teleporters and pick a random one
                  if let teleporter = context.entities(matching: Self.teleporterQuery, updatingSystemWhen: .rendering).shuffled().first {
                      
                      // 3. If no other robots are in the way, pass it to the designated spawn method
                      if safeToUse(teleporter: teleporter, context: context) {
                          controlCenter._spawnHandler(teleporter)
                      }
                  }
                  
                  // 4. Set the delay till the next spawn event
                  controlCenter.countdown = controlCenter.interval
              }
              
              // FIXME: Control Center is not being updated
          }
      }
      
      extension ParticleEmitterComponent.Presets {
          /// Makes a particle emitter component that looks like a teleporter
          fileprivate static var teleporter: ParticleEmitterComponent {
              var particleEmitter = ParticleEmitterComponent.Presets.rain
              
              particleEmitter.birthLocation = .surface
              particleEmitter.emitterShape = .torus
              particleEmitter.particlesInheritTransform = false
              particleEmitter.fieldSimulationSpace = .global
              particleEmitter.speed = 0.07
              particleEmitter.speedVariation = 0.03
              particleEmitter.radialAmount = 360
              particleEmitter.torusInnerRadius = 0.001
              particleEmitter.emissionDirection = [0, 1, 0]
              particleEmitter.spawnedEmitter = nil
              particleEmitter.burstCount = 5000
              particleEmitter.mainEmitter.opacityCurve = .linearFadeOut
              particleEmitter.mainEmitter.birthRate = 50
              particleEmitter.mainEmitter.birthRateVariation = 10
              particleEmitter.mainEmitter.lifeSpan = 0.5
              particleEmitter.mainEmitter.lifeSpanVariation = 0.01
              particleEmitter.mainEmitter.size = 0.001
              particleEmitter.mainEmitter.sizeVariation = 0.0005
              particleEmitter.mainEmitter.sizeMultiplierAtEndOfLifespan = 0.01
              particleEmitter.mainEmitter.stretchFactor = 10
              particleEmitter.mainEmitter.noiseStrength = 0
              particleEmitter.mainEmitter.spreadingAngle = 0
              particleEmitter.mainEmitter.angle = 0
              
              particleEmitter.spawnedEmitter = nil
              
              return particleEmitter
          }
      }
      
      // MARK: Dancing
      
      /// Represents a single Attractor in the DanceMotivationSystem
      struct AttractorComponent: Component {
          enum State {
              case vacant
              case attracting
              case motivating
          }
          
          private(set) var state: State = .vacant
          
          var target: Entity?
          var walkSpeed: Float = 0.1
          var interval: TimeInterval = 5
          var countdown: TimeInterval = 5
          var club: Entity?
          
          var isVacant: Bool {
              if case .vacant = state {
                  return true
              }
              return false
          }
          
          mutating func setTarget(_ target: Entity) {
              self.target = target
              self.state = .attracting
          }
          
          mutating func targetReached() {
              self.state = .motivating
          }
      }
      
      /// Represents a single Robot in the DanceMotivationSystem
      struct Newcomer: Component {}
      
      /// Works with the DanceMotivationSystem to provide additional Debug information to the RealityKit Debugger
      struct DanceSystemDebugComponent: Component {
          var states: UIImage? = nil
          var vacant: Int = 0
          var attracting: Int = 0
          var motivating: Int = 0
      }
      
      /// Provides additional Debug information about a single Attractor in the DanceMotivationSystem to the RealityKit Debugger
      struct AttractorDebugComponent: Component {
          var state: AttractorComponent.State
          var attractor: Entity
          var robot: Entity?
      }
      
      /// Manages the states of dance floor attractors, the movement of robots and the relationships between them
      @MainActor
      class DanceMotivationSystem: System {
          private static let attractorQuery = EntityQuery(where: .has(AttractorComponent.self))
          private static let targetQuery = EntityQuery(where: .has(Newcomer.self))
          private static let clubbersQuery = EntityQuery(where: .has(AutomatonControl.self))
          private static let debugRootQuery = EntityQuery(where: .has(DanceSystemDebugComponent.self))
          private static let debugVisualizationsQuery = EntityQuery(where: .has(AttractorDebugComponent.self))
          
          required init(scene: RealityKit.Scene) {}
          
          func update(context: SceneUpdateContext) {
              
              // 1. Check for newcomers at the club who could be enticed to come and dance
              for visitor in context.entities(matching: Self.targetQuery, updatingSystemWhen: .rendering) {
                  
                  // 2. Randomly pick an attractor
                  guard let attractor = context.entities(matching: Self.attractorQuery, updatingSystemWhen: .rendering)
                      .filter({ $0.components[AttractorComponent.self]?.isVacant ?? false })
                      .randomElement() else {
                      return
                  }
                  
                  // 3. Start attracting the visitor
                  var attractorComponent = attractor.components[AttractorComponent.self]!
                  attractorComponent.setTarget(visitor)
                  attractor.components[AttractorComponent.self] = attractorComponent
                  
                  // FIXME: Stop attractors competing over the same bot
              }
              
              // Let the attractors do their thing and attract visitors to come and dance
              for attractor in context.entities(matching: Self.attractorQuery, updatingSystemWhen: .rendering) {
                  guard var attractorComponent = attractor.components[AttractorComponent.self] else {
                      continue
                  }
                  
                  switch attractorComponent.state {
                  case .attracting:
                      if let updatedAttractorComponent = attractRobot(attractor: attractor, deltaTime: Float(context.deltaTime)) {
                          attractorComponent = updatedAttractorComponent
                      }
                      
                  case .motivating:
                      if let updatedAttractorComponent = motivateRobot(attractor: attractor, context: context) {
                          attractorComponent = updatedAttractorComponent
                      }
                      
                  default:
                      break
                  }
                  
                  // save changes
                  attractor.components[AttractorComponent.self] = attractorComponent
              }
              
      #if DEBUG
              updateDebugInfo(context: context)
      #endif
          }
          
          private func attractRobot(attractor: Entity, deltaTime: Float) -> AttractorComponent? {
              guard var attractorComponent = attractor.components[AttractorComponent.self],
                    case .attracting = attractorComponent.state,
                    let target = attractorComponent.target,
                    let robotCharacter = target.components[AutomatonControl.self]?.character else {
                  return nil
              }
              
              // robots wave when they first arrive, make sure that is completed first before moving
              var transitionAnimationTo: AnimationState?
              switch robotCharacter.animationState {
              case .wave: transitionAnimationTo = .idle
              case .idle: transitionAnimationTo = .walkLoop
              case .walkLoop: transitionAnimationTo = nil
              default: return attractorComponent
              }
              
              if let transitionAnimationTo {
                  if robotCharacter.animationState.transition(to: transitionAnimationTo) {
                      robotCharacter.playAnimation(robotCharacter.animationState)
                  }
              }
              
              // Convert the robot and target positions into the same coordinate system
              let targetPosition = target.position(relativeTo: attractorComponent.club)
              var danceSpotPosition = attractor.position(relativeTo: attractorComponent.club)
              danceSpotPosition.y = targetPosition.y
              
              let movementVector = danceSpotPosition - targetPosition
              let normalizedMovement = movementVector / length(movementVector)
              let move = normalizedMovement * deltaTime * attractorComponent.walkSpeed
              
              target.setPosition(targetPosition + move, relativeTo: attractorComponent.club)
              
              robotCharacter.characterModel.look(at: robotCharacter.characterModel.position - normalizedMovement,
                                                 from: robotCharacter.characterModel.position, relativeTo: robotCharacter.characterParent)
              
              // If the target is more or less in position then attach to the dance spot and change state to motivating
              if distance(danceSpotPosition, target.position(relativeTo: attractorComponent.club)) < 0.005 {
                  attractor.addChild(target, preservingWorldTransform: true)
                  
                  // Start Dancing
                  robotCharacter.transitionToAndPlayAnimation(.celebrate)
                  
                  // Update attractor state
                  attractorComponent.targetReached()
              }
              
              return attractorComponent
          }
          
          private func motivateRobot(attractor: Entity, context: SceneUpdateContext) -> AttractorComponent? {
              guard var attractorComponent = attractor.components[AttractorComponent.self],
                    case .motivating = attractorComponent.state,
                    let target = attractorComponent.target,
                    let robotCharacter = target.components[AutomatonControl.self]?.character else {
                  return nil
              }
              
              attractorComponent.countdown -= context.deltaTime
              
              if attractorComponent.countdown <= 0 {
                  // Turn to face a random fellow clubber
                  if let friend = Array(context.entities(matching: Self.clubbersQuery, updatingSystemWhen: .rendering)).randomElement() {
                      let friendsPosition = friend.position(relativeTo: robotCharacter.characterParent)
                      
                      robotCharacter.characterModel.look(at: friendsPosition,
                                                         from: robotCharacter.characterModel.position, relativeTo: robotCharacter.characterParent)
                      
                      // TODO: remove me
                      print("🔥 friendsPosition \(friendsPosition) targetPosition \(robotCharacter.characterModel.position)")
                  }
                  
                  attractorComponent.countdown = attractorComponent.interval
              }
              
              return attractorComponent
          }
          
      #if DEBUG
          let vacantColor = UnlitMaterial.BaseColor(tint: .yellow.withAlphaComponent(0.5))
          let attractingColor = UnlitMaterial.BaseColor(tint: .orange.withAlphaComponent(0.5))
          let motivatingColor = UnlitMaterial.BaseColor(tint: .red.withAlphaComponent(0.5))
          
          private func updateDebugInfo(context: SceneUpdateContext) {
              var vacantCount: Int = 0
              var attractingCount: Int = 0
              var motivatingCount: Int = 0
              
              context.entities(matching: Self.debugVisualizationsQuery, updatingSystemWhen: .rendering).forEach { visualization in
                  guard let visualizationComponent = visualization.components[AttractorDebugComponent.self],
                        let attractorComponent = visualizationComponent.attractor.components[AttractorComponent.self] else {
                      return
                  }
                  
                  updateVisualizationEntity(visualization, relativeTo: attractorComponent.club)
                  
                  switch attractorComponent.state {
                  case .vacant: vacantCount += 1
                  case .attracting: attractingCount += 1
                  case .motivating: motivatingCount += 1
                  }
              }
              
              context.entities(matching: Self.debugRootQuery, updatingSystemWhen: .rendering).forEach { debugRoot in
                  if var debugComponent = debugRoot.components[DanceSystemDebugComponent.self] {
                      debugComponent.vacant = vacantCount
                      debugComponent.attracting = attractingCount
                      debugComponent.motivating = motivatingCount
                      debugComponent.states = makeChart(vacantCount: vacantCount, attractingCount: attractingCount, motivatingCount: motivatingCount)
                      debugRoot.components[DanceSystemDebugComponent.self] = debugComponent
                  }
              }
          }
          
          private func updateVisualizationEntity(_ visualization: Entity, relativeTo root: Entity?) {
              guard var visualizationComponent = visualization.components[AttractorDebugComponent.self],
                    let attractorComponent = visualizationComponent.attractor.components[AttractorComponent.self] else {
                  return
              }
              
              // Update the position
              var position = visualizationComponent.attractor.position(relativeTo: root)
              position.y = visualization.position.y
              visualization.setPosition(position, relativeTo: root)
              
              // Update the state
              visualizationComponent.state = attractorComponent.state
              visualization.name = "[Debug] \(visualizationComponent.attractor.name) (\(attractorComponent.state))"
              
              // Update the base material color to signify the attractor state
              if var modelComponent = visualization.components[ModelComponent.self],
                 var material = modelComponent.materials.first as? UnlitMaterial {
                  
                  switch attractorComponent.state {
                  case .vacant: material.color = vacantColor
                  case .attracting: material.color = attractingColor
                  case .motivating: material.color = motivatingColor
                  }
                  
                  modelComponent.materials = [material]
                  visualization.components[ModelComponent.self] = modelComponent
              }
              
              // Update the target
              visualizationComponent.robot = attractorComponent.target
              visualization.components[AttractorDebugComponent.self] = visualizationComponent
          }
          
          private func makeChart(vacantCount: Int, attractingCount: Int, motivatingCount: Int) -> UIImage? {
              ImageRenderer(content: chartView(vacantCount: vacantCount, attractingCount: attractingCount, motivatingCount: motivatingCount)).uiImage
          }
          
          private func chartView(vacantCount: Int, attractingCount: Int, motivatingCount: Int) -> some View {
              Chart(
                  [
                      (name: "Vacant", count: vacantCount),
                      (name: "Attracting", count: attractingCount),
                      (name: "Motivating", count: motivatingCount)
                  ], id: \.name) { name, count in
                      SectorMark(
                          angle: .value("Value", count),
                          angularInset: 1.5
                      )
                      .cornerRadius(5)
                      .foregroundStyle(by: .value("Name", name))
              }
              .chartLegend(.hidden)
              .chartForegroundStyleScale(["Vacant": .yellow, "Attracting": .orange, "Motivating": .red])
              .frame(width: 1024, height: 1024)
          }
          
      #endif
      }
      
      // MARK: Debug Helpers
      
      extension Entity {
          /// creates an semi-transparent entity that can be useful in debug invisible entities in the RealityKit Debugger
          static func makeDebugMarker(name: String? = nil, height: Float, radius: Float, color: UIColor = .white, enabled: Bool = false) -> Entity? {
      #if DEBUG
              var debugMaterial = UnlitMaterial()
              debugMaterial.color = .init(tint: color)
              debugMaterial.blending = .transparent(opacity: 0.7)
              
              let marker = ModelEntity(mesh: .generateCylinder(height: height, radius: radius), materials: [debugMaterial])
              if let name {
                  marker.name = name
              }
              marker.isEnabled = enabled
              
              return marker
      #else
              return nil
      #endif
          }
          
          /// adds an semi-transparent child entity that can be useful in debug invisible entities in the RealityKit Debugger
          @discardableResult
          func addDebugMarker(name: String? = nil, height: Float? = nil, radius: Float? = nil, color: UIColor = .white, enabled: Bool = false) -> Entity? {
      #if DEBUG
              var markerRadius: Float
              if radius != nil {
                  markerRadius = radius!
              } else {
                  // If no provided radius then calculate from the visual bounds
                  let extents = visualBounds(relativeTo: nil).extents
                  let boundingXZRadius = max(extents.x, extents.z) / 2
                  
                  if boundingXZRadius.isNormal {
                      markerRadius = boundingXZRadius
                  } else {
                      // If no visual bounds then use a default radius of 1cm
                      markerRadius = 0.01 * scale(relativeTo: nil).max()
                  }
              }
              
              // If no provided height then use a default value of 10cm
              let markerHeight = height ?? 0.1 * scale(relativeTo: nil).max()
              
              let name = name ?? "[Debug] \(self.name)"
              if let marker = Entity.makeDebugMarker(name: name, height: markerHeight, radius: markerRadius, color: color, enabled: enabled) {
                  marker.position = [0, markerHeight / 2, 0]
                  addChild(marker)
                  
                  return marker
              }
      #endif
              return nil
          }
      }
      
      // MARK: Demo Helpers
      
      extension MeshResource {
          /// Generates an cylinder with all the normals facing downwards. Probably has no uses other than demo'ing a broken mesh.
          static func generateAbnormalCylinder(height: Float, radius: Float) -> MeshResource {
              let meshResource = MeshResource.generateCylinder(height: height, radius: radius)
              var contents = meshResource.contents
              let models = contents.models.map { model in
                  var model = model
                  let parts = model.parts.map { part in
                      var part = part
                      part.normals = part.normals.map { normals in
                          let transformedNormals: [SIMD3<Float>] = normals.map { _ in
                              [0, -1, 0]
                          }
                          
                          return MeshBuffer(transformedNormals)
                      }
                      
                      return part
                  }
                  model.parts = MeshPartCollection(parts)
                  
                  return model
              }
              contents.models = MeshModelCollection(models)
              try? meshResource.replace(with: contents)
              
              return meshResource
          }
      }
    • 3:02 - Add a volumetric club scene

      WindowGroup(id: "RobotClub") {
          GeometryReader3D { geometry in
              ClubView()
                  .volumeBaseplateVisibility(.visible)
                  .environment(appState)
                  .scaleEffect(geometry.size.width / initialVolumeSize.width)
          }
          .onAppear {
              dismissWindow(id: "RobotCreation")
          }
      }
      .windowStyle(.volumetric)
      .defaultWorldScaling(.dynamic)
      .defaultSize(initialVolumeSize)
    • 3:09 - Add a button to open the club

      VStack {
          Button("🪩") {
              openWindow(id: "RobotClub")
          }
          .padding()
      
          Spacer()
      }
      .padding([.trailing, .top])
    • 6:50 - FIX: Unintentionally inheriting an ancestor's transformation

      discoBall.addChild(background)
    • 10:18 - FIX: Control Center is not being updated

      // 5. Save updated component back to the entity
      controlCenterEntity.components[ControlCenterComponent.self] = controlCenter
    • 18:15 - FIX: Stocking bottles

      private func stockBottles(placementRadius: Float) -> Entity {
          let bottleRadius: Float = 0.003
          let bottleHeight: Float = 0.022
          let angleIncrement: Float = -12
          let outOfStockBrands: Set = [3]
          
          // Make a wrapper entity
          let bottleGroup = Entity()
          bottleGroup.name = "Bottle Group"
          bottleGroup.position = [0, 0.04, 0]
          bottleGroup.orientation = .init(angle: 180 * (.pi / 180), axis: [0, 1, 0])
          
          // Make a nice green material
          var bottleMaterial = PhysicallyBasedMaterial()
          bottleMaterial.baseColor = .init(tint: .green)
          bottleMaterial.blending = .transparent(opacity: .init(floatLiteral: 0.5))
          
          for i in 0..<9 {
              let angle = Angle2D(degrees: angleIncrement * Float(i))
              let bottleMesh = MeshResource.generateCylinder(height: bottleHeight, radius: bottleRadius)
              let bottle = ModelEntity(mesh: bottleMesh, materials: [bottleMaterial])
              bottle.name = "BT\(i)"
              bottle.position = pointOnCircumference(angle: angle, radius: placementRadius, y: bottleHeight / 2)
              if outOfStockBrands.contains(i) {
                  bottle.components[OutOfStockComponent.self] = OutOfStockComponent()
              }
              
              bottleGroup.addChild(bottle)
          }
          
          return bottleGroup
      }
    • 22:48 - FIX: Attractors

      // 4. Untag them as a Newcomer
      visitor.components[Newcomer.self] = nil

Developer Footer

  • ビデオ
  • WWDC24
  • RealityKitデバッガの詳細
  • メニューを開く メニューを閉じる
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    メニューを開く メニューを閉じる
    • アクセシビリティ
    • アクセサリ
    • App Extension
    • App Store
    • オーディオとビデオ(英語)
    • 拡張現実
    • デザイン
    • 配信
    • 教育
    • フォント(英語)
    • ゲーム
    • ヘルスケアとフィットネス
    • アプリ内課金
    • ローカリゼーション
    • マップと位置情報
    • 機械学習
    • オープンソース(英語)
    • セキュリティ
    • SafariとWeb(英語)
    メニューを開く メニューを閉じる
    • 英語ドキュメント(完全版)
    • 日本語ドキュメント(一部トピック)
    • チュートリアル
    • ダウンロード(英語)
    • フォーラム(英語)
    • ビデオ
    Open Menu Close Menu
    • サポートドキュメント
    • お問い合わせ
    • バグ報告
    • システム状況(英語)
    メニューを開く メニューを閉じる
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles(英語)
    • フィードバックアシスタント
    メニューを開く メニューを閉じる
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(英語)
    • News Partner Program(英語)
    • Video Partner Program(英語)
    • セキュリティ報奨金プログラム(英語)
    • Security Research Device Program(英語)
    Open Menu Close Menu
    • Appleに相談
    • Apple Developer Center
    • App Store Awards(英語)
    • Apple Design Awards
    • Apple Developer Academy(英語)
    • WWDC
    Apple Developerアプリを入手する
    Copyright © 2025 Apple Inc. All rights reserved.
    利用規約 プライバシーポリシー 契約とガイドライン