WindowとAndroidでNiantic LightshipのARDK Example Scenesをビルドする

WindowとAndroidでの組み合わせでNiantic light ship ARDKのサンプルシーンをビルドする為の手順を纏めます。
詳しくは公式のガイドを見て頂くのが良いかと思いますが、クイックスタートとして使えるメモ書きがあると便利だなと思い書くことにしました。

ビルド設定以外は駆け足で進めますのでご了承ください。

検証環境

  • Unity 2021.4.34f1
  • ARDK 1.1.0(ardk-1.1.0.unitypackage)
  • ARDK Example Scenes (ardk-examples-1.1.0.unitypackage)
  • Android11 (Xperia 1III)

手順

  1. Unityにて3Dプロジェクトを作成
  2. Build SettingにてPlatformをAndroidにSwitchします。
  3. ardk-1.1.0.unitypackageをImport(メニューより Assets > Import Package)
  4. ardk-examples-1.1.0.unitypackageもImport
  5. サンプルシーンを開く(例:Projectより Meshing > ARDKExamples > ContextAwareness > Meshing)
  6. マルチプレイ機能を使用するサンプルの場合はArdkAuthConfig.assetにライセンスキーを設定する(Meshingなどマルチプレイを伴わないサンプルでは不要)
  7. Build SettingにScenes In Buildにビルドするシーンを追加(既存でシーンが設定されている場合は外す)
  8. Project Settingの Player > Android > Other Setting を以下の様に設定します

f:id:Q1J011:20220126173812p:plain

設定が出来たらビルド! Android端末に入れて動作確認をしてみてください。

書き出してみるとそれほど複雑でもないですし、慣れればさくさく進められると思います。
公式のマニュアルも丁寧に書かれているので疑問点があればぜひ参考にしてみてください。

lightship.dev

GUIにおける状態管理の考え方について

はじめに

エンジニアの松原です。業務以前に個人開発でUnityを触っていたのですが、まずはサードパーティ製ライブラリを取り込んでから開発がスタートするのが多かったと思います。(分かりやすい例として、Oculus IntegrationUltraleap Plugin For Unityを真っ先に入れていた気がしています)

当時の個人開発のアプリではあまりアプリケーションの設計を考えておらず、UnityのScene間でデータをやり取りすることが少なく、何かしらの処理は理由もなく多数のクラスや関数に依存していました。 業務でUnityを使った開発をするようになってからは、サーバーサイドのAPIと連携するようになったため、個人開発でもある程度設計を意識するようになりました。

今回は設計のうち、GUIにおける状態管理に対する考えのパターンについて記事にしてみました。(Unityの話だけでなく、Webフロントエンド開発で使われているトピックも入ってきます)Unityで実際にコードを書くことに関してはまた別の機会で行いたいと思います。

今回の記事の注意として、UnityのAnimator Controllerなどで登場するStateはFSM(Finite State Machine)と呼ばれるステートマシンで、同じ状態を表していますが、扱うトピックが異なるため、今回は登場しません。

なぜGUIに状態管理が必要なのか

Unityのみならず、GUIを扱う場合、以下の課題に遭遇することが多いと思います。

  • ステートレスなサーバーサイドAPIとやり取りした結果をGUIに反映したい
  • 複数のUIのコンポーネントやクラス間で共有したいプロパティがある
  • 画面の切り替え(またはUnityのシーン)の切り替え時に変更したプロパティを維持したい

これらの課題を応急処置的なコードを使って開発をすすめていくと、複雑な依存関係が増えてきます。依存関係が増えるということは状態の扱いが難しくなり、設計の変更も容易でなくなってきます。

そこで状態を管理しているオブジェクトをいくつかの設計パターンに切り出して考えてみます。

状態管理を外部オブジェクトとして設置する(Storeパターン)

共有化したい状態をStoreというグローバルなオブジェクトを利用して管理する方法を紹介します。 こちらのパターンはWebフロントエンド開発のJavaScriptフレームワークであるVueの公式ページで紹介されている状態管理のパターンを参考にしたものになります。

jp.vuejs.org

Vue公式の方ではObserverの記述は省略されているのですが、状態管理にリアクティブなプロパティが利用できる、もしくはStateを監視する仕組みをVueでは暗黙的に利用することが多く、JavaScriptフレームワーク以外で一般的に説明するためObserverを書き加えています。

f:id:fb8r5jymw6fd:20220124005617j:plain:h500

上の図に追加して、State書き換え時の処理ギミックを加えたものが以下の図になります。ここのギミックに関しては実装方針でも変わってきますが、最低限イメージしやすい内容にしています。

初期値の設定まわりは省略していますが、Stateの更新サイクルとしては以下のようになります。

  1. GUIパーツの操作(ボタン押下、スライドの値変更時など)に紐づいたトリガーが発火時、そのトリガーに対応する処理が呼び出される
  2. 処理が実行され、Stateの変更がある場合はStateの変更処理を行う命令を呼び出す
  3. Stateの変更命令を受け取り、保持しているStateを書き換える
  4. Stateの書き換えを検知する
  5. 変更されたStateに依存しているPresenterやViewに対して変更を通知する

f:id:fb8r5jymw6fd:20220124173631j:plain:h500

上記で登場したPresenterやViewは MVC / MVP パターンで登場する概念ですが、Unityで分かりやすい例えを考えてみます。
UnityではuGUIを利用してGUIを組むことが一般的だとは思いますが、Store内でStateが更新されても、画面上に反映するためにはGUIコンポーネントの表示部をスクリプトから更新する必要があります。

下図のTextコンポーネントも同様で、このコンポーネントのTextフィールドに対して直接更新処理をかけないと画面上に変更が反映されません。このように、特定のStateに依存しており、Stateの変更に伴ってState変更処理以外の手続きを行う必要がある部分の実装をViewと捉えると分かりやすいかと思います。このStateの書き換え後のギミックを仲介する役割がObserverとなります。

f:id:fb8r5jymw6fd:20220124175051j:plain:h500

状態管理を行うモダンなアーキテクチャを考える(Fluxパターン)

上記のように状態管理をしつつ、処理の依存関係を極力減らすために、処理の構造を単方向にするFluxというコンセプト(またはアーキテクチャ)があります。 現在はFacebookのGitHubにアーカイブとして残っています。このコンセプトを基に実装・派生したReduxMobXを使うことをFacebookは推奨しています。

github.com

Fluxのコンセプトの実装例として以下のようにまとめることができます。先ほど紹介したStoreパターンではStateの書き換えは外部から参照、命令を呼び出すことで直接書き換えができていましたが、こちらは役割をさらに細分化して処理を単方向に扱えるようにしています。 それぞれ以下の役割を持っています。

  • Action - 具体的な処理を記述するためのパターンで、処理単位のテンプレートとして定義されている。成功や失敗などの状態変更の起点を持つことができる
  • Action Creator - Actionを作成する責務を持つ
  • Dispatcher - Action Creatorから作られたActionをReducerに送る責務を持つ(実装によっては省略されることもある)
  • Reducer - Actionを実際に実行し、Stateを書き換える責務を持つ。StateはReducerからでしか書き換えができない
  • State - 保持したい情報の単位または入れ物
  • Watcher(Observer) - Stateの更新を監視し、依存しているコンポーネントに更新を通知する

f:id:fb8r5jymw6fd:20220124192212j:plain:h500

Fluxの実装によっては定義されている言葉が違うことがありますが、かねがねそれぞれの役割としては上記のようになっていることが多いと思います。 Fluxではより役割を厳格に定めることによって、双方向の依存関係を無くすように設計されたアーキテクチャであるといえると思います。

今回のまとめ

今回はWebフロントエンドで登場する状態管理の考え方をJavaScriptフレームワークで利用されている状態管理のパターンを取り上げました。

これらを実際にUnityで扱うにはUniduxなどがありますが、こちらは画面遷移も取り扱っているため、今回はGUIでの状態管理について解説したかったので、また別の機会に紹介したいと思います。 github.com

また、StoreパターンやFluxパターンは MVC / MVP や MVVM のようにアプリケーションの設計に深く根付いているというより、状態管理という責務分担が難しいものをうまくやりくりするためのコンセプトのようなので、派生形のReduxやMobXもどこかの機会で追ってみたいと思います。

Apple Watch(+α)のxR開発で使う座標系を調べた

エンジニアの岡村です。

先月、Synamon初のアドベントカレンダー企画を行いました(無事完走!)。その中で自分は仕事で触っていない技術へのチャレンジを行い、ほぼ初めてのSwiftとApple系デバイスを使った開発を行いました。

synamon.hatenablog.com

自分はUnityやDirectX以外の座標系をあまり触ったことがなく、開発の際に座標系に少し戸惑ったので、この記事に軽く調べてみた情報を纏めておきます。

座標系の種類

座標系には大きく分けて右手系と左手系が存在します。それぞれの座標系は、親指をX軸、人差し指をY軸、中指をZ軸とし、各指が90度で直交する形を作り、回転することでその形と一致するかどうかで確かめることができます。これらの座標系が異なる場合、回転により一致させることが出来ず、相互変換する際に座標の解釈を変更する必要があります。

例えば、Unityの座標系は左手系であり、Blenderの座標系は右手系です。BlenderとUnityを使ってコンテンツを作成している方は、いつもこの座標系の違いを乗り越えているんですね。

また、座標系にはどの軸を上方向とするのか(Y-up、Z-up)という基準もあります。しかし、デバイスの座標系を考えたとき、机に置いた状態の上方向と、立てて使用している時の上方向など、その瞬間におけるデバイスの状態によって解釈が異なる為、この基準を簡単に適用することはできません。代わりに、デバイスのどの方向が何軸に対応しているのかを明示しておきます。

Apple系デバイスの座標系

Apple系デバイスのネイティブアプリを開発する際、モーションセンサーの値を取得するにはCoreMotionというフレームワークを利用します。このフレームワーク内において利用されている座標系は右手系となっており、これは公式ドキュメント内のこちらのページで確認することが出来ます。

https://developer.apple.com/documentation/coremotion/getting_processed_device-motion_data/understanding_reference_frames_and_device_attitude

また、この図によると、ディスプレイ正面方向がZ+ディスプレイ上方向がY+となっており、これが画面が付いてるデバイスにおける標準となっているようです。

f:id:Sokuhatiku:20220117181330p:plain:w300

Apple Watch

Apple Watchでも基本的には同じです。ただし、Apple Watchは左右どちらの手首に装着するか、Digital Clownの向きを左右どちらにするかで4通りの装着方法が考えられます。

f:id:Sokuhatiku:20220117191956p:plain:w300

ですが、どの向きでもディスプレイ正面がZ+Digital Clownのある方向がX+となっています。Digital Clownの向きを変えると画面の上下が反転しますが、CoreMotionにおける軸方向は変化しません。つまり左にDigital Clownが来るような状態にした場合、腕を下に下げるとデバイスのY+方向に加速度がかかることになります。

f:id:Sokuhatiku:20220117193118p:plain

ちなみに

今まで画面が付いているデバイスの話をしていましたが、画面がついていないAirPodsもCoreMotionでモーションセンサーの値を取得出来ます。ただし、AirPodsの場合は、回転量の差分のみが取得可能なようです(3DoF)*1。この場合はデバイスの面と軸の対応を考えることは出来ません。

おまけ(Android)

ついでにAndroidにおけるセンサーの座標系も調べてみました。公式情報はこちらのページにあります。 https://developer.android.com/guide/topics/sensors/sensors_overview?hl=ja#sensors-coords

f:id:Sokuhatiku:20220117193512p:plain:w300

座標系はAppleと同様に右手系ディスプレイ正面がZ+ディスプレイ上がY+ となっているようです。

おわりに

基本的にUnity等のゲームエンジンを使っている限り、それ用に用意されたライブラリが座標系を合わせてくれるのが凄く有難いです。しかし、何らかの事情でネイティブ開発をする必要が出てきた場合、座標系の違いは(特にドキュメントが充実していない場合)大変になるでしょう。それに備えて知識だけは持っておきたいと思います。

Unity2021でVRテンプレートを使ってOculusQuest2の開発環境を構築してみる

こんにちは、Synamon のエンジニアリングマネージャーの渡辺(@mochi_neko_7)です。

自分はエンジニアではあるのですが、肩書きにもあるようにここ1~2年くらいは開発以外の仕事(EM、採用)をすることがほとんどで、実は最近のバージョンのUnityを触れていませんでした。

ですのでリハビリも兼ねて、最新のUnity正式版であるUnity 2021の環境でVRテンプレートを使用して、VRの開発環境をゼロから構築してみたいと思います。

読者の対象としては、以下を想定しています。

  • Unityは触ったことあるが、VRの開発は初心者の方
  • VRの開発をここ1~2年くらい触っていない方

今回の開発環境は以下で構築していますが、バージョンに依って内容に変更があり得ることにご注意ください。

  • Windows 10
  • Unity 2021.2.7f1

ちなみに今回はデバッグのしやすいOculus Linkも使用するのでWindowsを使用していますが、 Quest2(Android)のみで動作確認する場合はMac等でも開発可能かと思います。

またUnity自体の基本的な説明、Unity以外のソフトウェアの設定、ハードウェアに関する説明などは割愛させていただきます。

Unityのインストール

まずはUnity Hubを使ってUnityのインストールをします。

unity3d.com

Unity Hubでは、インストール > インストール(右上の青いボタン) > バージョンを選択 > 追加するモジュールを選択 します。

ここで使用するUnityのバージョンは、2022/01/11で最新の正式版である Unity 2021.2.7f1 とします。(LTSではないです)

念のためですが、実際に製品開発をされる方は (LTS) の付くバージョンを使用することが推奨されていますのでご注意ください。

f:id:mochinekos:20220111104604p:plain
Unityのインストール

追加するモジュールでは以下を選択しておきます。

  • Android Build Support
    • Android SDK & NDK Tools
    • OpenJDK
  • Windows Build Support (IL2CPP)

特に前者のAndroid向けのモジュールは最終的にOculus Quest 2単体で動作させたい場合には必須になります。

f:id:mochinekos:20220111105505p:plain

プロジェクトの作成

Unity Hubでプロジェクトの新規作成を行います。

プロジェクト > 新規作成(右上の青いボタン、の右の▼のボタン) > 2021.2.7f1

右の▼からバージョンを指定するのにご注意ください。

プロジェクト名、保存先は好きに変えてください。

ここで、テンプレートでVRを選択しましょう。(初めての場合はダウンロードが必要です)

f:id:mochinekos:20220111110841p:plain

このVRのテンプレートはVR開発向けに必要なUnity Packageの追加やプロジェクトの設定が適用されているものになります。

これを使用することでそれらの初期設定の手間を省いて、手軽にプロジェクトの作成ができるようになっています。

f:id:mochinekos:20220111111820p:plain

Unityパッケージの確認

画面上のタブ > Window > Package Manager

からPackage Managerを開きます。

VRのテンプレートではXR開発向けのパッケージがデフォルトで追加されているようです。

  • Oculus XR Plugin
  • Windows XR Plugin
  • XR Plugin Management

f:id:mochinekos:20220111112051p:plain

それぞれのバージョンは最新に更新しておいて基本的には問題ないと思いますので、以下のバージョンを使用します。

  • Oculus XR Plugin > 1.11.2
  • Windows XR Plugin > 4.4.1
  • XR Plugin Management > 4.2.1

プロジェクトの設定

今回使用するOculus Quest 2向けに設定を少しだけします。

画面上のタブ > Project Settings > XR Plug-in Management

を開きます。

Oculus Linkを使用するためには、PC向けの設定で Oculus にチェックを入れます。

f:id:mochinekos:20220111113339p:plain

Quest2単体で動かすためには、Android向けの設定で Oculus にチェックを入れます。

f:id:mochinekos:20220111113507p:plain

Editor上で動作確認

Oculus Linkを使って、Editor上で動作確認してみましょう。

Oculus Linkのセットアップはまず公式のこちらをご覧ください。

support.oculus.com

使用するOculus Quest 2は開発者モードに設定しておきましょう。

https://developer.oculus.com/documentation/native/android/mobile-device-setup/?locale=ja_JP

VRテンプレートにはサンプルである SampleScene があり、初回起動時に展開されているのでこちらを使ってみましょう。

Linkを起動した状態で、Unityで再生をします。

うまくつながっている場合、このように白い空間で頭と両手のトラッキングがされているのが分かります。

f:id:mochinekos:20220111140855p:plain

ビルド

同じサンプルシーンが正常にビルドでき、実機で動作することを確認してみましょう。 今回は2通りの方法で動作確認します。

  • Windows向け(Oculus Link)
  • Android向け(Quest2単体)

Windows向け(Oculus Link)

画面上のタブ > File > BuildSettings を開き、Add Open Scenes を押して SampleScene を追加して、 Windowのプラットフォームになっていることを確認してから、Build を押します。

エラーが発生しなければビルド成功です。

指定したフォルダの プロジェクト名.exe のファイルを起動すると、先ほどのEditorと同じ動作をするアプリケーションが立ち上がるかと思います。

Android向け(Quest2単体)

同じくBuild Settingsから、一度PlatformをAndroidに変更します。

Platform > AndroidTexture Compression を推奨の ASTC に変更しておき、それから画面下の Switch Platform を押します。

それから Build もしくは Build And Run を押します。

確認方法はいくつかあるのですがどれでも構いません。

  • Build And Run でビルド後に直接実機で動かす
  • Build してできた .apk ファイルを
    • Side Questでインストールする
    • Android Studioを入れてADBコマンドでインストールする

サンプルがちゃんと動いてトラッキングがうまくされている、画面がちゃんと表示されているのが確認できればOKです。

コントローラーの入力の取得方法

SampleScene を見ると、XR Rig というオブジェクトの下に Camera OffsetRightControllerLeftControllerの3つの子オブジェクトがあり、これらが頭・両手のコントローラーのトラッキングを再現していることが分かります。

つまり、既にトラッキングの情報は取得できています。

後はQuest2のコントローラーの入力の取得さえできれば、最低限基本的な開発ができるでしょう。

こちらの記事

synamon.hatenablog.com

にもあるように、Unityの(New)Input SystemではOculus Quest 2のコントローラーの入力も取得することができます。

詳しくはこちらの記事を参考にしてください。

jmpelletier.com

ちなみに 入力イベントを取得する Player Input でのイベントの戻り値は UnityEngine.InputSystem.InputAction.CallbackContext のようです。

まとめ

以上でできるようになったことをまとめます。

  • 動作
    • UnityのEditor上で、Oculus Linkで動かす
    • Windowビルドして、Oculus Linkで動かす
    • Androidビルドして、Quest2単体で動かす
  • 入力
    • 頭と両手のトラッキングができる
    • コントローラーのボタン等の入力が取れる

ここまで来れば、後は作りたいコンテンツを作るだけですね。

さらっとここまで来ていますが、1~2年前までは他にも

  • Oculus IntegrationなどのSDKをインストールする
  • Android SDKまわりを手動でセットアップしてビルドを通す
  • Oculus Quest 2向けのビルド設定を一つずつ修正する

などをしながら実機で動かすところまでやる必要があり、地味に大変でした。

それが今では、

  • XR Plugin Management & New Input System → OculusのSDKまわりをうまくラップしてくれている
  • Android向け追加モジュール → Android SDKのセットアップまわりをうまくやってくれている
  • VR Template → Quest2向けの細かいセットアップを自動でやってくれている

のように整備されていて、開発環境を構築するまでのハードルがかなり下がっていることが分かります。

また、基本的にQuest2向けに設定したのは XR Plugin Management の部分と、コントローラーの取得の部分だけなので、XR Plugin Managementが対応している範囲なら他のHMDもほぼ同じように対応できると思います。(確認はしてませんがOpenXRの設定欄もありますし)

あと今回はOculus Integrationを自分では入れていないので、このままGitHubに上げてもライセンス上問題ないはずで、GitHubでPublicに開発するハードルも下がるかと思います。

その他、注意事項としては以下を挙げておきます。

  • Unity2021のバージョンはまだLTSではないので、今プロダクションで使用するならUnity2020.3が推奨
  • ちゃんと確認したわけではないのですが、Oculusの最新のSDKを使用したいなら、おそらく手動でOculus Integrationを入れる必要がありそう
  • これも時間がなくて確認できていないのですが、Unityの提供しているUnity Package Oculus XR Plugin がOculusの公式のSDKである Oculus Integration とどうバージョン対応しているのかわからず
    • (2022/01/17追記)新しめのバージョンでは、Oculus Integrationのv33には対応しているようなので、それ以上新しいバージョンを使用したい場合のみ手動でどうにかする必要がありそうです

以上、これからOculus Quest 2を使って新しく開発を始めてみたい方に参考になれば幸いです。

Synamon Advent Calendar 2021完走しました!

あけましておめでとうございます! 🎍
エンジニアリングマネージャーの佐藤(@unsoluble_sugar)です。

新年1記事目ということで、年の瀬のアドベントカレンダーについて振り返ってみます。

アドベントカレンダー戦績発表

2021年12月に実施したSynamon Advent Calendar 2021ですが、見事に完走いたしました!!

f:id:unsoluble_sugar:20220102155436j:plain

なんと、Synamonとしてのアドベントカレンダー初挑戦にもかかわらず、全25記事すべて埋まるという快挙…!

参加していただいたエンジニア、デザイナーメンバー、そして記事のシェアに協力いただいた皆さんの素晴らしい成果です!👏👏👏

記事一覧

せっかくなので本記事でもアドベントカレンダーに投稿された25記事の一覧を列挙しておきます。気になる記事があればぜひ読んでください!

  1. テックブログ運用を回すための取り組み 〜黄金の回転編〜
  2. Unityでのマルチプレイアプリ開発を便利にしてくれるEditor拡張-ParrelSyncを深ぼってみる
  3. Oculus GoのアンロックOSビルドを入れてみた
  4. Netcode for GameObjectsの機能紹介 NetworkTransformコンポーネントについて
  5. Blenderでオブジェクトを統合するとテクスチャが表示されなくなる問題
  6. ラブライブに学ぶリーダーシップ
  7. だれでもできる!Nreal Lightアプリ作成入門
  8. VR企業はこうやって引っ越しをする!!
  9. AR Foundationでユニティちゃん召喚スマホアプリを作ってみた
  10. HubsCloudをDocker環境で動かしてみる
  11. Netcode for GameObjectsでプレイヤー間で同期する物体の生成 / 破棄
  12. NaughtyAttributesの実装を読み解く
  13. VRoid Studioでメタバース時代のオリジナルアバターを自作したい!
  14. HaritoraX完全に理解した
  15. AppleWatchをハンドトラッキング補助デバイスとして使えないか試してみた
  16. .NET 6 MAUI(Preview)をWindowsとMacで触ってみたかった
  17. インフラ周りで直面した課題と対応策について登壇しました
  18. Netcode for GameObjectsでMobをスポーンさせる
  19. 3Dモデリング未経験のエンジニアがBlender入門してみた
  20. PlayMakerとBehaviourTreeを使ってステートマシンにイベントハンドリングをできるようにしてみた
  21. Addressable Asset System入門
  22. Bledner上での確認にLooking Glass Portraitを使う
  23. Houdini Engine For Unityを色々試してみる
  24. SIer経験者がXR業界に入って感じたこと
  25. 修正に強いモデリング方法

「ネタ記事でも大丈夫ですよ!」とは言っていたものの、皆さんわりと真面目な記事を書いてくださった印象ですw

皆さんの得意領域や今回の機会を利用したチャレンジを見ることができて個人的にも嬉しいです!

掲載媒体の内訳について

今回はアドベントカレンダー初挑戦かつ、まだテックブログに書いたことのないメンバーも居たこともあり「Qiitaやnoteなど他媒体でも書いてOK」という体制を取りました。純粋にテックブログ向きではないテーマもありますからね。

投稿媒体の内訳は以下のとおり。

  • テックブログ:15記事
  • note:6記事
  • Qiita:3記事
  • Zenn:1記事

Synamon公式noteにはデザイナーマガジンがあるため、デザイナーさんはこちらに掲載いただいたケースもありました。エンジニア以外に向けた情報発信はnoteが適していますね。

note.synamon.jp

それでも25記事中、15記事もテックブログで書かれたのは好成績です。12月のPV数も年間最高値が出ていました。

f:id:unsoluble_sugar:20220102183154j:plain

右肩上がりで気持ちの良いグラフですね!
公開した記事数自体が多く、目にしていただける機会も一段と多かったようで何よりです。

2022年もよろしくお願いします

昨年の後半から体制を整えだしたテックブログ運営ですが、アドベントカレンダーも無事に完走を果たすことができました。参加いただいた皆さん全員がバリューを体現したと言えるでしょう。

f:id:unsoluble_sugar:20220102171135j:plain

今回書かれた記事はストック型の技術情報として、今後も長く読まれることを期待しています。

Synamonで「技術的にどのような取り組みを行なっているか」「どんなことに興味のあるメンバーが在籍しているか」アドベントカレンダーの記事を通して、少しでも多くの方に伝われば幸いです。

synamon.hatenablog.com

2022年もSynamon’s Engineer blogをよろしくお願いいたします!

最後に

本テックブログやnote記事のお知らせは、Synamon公式Twitterで発信しています。弊社の取り組みに興味を持っていただけましたらぜひフォローお願いします!

twitter.com

カジュアル面談も実施中ですので「詳しく話を聞いてみたい!」という方はチェックいただけると嬉しいです。

▼カジュアル面談はMeetyから   meety.net  

▼エントリーはこちら   herp.careers

UnityのTimelineを利用してGUIのトランジション効果を作ってみる

f:id:fb8r5jymw6fd:20211226110009p:plain

はじめに

UnityでGUIを構築する際、uGUIや新しい機能であるUI Toolkitが利用できますが、画面遷移のアニメーションのふるまいなどはユーザー側に任せられています。ユーザーで用意するぶん、いくらでもカスタマイズできますが、ギミックやアニメーションの処理を用意するのに手間な部分でもあります。

私はUnityのTimelineについて触れる機会が少なく、これまであまり詳しいことが分かっていませんでしたが、Timelineはスクリプトからの操作やプロパティの変更ができる箇所も多く、カットシーンやイベントシーン以外の用途でもTimelineが利用できそうということが分かりました。今回その一例としてGUIの画面遷移(トランジション)の処理をTimelineを使って行うことを記事に書きました。

今回紹介する内容は個人のGitHubにサンプルのプロジェクトファイルを置いていますので、触っていただけると嬉しいです。

事前準備

今回の記事では以下のUnityのバージョンとパッケージを利用しています。

  • Unity 2020.3.25f1
  • Timeline 1.4.8
  • Input System 1.2.0
  • Unity UI 1.0.0

対象のGUIパネルの作成とAnimatorコンポーネントの追加

トランジション効果を持たせたいパネルを作成し、それぞれAnimatorコンポーネントを追加します。これはAnimationとして動かす際に必要になります。Animator ControllerやAvatarはNoneのままにしておきます。

f:id:fb8r5jymw6fd:20211226095217p:plain

 

また、以下のセクションで登場するAnimationはTransformを持つGameObjectから作成する必要があり、RectTransformとTransformの原点座標を揃える必要があります。GUIパネルとして利用したいGameObjectのRectTransformの以下のように設定します。

Anchor Presetsは下図のように[Middle Center]にします。(AnchorsのPivotは x: 0.5, y: 0.5に設定)他設定は上図のAnchorsを参考にしてください。

f:id:fb8r5jymw6fd:20211226095030p:plain

 

Animationを作成する

まずは遷移元になるAnimationと遷移先になるAnimationをそれぞれ作成します。Projectパネルから任意のディレクトリで右クリック、 Create > Animation から2つほどAnimationを作成します。

f:id:fb8r5jymw6fd:20211225173855p:plain

 

それぞれのAnimationに適当な名前を付けたら、Hierarchyに新しくGameObjectを2つ分用意し、先ほど作成したAnimationをD&Dで追加します。

f:id:fb8r5jymw6fd:20211226112331p:plain

 

Projectパネルから作成したAnimationの一つをダブルクリックしてアニメーションを編集します。

f:id:fb8r5jymw6fd:20211225175957p:plain

 

ここで一度Hierarchyから対象のAnimationが追加されているGameObjectを選択します。この操作をしないと右側のAdd Propertyのボタンが押せないので注意してください。

続けてAdd Propertyを押します。

f:id:fb8r5jymw6fd:20211225180325p:plain


追加するプロパティは Transform > Position で、Transformの「▷」マークを押すことで候補が出てきます。右側の「+」マークを押して追加します。

f:id:fb8r5jymw6fd:20211225180711p:plain

 

デフォルトでキーフレームが0:00の箇所と1:00(1秒相当)の箇所に入っていますので、末尾のキーフレームのマーカーをドラッグしてを0:30の位置に動かします。(ここは任意の時間でも大丈夫です)

f:id:fb8r5jymw6fd:20211225180905p:plain

 

キーフレームを上書き編集するため、Recordingボタンを押します。

f:id:fb8r5jymw6fd:20211225181057p:plain

 

一つの目のアニメーションは画面外にスライドするアニメーションを作るので、最後のキーフレームを選択します。

f:id:fb8r5jymw6fd:20211225181328p:plain

 

横の数値はCanvas Scalerの画面幅を基準に設定します。左側へのスライドのため、マイナスの数値が入っています。下記数値と異なる場合は自身のCanvasScalerの数値を基準に入れてください。

f:id:fb8r5jymw6fd:20211225181529p:plain

f:id:fb8r5jymw6fd:20211225181800p:plain

 

最初のキーフレームの値と最後のキーフレームの値が以下のようになっていればOKです。

f:id:fb8r5jymw6fd:20211225182301p:plain

 

もう一方の方は右側の画面外からフレームインするように、終わりのキーフレームの位置を0にするように設定します。

f:id:fb8r5jymw6fd:20211225184140p:plain

 

編集が終わったら、使用したGameObjectと、自動生成されたAnimator Controllerは不要になりますので削除します。

f:id:fb8r5jymw6fd:20211225184415p:plain

f:id:fb8r5jymw6fd:20211225184548p:plain

 

Timelineを作成する

次にTimelineを作成します。Projectパネル上で右クリックし、 Create > Timelineを選択します。

f:id:fb8r5jymw6fd:20211225184649p:plain

 

名前をわかりやすいものに変更しておきます。

f:id:fb8r5jymw6fd:20211226103050p:plain

 

Hierarchyに空のGameObjectを作成し、そのGameObjectにPlayableDirectorのコンポーネントを追加します。

f:id:fb8r5jymw6fd:20211226065048p:plain

 

Inspectorから先ほど作成したTimelineをPlayableDirectorのPlayableの箇所にD&Dします。ついでにPlay On Awakeのチェックも外して、実行時自動的にTimelineが再生されないようにします。

f:id:fb8r5jymw6fd:20211226065423p:plain

 

さきほど作成したTimelineをダブルクリックしてTimelineのパネルを開きます。

f:id:fb8r5jymw6fd:20211226064821p:plain

 

Timelineのパネルが開いたら、一度HierarchyのPlayableDirectorをアタッチしたGameObjectを選択し、Timelineパネルをロックしておきます。

f:id:fb8r5jymw6fd:20211226065725p:plain

f:id:fb8r5jymw6fd:20211226065855p:plain

 

Animation Trackを作成する

Timelineの左側のところで右クリックし、メニューからAnimation Trackを二つ用意します。それぞれ遷移元のGUIパネルのアニメーション、遷移先のGUIパネルのアニメーション用に利用します。

f:id:fb8r5jymw6fd:20211226065944p:plain

 

作成したAnimation Trackをリネームします。Timelineの画面上に追加された項目の、枠の部分もしくは、下図のようにアイコンの部分をクリックすることで選択できます。

f:id:fb8r5jymw6fd:20211226070353p:plain

 

リネームはInspectorから行います。ついでにApply Avatar Maskの箇所のチェックを外します。

f:id:fb8r5jymw6fd:20211226070547p:plain

 

もう一方のAnimation Trackも同じようにリネームします。

f:id:fb8r5jymw6fd:20211226070649p:plain

 

AnimationTrackにAnimationを追加する

それぞれのAnimationTrackに作成していたAnimationを追加します。Animationは任意の位置に追加すればよいですが、今回の画面遷移の仕組みではアニメーションの開始と終了がそろっていた方が良いです。

マウスホイールを使ってTimelineの縮尺を変えることができるので、追加しづらいときは縮尺を変えてみてください。

f:id:fb8r5jymw6fd:20211226071107p:plain

 

追加したTimelineパネル上のAnimationをクリックし、Inspectorから操作を行います。

f:id:fb8r5jymw6fd:20211226071359p:plain

 

Animation Playable Asset内のにある、Remove Start Offsetの項目のチェックを外します。Remove Start Offsetのチェックを外しておくことで、Animationに設定したPositionのアニメーションが絶対位置として反映されるようになります。今回の仕組みには必須です。

Remove Start Offsetの項目は通常のTransformを持ったGameObjectから作ったAnimationのみ利用できます。GUIのようにRectTransformを持ったものからAnimationを作るとこの項目は表示されないので注意してください。*1

f:id:fb8r5jymw6fd:20211226072507p:plain

もう一方のTimeline上に配置したAnimationも同じようにRemove Start Offsetのチェックを外しておきます。

 

アニメーションをチェックする

一度アニメーションの挙動を確認してみます。HierarchyのPlayableDirectorをアタッチしたGameObjectを選択し、Inspectorを操作します。
f:id:fb8r5jymw6fd:20211226065725p:plain

 

PlayableDirectorコンポーネントのBindingsの項目にAnimationTrackが追加されています。それぞれの箇所にHierarhy側のトランジションさせたいGUIを持つGameObjectをセットします。

f:id:fb8r5jymw6fd:20211226073320p:plain

 

Timelineパネルから再生ボタンを押すか、数字目盛の箇所をドラッグして動きを確認します。

f:id:fb8r5jymw6fd:20211226074032p:plain

f:id:fb8r5jymw6fd:20211226134942g:plain

 

うまく動かないときは、Animationの確認、AnimationTrackの確認、Previewのボタンを押したり、パネルのロックを外して再選択してみるなどの操作を行ってください。

f:id:fb8r5jymw6fd:20211226073846p:plain

 

確認が終わったらPlayableDirectorコンポーネントのBindingsから先ほどセットしたGameObjectを外し、Noneに戻しておきます。

f:id:fb8r5jymw6fd:20211226074955p:plain

 

SignalAssetを作成する

次にSignalAssetを作成します。このSignalAssetはTimeline上での再生位置、再生終了の検出のために利用します。Projectパネル上で右クリックし、 Create > Signal を選択しSignalAssetを作成します。

f:id:fb8r5jymw6fd:20211226075220p:plain

 

二つぶん作り、それぞれ 「Start~」、「End~」という感じに名前を付けます。これはトランジション開始、終了をそれぞれ表すためにつけています。このルールは後で必要になってきます。

f:id:fb8r5jymw6fd:20211226075637p:plain

 

次にTimelineにこのSignalAssetをマーカーとして登録します。

マーカーを見えるようにするため、Timelineの数字目盛の箇所を右クリックし、 Show makersを選択し、Markersの項目を表示します。

f:id:fb8r5jymw6fd:20211226082002p:plain

 

SignalAssetを追加し、マーカーとして登録します。それぞれ遷移アニメーションの開始位置、終了位置にマーカーを配置します。

f:id:fb8r5jymw6fd:20211226082517p:plain

 

SignalReceiverコンポーネントを追加する

PlayableDirectorのComponentが追加されているGameObjectにSignalReceiverコンポーネントを追加しておきます。SignalReiverコンポーネントは追加するのみで、このコンポーネントに対してはInspectorからは何も操作しません。(後でスクリプトで操作できるようにするためです)

f:id:fb8r5jymw6fd:20211226082937p:plain

 

トランジションを行うためのスクリプトを追加する

さらに以下のようなスクリプトを作成し、PlayableDirector、SignalReceiverを持っているGameObjectにコンポーネントに追加します。サンプルではMonoBehaviour継承クラスで、TimelineTransitionという名前にしています。

f:id:fb8r5jymw6fd:20211226091726p:plain

 

ソースコード全体はこんな感じです。今回本文が長いため、ソースコードの解説は割愛させて頂きます。StartTransition()のメソッドからトランジション効果を呼び出すことができます。

ギミック自体はとても面白いので、Playable APIを含めて今後別の記事で紹介できるかもしれません。

#nullable enable

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Playables;
using UnityEngine.Timeline;

[RequireComponent(typeof(PlayableDirector), typeof(SignalAsset))]
public class TimelineTransition : MonoBehaviour
{
[SerializeField]
private PlayableDirector? playableDirector;
[SerializeField]
private string startMarkerPrefix = "Start";
[SerializeField]
private string stopMarkerPrefix = "End";
[SerializeField]
private string sourceTrackName = "TransitionA";
[SerializeField]
private string destinationTrackName = "TransitionB";

private readonly Dictionary<string, double> startMarkerTimes = new Dictionary<string, double>();

private TrackAsset? sourceTrackAsset;
private TrackAsset? destinationTrackAsset;

public IReadOnlyList<string>? StartMarkers => startMarkerTimes.Keys.Any() ? startMarkerTimes.Keys.ToList() : null;

private GameObject? targetObjectToDisable;

public void StartTransition(
Animator? sourceAnimator,
Animator? destinationAnimator,
string transitionName,
bool disableSourceAfterTransition = false,
bool enableDestinationBeforeTransition = false)
{
if (playableDirector == null) return;
if (playableDirector.state == PlayState.Playing) return;

if (sourceTrackAsset != null && sourceAnimator != null)
{
targetObjectToDisable = disableSourceAfterTransition ? sourceAnimator.gameObject : null;
playableDirector.SetGenericBinding(sourceTrackAsset, sourceAnimator);
}

if (destinationTrackAsset != null && destinationAnimator != null)
{
if (enableDestinationBeforeTransition)
{
destinationAnimator.gameObject.SetActive(true);
}
playableDirector.SetGenericBinding(destinationTrackAsset, destinationAnimator);
}

StartAnimationAtMarker(transitionName);
}

private void StartAnimationAtMarker(string markerName)
{
if (startMarkerTimes.ContainsKey(markerName))
{
if (playableDirector == null) return;
playableDirector.time = startMarkerTimes[markerName];
playableDirector.Play();
}
}

#region MonoBehaviour implements

private void Reset()
{
playableDirector = gameObject.GetComponent<PlayableDirector>();
}

private void Awake()
{
if (playableDirector == null) return;

var stopTimelineEvent = new UnityEvent();
stopTimelineEvent.AddListener(() =>
{
targetObjectToDisable?.SetActive(false);
targetObjectToDisable = null;
playableDirector?.Stop();
});

var signalReceiver = gameObject.GetComponent<SignalReceiver>();

var timelineAsset = playableDirector?.playableAsset as TimelineAsset;
foreach (var marker in timelineAsset!.markerTrack.GetMarkers())
{
var signalEmitter = marker as SignalEmitter;
if (signalEmitter == null) continue;

var signalAsset = signalEmitter.asset;
if (signalAsset.name.Contains(startMarkerPrefix))
{
startMarkerTimes.Add(signalAsset.name, marker.time);
}

if (signalAsset.name.Contains(stopMarkerPrefix))
{
if (!signalReceiver.GetRegisteredSignals().Contains(signalAsset))
{
signalReceiver.AddReaction(signalAsset, stopTimelineEvent);
}
}
}

foreach (var trackAsset in timelineAsset!.GetRootTracks())
{
if (trackAsset as AnimationTrack == null) continue;
if (trackAsset.name.Contains(sourceTrackName))
{
sourceTrackAsset = trackAsset;
}

if (trackAsset.name.Contains(destinationTrackName))
{
destinationTrackAsset = trackAsset;
}
}
}

#endregion
}

 

それぞれのプロパティは下図のような関係持っています。

Start Maker Prefixはアニメーション開始のマーカーを識別するためのキーワード(接頭辞)、Stop Marker Prefixはアニメーション停止のマーカーを識別するためのキーワードとして設定します。このマーカーはSignalAssetの名前を基準にして行っています。

SourceTrackNameとDestinationTrackNameはそれぞれアニメーションをさせたいAnimationTrackの名前を文字列で指定しています。

f:id:fb8r5jymw6fd:20211226094152p:plain

 

トランジション効果を呼び出す処理を追加する

各GUIのボタンに相当するGameObjectにスクリプトを追加します。DestinationAnimatorに切り替え先のパネルをアタッチします。

f:id:fb8r5jymw6fd:20211226104146p:plain

 

TransitionControllerのソースコードはこんな感じです。現在ボタンに設定されている親のGameObjectのAnimatorをsourceAnimatorに指定しています。GUIボタンからはOnButtonClicked()メソッドから呼び出すように定義しています。

timelineTransition?.StartTransition()でトランジション効果を呼び出します。transitionNameのプロパティに指定されている"StartTransition"はTimelineのマーカー名で、アニメーション開始位置として設定しているSignalAssetの名前を入れています。

#nullable enable

using UnityEngine;

public class TransitionController : MonoBehaviour
{
[SerializeField]
private Animator? destinationAnimator;

[SerializeField]
private string transitionName = "StartTransition";

private Animator? sourceAnimator;

private TimelineTransition? timelineTransition;

public void OnButtonClicked()
{
if (timelineTransition != null && sourceAnimator != null && destinationAnimator != null)
{
if (sourceAnimator == destinationAnimator) return;

StartTransition(sourceAnimator, destinationAnimator);
}
}

private void StartTransition(in Animator source, in Animator destination)
{
timelineTransition?.StartTransition(source, destination, transitionName);
}

#region MonoBehaviour implements

private void Start()
{
timelineTransition = FindObjectOfType<TimelineTransition>();

var currentTransform = transform;
while (currentTransform.parent != null)
{
var parent = currentTransform.parent.gameObject;
var animator = parent.GetComponent<Animator>();
if (animator != null)
{
sourceAnimator = animator;
break;
}

currentTransform = currentTransform.parent;
}
}

#endregion
}

ButtonコンポーネントのOnClickイベントに上記スクリプトのメソッドを忘れず追加しておきます。

f:id:fb8r5jymw6fd:20211226105424p:plain

 

他のボタンコンポーネントも同じように、遷移先のGUIパネルを設定します。

 

できたもの

以下の動画が今回できたものになります。

f:id:fb8r5jymw6fd:20211226125314g:plain

 

また、今回の記事には解説していませんが、GitHubサンプルには他の例も入れています。

f:id:fb8r5jymw6fd:20211226125358g:plain

 

終わりに

以前からUnityのTimelineはPlayableTrack をはじめとした独自拡張が可能で、いろいろな用途に応用できそうなのは分かっていましたが、今回改めてその拡張性や新たな可能性を感じることができました!

TimelineそのものはPlayable APIの機能を使っているようなので、こちらへの理解も進めるとさらに広く応用が利きそうです。

お知らせ

弊社では現在様々な職種を募集しています。興味がある方は是非、以下のページを覗いてみてください!

twitter.com

meety.net

herp.careers

 

付録

*1:RectTransformからAnimationを作ろうとする場合、Add Propertyの項目がTransformではなく、RectTransformになっています。

f:id:fb8r5jymw6fd:20211226101751p:plain

Addressable Asset System入門

エンジニアの小松(@vtuber_watch)です。Synamon Advent Calendar 2021の21日目です。

UnityのAddressable Asset Systemの入門記事になります。Addressablesの紹介記事は多くありますが情報が古かったり必要な説明が抜けていたりするので最新のバージョンにおける情報をまとめています。

この記事では基本的な使い方を紹介します。アセットバンドルをビルドしてローカルからロードするまでをやります。これだけでもAddressablesの恩恵を受けられます。記事が長くなりすぎたのでアセットバンドルをサーバに配置する使い方は別記事に分割します。

Addressable Asset Systemとは

Addressablesを使うとUnityのアセットをアセットバンドルにまとめていい感じに読み込みできるようになります。

Addressablesによって得られるメリットはたくさんありますがここではメモリ使用量の削減とアセットバンドルの扱いの2点から紹介します。

メリット1. メモリ使用量の削減

Addressablesを使うとメモリ使用量を減らせます。この点については以下のUnity Blogの記事で紹介されています。

記事の中では3つのプレハブ(Sword, Boss Sword, Shiled)をランタイムに生成する例を使って説明されています。

public class SampleMonoBehaviour : MonoBehaviour
{
    // Addressablesを使わない場合
    [SerializeField] private GameObject prefab;

    // Addressablesを使う場合
    [SerializeField] private AssetReferenceT<GameObject> prefabReference;
}

Addressablesを使わない場合、MonoBehaviourのフィールドにプレハブをGameObjectとして持ち、必要なときにインスタンス化します。
この方法はメモリ効率が悪いです。プレハブを直接参照しているためこのMonoBehaviourが存在する限りプレハブ全体がメモリに載ってしまいます。

Addressablesを使う場合、MonoBehaviourのフィールドに持つのはGameObjectではなくAssetReferenceTになります。AssetReferenceTはGameObjectそのものではなくGameObjectへの参照です。必要に応じてプレハブを取得してインスタンス化します。
これによって必要なときだけプレハブがメモリに載るようになります。不要になったときにアンロードすることもできます。

メリット2. アセットバンドルを簡単に使える

Addressables以前は、アセットバンドルを実用するには自前で管理するためのシステムを用意する必要がありました。アセットバンドルの構成設定とビルド、ロードとアンロード、依存関係の解決などかなりの実装が必要です。

Addresssablesではこれらの機能がすでに用意されており、簡単にアセットバンドルを使い始められます。多くの機能が拡張できるように作られているので必要な部分だけカスタマイズして使えます。

パッケージのインポートと初期設定

Addressablesを使い始めるにはまずパッケージをインストールします。

1. メニューのWindow/Package ManagerからPackageManagerを開き、Addressablesを選んでInstallボタンを押します。
f:id:sakanox:20211221161925p:plain

2. インストールが完了したらメニューのWindows/Asset Management/Addressables/Groupsを押します。
f:id:sakanox:20211221162152p:plain

3. Create Addressables Settingsボタンを押します。
f:id:sakanox:20211221162534p:plain

以上でAddressablesがインストールされ、必要な設定ファイルが生成されました。

Addressablesの設定ファイルはAssets/AddressableAssetsDataフォルダに配置されます。

その中のAddressableAssetSettingsが全体の設定になります。
それぞれの設定の説明はマニュアルを見てください。今はデフォルトのままで大丈夫です。
f:id:sakanox:20211221163237p:plain

使ってみる

初期設定は出来たのでとりあえず使ってみます。

プレハブをアドレス可能(Addressable)にする

まずはサンプルとして使うプレハブを作ります。何でもいいですがここではデフォルトのCubeだけのプレハブを作ります。

作成したプレハブを選択すると、インスペクタの一番上に「Addressable」というチェックボックスがあります。ここにチェックを入れるとそのアセットはアドレス可能(Addressable)になります。アドレス可能なアセットがAddressablesシステムでロードできます。

チェックボックス隣のテキストはこのアセットのアドレスです。デフォルトではアセットのパスになっていますが自由に変更できます。このアドレスを指定してアセットをロードします。
f:id:sakanox:20211221164443p:plain

プレハブをインスタンス化する

作成したプレハブをロードしてみます。

ロードする方法はいくつかありますがここでは3つ紹介します。

  1. Addressables.LoadAssetAsyncはアドレスを指定してアセットをロードできます
  2. Addressables.InstantiateAsyncはアドレスを指定してプレハブをインスタンス化できます。
  3. AssetReferenceTはインスペクタでアセットを設定してアセットのロードやインスタンス化ができます。
1. Addressables.LoadAssetAsyncを使う

以下のスクリプトをシーンに配置して実行してください。プレハブがロードされてインスタンス化されます。

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class SpawnerByLoadAssetAsync : MonoBehaviour
{
    private AsyncOperationHandle<GameObject> prefabHandle;
    private GameObject spawnedGameObject;

    private async void Start()
    {
        // Addressables.LoadAssetAsyncで読み込む
        prefabHandle = Addressables.LoadAssetAsync<GameObject>("Assets/Prefabs/Cube.prefab");

        // .Taskで読み込み完了までawaitできる
        GameObject prefab = await prefabHandle.Task;

        // 読み込んだプレハブをインスタンス化する
        spawnedGameObject = Instantiate(prefab);
        spawnedGameObject.name = "Spawned Game Object";
    }

    private void OnDestroy()
    {
        // インスタンス化したGameObjectを破棄する
        Destroy(spawnedGameObject);

        // 使い終わったらhandleをリリースする
        Addressables.Release(prefabHandle);
    }
}

アドレス可能なアセットはAddressables.LoadAssetAsyncでロードします。ここにロードしたいアセットのアドレスを指定します。

LoadAssetAsyncの戻り値はAsyncOperationHandleになっています。Addressablesの操作はほとんど非同期になっており、AsyncOperationHandleで操作の状態を表します。.Taskを付けてawaitすれば簡単に操作の完了を待てます。

また、Addressablesでロードしたアセットは使い終わったら必ずリリースする必要があります。Addressables.Releaseを使います。これを忘れるとプレハブが読み込まれたままになってしまうので注意してください。リリース漏れは後で紹介するEvent Viewerで確認できます。

内部では参照カウンタ方式でそのアセットが何ヵ所から使われているかをカウントしています。リリースするたびにカウントが減り、0になったらアセットがアンロードされます。

2. Addressables.InstantiateAsyncを使う

次のスクリプトも先のSpawnerByLoadAssetAsyncと同じような動作をします。

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class SpawnerByInstantiateAsync : MonoBehaviour
{
    private GameObject spawnedGameObject;

    private async void Start()
    {
        // Addressables.InstantiateAsyncでプレハブをインスタンス化する
        AsyncOperationHandle<GameObject> handle = Addressables.InstantiateAsync("Assets/Prefabs/Cube.prefab");

        // .Taskでインスタンス化完了までawaitできる
        spawnedGameObject = await handle.Task;

        spawnedGameObject.name = "Spawned Game Object";
    }

    private void OnDestroy()
    {
        // 使い終わったらインスタンスをリリースする
        Addressables.ReleaseInstance(spawnedGameObject);
    }
}

Addressables.LoadAssetAsyncの代わりにAddressables.InstantiateAsyncを使っています。名前の通り、ロードする代わりに直接インスタンス化できます。

リリースでもAddressables.ReleaseではなくAddressables.ReleaseInstanceを使います。AsyncOperationHandleではなくインスタンス化されたGameObjectを渡してリリースできます。

Addressables.LoadAssetAsyncはGameObject以外のアセットもロードできますがAddressables.InstantiateAsyncはプレハブ専用です。プレハブをインスタンス化したい場合はAddressables.InstantiateAsyncの方が簡単に書けます。

3. AssetReferenceTを使う

先ほどの2つの方法はアドレスを指定してロードする方法でしたが、AssetReferenceTを使うとインスペクタでアセットを設定してロードできます。*1

次のコードをシーンに配置して、prefabReferenceにCubeプレハブを設定します。

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class SpawnerByAssetReferenceT : MonoBehaviour
{
    [SerializeField] private AssetReferenceT<GameObject> prefabReference;

    private GameObject spawnedGameObject;

    private async void Start()
    {
        // InstantiateAsyncでプレハブをインスタンス化する
        AsyncOperationHandle<GameObject> handle = prefabReference.InstantiateAsync();

        // .Taskでインスタンス化完了までawaitできる
        spawnedGameObject = await handle.Task;

        spawnedGameObject.name = "Spawned Game Object";
    }

    private void OnDestroy()
    {
        // 使い終わったらインスタンスをリリースする
        prefabReference.ReleaseInstance(spawnedGameObject);
    }
}

ここではInstantiateAsyncを使ってプレハブをインスタンス化していますが、LoadAsyncもあるのでアセットのロードにも使えます。

AssetReferenceTはインスペクタで参照を設定できるのが強みです。特に理由がなければAssetReferenceTを使うのをお勧めします。

f:id:sakanox:20211221174854p:plain

Addressables Groups

全てのアドレス可能なアセットはグループにわけて管理されます。

メニューのWindows/Asset Management/Addressables/Groupsでグループを管理できます。

f:id:sakanox:20211221180242p:plain

画像のPrefabsやMaterialsがグループで、その下のAssets/Prefabs/Cube.prefabなどがグループに入っているアセットです。

グループは右クリックで新しく作成できます。グループにアセットをドラッグ&ドロップしてアセットを振り分けられます。

同じグループのアセットは1つのアセットバンドルとしてビルドされます。*2つまり、グループ単位でロードやアンロードが行われるということです。同じタイミングで使われるアセットを同じグループにしておくと効率が良くなります。

Play Mode Script

Play Mode Scriptメニューでエディタ上で実行したときの動作を設定できます。

f:id:sakanox:20211221185214p:plain

「Use Asset Database」ではアセットバンドルを使わずにアセットを直接読み込みます。動作が速いので開発中にはこれを使います。

「Simulate Groups」ではアセットバンドルは使われませんが、グループの設定に従ってアセットバンドルの動きをシミュレートします。これによってEventViewerでアセットバンドルのロードやアンロードを監視できるようになります。
エディタ上でグループの設定を試すのに使えます。

「Use Existing Build」では実際にビルドされたアセットバンドルが使われます。あらかじめアセットバンドルをビルドしておく必要があります。

アセットバンドルのビルド

Build/New Build/Default Build Scriptでグループをアセットバンドルにビルドできます。ビルドはLibrary/com.unity.addressables/aaフォルダに出力されます。
f:id:sakanox:20211221190757p:plain

ビルドされたアセットバンドルは、プロジェクトのビルド時には自動的にStreamingAssetsにコピーされます。これによって実機でも正しくアセットバンドルをロードできます。

グループの設定

Addressables Groupsでグループを選択するとグループごとの設定ができます。

f:id:sakanox:20211221180740p:plain

ここではBuild & Load Pathsについて説明します。他の項目についてはマニュアルを見てください。

Build & Load Paths

一番上の「Build & Load Paths」でアセットバンドルをどこにビルドしてどこからロードするかを設定できます。

f:id:sakanox:20211221182117p:plain
「Local」ではLibraryフォルダ内にビルドされ、プロジェクトのビルド時には自動的にStreamingAssetsにコピーされます。
ロードもEditor実行時はLibraryフォルダ、ビルドではStreamingAssetsから自動的に行われます。

f:id:sakanox:20211221182131p:plain
「Remote」ではServerDataフォルダ内にビルドされます。ビルドしたアセットバンドルは手動でサーバなどにアップロードする必要があります。
ロード時にサーバからダウンロードされます。

f:id:sakanox:20211221182458p:plain
「custom」ではパスを自由に設定できます。

Remoteに設定したときの詳しい使い方はまた別の記事で説明します。

Addressables Analyze

アセットバンドルには、そのグループに振り分けたアセットだけでなく、それらのアセットが依存するアセットも含めてビルドされます。

依存先のアセットがどこかのグループに入っていればそこへの参照が張られます。どこのグループにも入っていない場合は同じアセットバンドルに直接含まれます。実際にどのアセットがどのアセットバンドルに含まれるかをAddressables Analyzeで確認できます。

Addressables AnalyzeはAddressable GroupsのTools/Window/Analyzeメニューで表示できます。
f:id:sakanox:20211221193405p:plain

「Analyze Select Rules」を押すとグループ設定を検証できます。

使ってみる

実際に以下のグループ設定で検証してみます。Cube.prefabとCube2.prefabはどちらもGreen.matというマテリアルに依存しています。
f:id:sakanox:20211221193604p:plain

この状態で「Analyze Select Rules」を押すと以下のような結果になります。
f:id:sakanox:20211221193806p:plain

アセットバンドルに含まれるアセット一覧はBundle Layout Previewに表示されています。

ここでは2つのアセットバンドルにGreen.matが暗黙的(Implicit)に含まれてしまっています。同じアセットが別々のアセットバンドルに含まれると、同じアセットが2重にロードされて効率が悪くなります。Analyze結果でも黄色い三角で警告として表示されています。

これを解決するにはGreen.matをどこかのグループに入れてしまえばOKです。「Fix Selected Rules」を押すと自動的に重複するアセットを新しいグループに隔離してくれます。実行するとGreen.matがDuplicate Asset Isolationというグループに入りました。
f:id:sakanox:20211221194402p:plain

Addressables Event Viewer

Event Viewerを使うとアセットバンドルのロードやアンロードを監視できます。アセットのリリース漏れなどがないかを確認できます。

Event Viewerを使うには設定を変更する必要があります。

  1. AddressableAssetsData/AddressableAssetSettingsを選び、Send Profiler Eventsにチェックを入れます。
  2. Addressables GroupsでPlay Mode ScriptをSimulate GroupsまたはUse Existing Buildにします。

EventViewerはAddressable GroupsのTools/Window/Event Viewerで表示できます。エディタ上で実行開始すると以下のように現在の状態を見られます。
f:id:sakanox:20211221195520p:plain
ここではCube.prefabが1つロードされていることがわかります。

まとめ

  • グループに割り振られたアセットがアドレス可能となり、Addressablesシステムでロードできます。
  • グループに割り振られたアセットがアセットバンドルとしてひとまとめにビルドされます。
  • Addressables Analyzeを使ってアセットの重複を検査・修正できます。
  • Addressables Event Veiwerを使ってアセットのリリース漏れを検査できます。

*1:Unity2019以前ではインスペクタでGenericsを使えないためAssetReferenceT<GameObject>ではなくAssetReferenceGameObjectになります

*2:グループ設定で別のビルド方法もできますが、ビルド時間が延びたり管理しづらくなったりするのでお勧めしません