20210503のUnityに関する記事は5件です。

【Unity】最強のメッセージングライブラリ「MessagePipe」のメリットと使いどころを解説してみた

はじめに 最近はVRMmodの人と認識されるようになった@yoship1639です。 久々の投稿となります。 皆様、MessagePipeというライブラリを聞いたことはありますでしょうか。 多分ほとんどの方が聞いた事が無いと思います。なぜならリリースからまだ1週間しか経っていませんので…(2021/5/3現在) このMessagePipe、ポテンシャルがやばいので是非いろんな方に知っていただきたくて早めに記事にしました。 個人的にはUniRxやUniTaskレベルで使えるのではと思っています。 ※ ちなみに私が触った所感なので、本記事で述べるMessagePipeのメリットや使い方が正しいかどうかはまだ不明です MessagePipeについては既に以下の記事が投稿されておりますので、どのような機能があるかはこちらを参考にしていただければと思います。 本記事では細かい機能は述べません。 MessagePipe入門:序 MessagePipe入門:破 まずは、簡単にどんなライブラリか説明します。 MessagePipeとは MessagePipeは、C#大統一理論でお馴染みneuecc先生が開発し2021/4/26にリリースした.Net, Unity用メッセージングパイプラインライブラリです。 嚙み砕いて言うと、「C#標準のeventよりも高速な多機能イベント・分散メッセージング用ライブラリ」となります。 主な用途として、Readmeには以下の様に記載されています。 Pub/Sub CQRSのMediatorパターン PrismのEventAggregator(V-VM decoupling) etc Unityの場合、オブジェクト同士のメッセージングや通信の仲介役として使われる想定ですね。 MessagePipeがどの位パフォーマンスがあるかというと この位高速らしいです。RxのSubjectやC#標準eventよりも高速なのは目を引きます。 MessagePipeのメリット そんなMessagePipeですが、どうしてポテンシャルがあると私が思ったかというと ずばり、オブジェクト同士が互いを認識することなく最小限のインターフェースで超高速にやり取りできるからです。 この一言でこのライブラリの凄さに気づいた方は鋭いです。 Unityでは今までオブジェクト同士がお互いが参照しあったり、あるマネージャが他のマネージャのメソッドを呼んだりなど密になった状態が当たり前となっていました。これはUnityでゲームを作っている方には避けられない道であるかと思います。イベント処理だとUniRxを使う方も多いですが、UniRxでもイベントの仲介者はサポートしていないため、オブジェクトのどちらか片方がもう片方の実態を認識しメッセージを送る必要がありました。 sample.cs class Enemy { public int Hp { get; set; } } class Player { public int Hp { get; set; } public int Atk { get; set; } // Player は Enemy というクラスを知っていなくてはならない public void Attack(Enemy enemy) { // 超適当な敵への攻撃 enemy.Hp -= Atk; } } これを回避するには、インターフェースを作成しそれを仲介させることでお互いの依存度を下げる対処を行う必要がありました。これは、有効な手段ですがUnityだと煩雑になりかねないのでインターフェースを記述せずに直接メッセージングを行ったりする事も多いのではないかと思います。 sample2.cs interface IDamageable { int Hp { get; set; } } class Enemy : IDamageable { public int Hp { get; set; } } class Player { public int Hp { get; set; } public int Atk { get; set; } // Player は IDamageable に対してダメージを与える // この場合、プレイヤーはEnemyの存在を知らなくてもよいが、インターフェースを定義していなければならない public void Attack(IDamageable damageable) { damageable.Hp -= Atk; } } これ以外にもAttackManagerを実装してAttackManagerがプレイヤーをとエネミーの攻撃処理を取りまとめる事もできますが、結局AttackManager依存となるため根本的な問題は解決しません。 ですが、MessagePipeはこれらの厄介な問題を一気に解決してくれます。 オブジェクトが互いを認識しなくても、AマネージャがBマネージャを認識しなくても、仲介役のインターフェースを記述しなくても他のオブジェクトにメッセージを送ることができるのです。詳しい例は次の章でご紹介します。 互いを認識せずともメッセージを送る事ができて一体どんなメリットがあるのか、それは最大限の疎結合化・モジュール化の手段となる事です。 分野にもよりますが、Unityを使ったゲーム開発であれば、これは設計においてエンジニアが目指すべき領域です。 疎結合化やモジュール化のメリットはググるとたくさん出てきますので、これらのメリットが解らない方はぜひ調べてみてください。 ※ 因みにここで指すモジュール化はオブジェクト指向の型によるモジュール化です。手続き型のモジュール化とは違います。 では、実際にMessagePipeの使用例を見てみましょう。 MessagePipeの使用例 先ほどの例で考えます。例えば3DアクションでプレイヤーがエネミーにZキーでダメージを与えたい場合、MessagePipeを使うとどのように記述できるのかをご紹介します。 まず、どんなメッセージがあればプレイヤーがエネミーにダメージを与えることができるのかを考えます。 PlayerAttackData.cs using UnityEngine; public class PlayerAttackData { public Vector3 position; // どの位置で public float radius; // どの範囲で public int value; // どの位のダメージで } 一先ず上のデータがあれば最低限のダメージデータとしては成り立ちそうです。 次に、AttackDataをMessagePipeで使うために登録します。 MessagePipeはver1.2.0時点ではDIコンテナを使う必要がありますので、今回は「Zenject」を使います。 ZenjectInstaller.cs using MessagePipe; using UnityEngine; using Zenject; public class ZenjectInstaller : MonoInstaller { public override void InstallBindings() { // MessagePipeをZenjectContainerにバインド var option = Container.BindMessagePipe(); // PlayerAttackDataのイベントを登録 Container.BindMessageBroker<PlayerAttackData>(option); } } これで、プレイヤーからエネミーに行くダメージイベントの登録が完了しました。 次に攻撃イベント発する機能をもつプレイヤークラスを記述します。 Zキーで攻撃イベントを発します。 Player.cs using MessagePipe; using UnityEngine; using Zenject; public class Player : MonoBehaviour { // Zenjectによって自動的に攻撃イベントが注入される [Inject] IPublisher<PlayerAttackData> AttackEvent { get; set; } [SerializeField] private int hp; [SerializeField] private int atk; void Update() { if (Input.GetKeyDown(KeyCode.Z)) { Debug.Log("プレイヤーの攻撃"); // Zキーで攻撃。攻撃データを発するだけ AttackEvent.Publish(new PlayerAttackData() { position = transform.position + transform.forward, // プレイヤーの前方 radius = 1.0f, // 半径1m value = atk, // ダメージ量 }); } } } 上のコードを見ていただければ分かりますが、Playerは攻撃データを発する事しかしていません。そこにはEnemyもIDamageableもありません。 IPublisherがMessagePipeのイベント機能です。名前の通りイベントをPublishします。 ここにはZenjectから勝手にInjectされるので個別にIPublisherを代入する必要はありません。 最後に攻撃イベントを受けるエネミークラスを記述します。 Enemy.cs using System; using MessagePipe; using UnityEngine; using Zenject; public class Enemy : MonoBehaviour { // Zenjectによって自動的に攻撃イベントが注入される [Inject] ISubscriber<PlayerAttackData> OnAttacked { get; set; } [SerializeField] private int hp; private IDisposable disposable; void Awake() { var d = DisposableBag.CreateBuilder(); OnAttacked.Subscribe(attack => { if (Vector3.Distance(transform.position, attack.position) <= attack.radius) { // プレイヤーの攻撃範囲内の場合、ダメージを受ける hp -= attack.value; Debug.Log("エネミーはダメージを受けた"); if (hp <= 0) { Debug.Log("エネミーは倒れた"); Destroy(gameObject); } } }).AddTo(d); disposable = d.Build(); } void OnDestroy() { // 破棄されるタイミングでOnAttackイベントの購読をやめる disposable.Dispose(); } } どこかしらから来たプレイヤーの攻撃イベントを受け取って、攻撃を受ける範囲内の場合にダメージを受ける処理を記述しています。 Enemyクラスを見ればわかりますがそこにはPlayerの存在がありません、MessagePipeを通して来た攻撃イベントを元にダメージを受ける処理を記述しているだけです。 これで、Zキーを押すたびに前方にいるエネミーにダメージを与えられます。 ZenjectInstaller、Player、Enemyすべてのクラスでお互いの実態を認識していません。そこにはAttackDataという攻撃のデータが行き来するだけです。 これがMessagePipeの凄いところです。 Unityでの使いどころ 使用例を見ただけではMessagePipeをどのようなところで使えばいいのか分からないと思うので、簡単にご紹介します。 モジュール同士のような広いスコープでのイベント処理 まず、使うべき場所はモジュール同士のような広いスコープでのイベント処理です。 イベントといえばUniRxも柔軟にイベントを処理できるライブラリですが、どちらか片方が相手の実態を認識していなければなりません。もし、UniRxを広いスコープで利用するとどうなるかというと、全く関係のないオブジェクト同士が密に繋がってしまい疎結合化やモジュール化ができなくなります。 しかし、広いスコープではMessagePipeを使えばこれを回避できます。モジュール同士が結合度を高めてしまう事をほぼ完全に防げるのです。 関係性が薄いオブジェクト同士がイベントのやり取りをしなければならない時 次に、関係性が薄いオブジェクト同士のイベント処理です。 プレイヤーとエネミーは互いに独立していた方が良いのはもちろんですが、どうしてもそれらをつながなければならない場合があります。先ほどの攻撃処理がそうですね。この場合はMessagePipeを使った方が良いです。C#標準のeventやUniRxでも同じような処理はできますが、先ほども述べたように、どちらかがどちらかの実態を認識する必要があります。これを回避できるのはありがたいですね。 逆を言えば関連性のあるオブジェクト同士には今まで通りUniRxやeventを使った方が良いです。わざわざMessagePipeを介すだけ無駄なので。 ボトムアップ型の設計をしたUnityプロジェクト 最後に、ボトムアップ型の設計をしたUnityプロジェクトです。 これを聞いてピンとこない方も多いかと思いますが、設計にはボトムアップ型とトップダウン型の2種類の設計があります。簡単に説明すると、○○マネージャが○○オブジェクトを操作、管理する手法がトップダウン設計で、逆に個々のオブジェクトが自発的にふるまいをしてプロジェクト全体を成り立たせるのがボトムアップ型の設計です。ざっくりいうと前者は手続き指向で後者はオブジェクト指向だと思ってください。細かい話はここではしないので各自調べていただければと思います。 どちらの設計が良いかは作ろうとしているプロジェクトによりますが、ゲーム開発において私がお勧めしているのはボトムアップ型の設計です。最速の処理速度を目指すなら手続き型が最適解になりますが、機能追加等の仕様変更や柔軟性に強いプロジェクトを目指すならボトムアップ型の方が良いです。 MessagePipeはオブジェクト同士がやり取りするためのライブラリなので、上から管理しているトップダウン型のプロジェクトにはあまり向きません。なぜなら、上が下の実態を認識しておりメッセージの仲介を上が行っているので疎なメッセージングを行う必要性が薄いからです。 しかし、ボトムアップ型だと効果は絶大です。MessagePipeはオブジェクト指向が目指している関心の分離、疎結合化を解決する最適な手段になり得ます。 おわりに まだMessagePipeはリリースされたばかりで文献が全くない状態なので本記事の内容が本当に正しいかは分かりません、ただ、いち早く触ってみた私の所感は記述した通りで大方まとめたつもりです。UniRxやZenjectといった前提知識が求められるライブラリで学習には少しハードルが高いかもしれませんが、MessagePipeはUniRxやUniTaskなどと並び得るライブラリだと個人的には思っています。それだけポテンシャルを持っています。 勘違いしてはならないのが、MessagePipeを使えば疎結合化が簡単に実現できる訳では無いという事です。これはどのようなライブラリにも言えますが、使い方を間違えると逆に悪化してしまう可能性があることを念頭に置いてください。特にUniRxやUniTask等がまだうまく使えないUnity初心者の方には全くお勧めできないライブラリです。UnRx、UniTask、ZenjectやVContainerといったライブラリの使い方を知っている、メリットも分かっている方には非常にお勧めです。 MessagePipeを試してみたいけど一々準備するのが面倒な方や、どう記述できるのか学習したい方向けにGithubにMessagePipeTestというプロジェクトをあげているので、軽く見てみたい方はぜひクローンしてみてください、必要なパッケージは自動でインポートされます。できれば更新してくださいお願いしますお願いします。。。 https://github.com/yoship1639/MessagePipeTest この記事がMessagePipeを導入しようとするきっかけになっていただければ幸いです! 結合度に関する追記(21/05/04) 勘違いされないためにも一応追記しておきます。 MessagePipeを使って結合度を下げる事は保守や変更に対して強くなるので良い事ではありますが、やり過ぎは禁物です。Pub/Subだらけにするなど行き過ぎた疎結合化はプロジェクト全体像を把握する事ができなくなります。すると逆にプロジェクトが保守できなくなる可能性もあります。これでは導入した意味がなくなってしまうので、適材適所に使うようにしてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity WebXR を動かすまで

UnityのWebXR ExportをFirebaseにデプロイするまで Firefox, Chrome の最新ブラウザで、VR的なコンテンツを表示したい!と思い、 いろいろと調べたので健忘録として 背景 なぜ、Nativeではないのか? インストールをしたくない。アプリストア審査で時間がかかるからバグの修正が大変になる Occulus Questとかでも動くかも。 ということですが、VRコンテンツとなると three.js などのスクリプトを駆使するのも大変なので、できるだけコンテンツ制作に近いツールを利用するようにしたいということで、Unityを選択しました。 準備 以下のURLにかかれている内容に沿っていきました。 https://bibinbaleo.hatenablog.com/entry/2020/12/29/203933 今回、使用した環境は、 Unity 2020.3.6f1 Package managerからgitでWebXR Export の後継プロジェクトを導入。Window>Package Manager> + > git url で以下の2つのURLをコピー&ペースト。 https://github.com/De-Panther/unity-webxr-export.git?path=/Packages/webxr https://github.com/De-Panther/unity-webxr-export.git?path=/Packages/webxr-interactions WebXR Interactions で Sample Scene を import 今回は、このサンプル Sceneを使って話をすすめます。 Unityの設定 テンプレートの導入 - Windows > WebXR > CopyWebGLTemplates WebXR Export を設定 - Project Setting -> XR Plugin Management > Plug-in Providers > WebXR Export をチェック HTMLの出力に変更 - HTML5 - Resolution and Presentation - WebXR2020 (Unity2020の場合)を選択 https://gaprot.jp/2020/10/26/unity2020-webgl-01/  により、  - Decompression fallback を ON にする を忘れないようにしておかないと、FireBaseでのリリースしたときに起動失敗のエラーが解消した ビルド Desertのサンプルを追加してビルド。ビルド先のフォルダ名は public以外にしておく ローカル https の設定 出力したindex.html などのファイルは、そのままローカルのブラウザにドロップしても動きません。 さらに、セキュリティの関係で httpsサーバーからダウンロードしたコンテンツでないと、 ジャイロが動きません。そのため、ローカルで https のサーバーを立ち上げて、動作確認できるようにします。 npm をインストール http-server のインストール npm install -g http-server 次に、httpsサーバーにするための認証鍵を生成 - Git for Windows をインストール - bash を開いて、以下のコマンドを入力してcert.pem を生成 openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem httpsの立ち上げは、Unityから生成した index.html のある場所に cert.pemをおいて以下のコマンドでテスト http-server -S -C cert.pem これで、ローカルのネットワークから Androidスマホでアクセスします。右下のVRボタンを押して左右に分かれてレンダリングされればOK. Firebase の設定 ローカルのテストがうまくいったら、外部のホストでアクセスできるようにホスティングサービスを利用します。 https が提供されて、手軽に試せる Firebase を使います。アカウント登録した前提で、 ホスティングのサービスで外部からのテストを実施。 事前に、Web画面でプロジェクトを作っておきます。 Unityで出力した一つ上のフォルダでコマンドプロンプトで移動しておきます。 Firebase CLIの基本的な使い方は、以下を参照 https://firebase.google.com/docs/cli?hl=ja#sign-in-test-cli コマンドラインツールを使うため npm からインストール npm install -g firebase-tools ログインは、アカウント登録画面が Webで表示されます。 firebase login プロジェクトの確認 firebase projects:list プロジェクトの初期化 firebase init ホスティングサービスを設定して、public が生成される。 Unityで出力したファイルを public の下にコピーする テスト段階では、誰かに勝手に使われて料金が発生するのは大変なので、 ひとまずホスティングの停止する場合は以下のdisableを実行。再開は、またdeployすれば良い。 firebase hosting:disable 動作テストで、作成した firebaseのプロジェクト https://XXX.web.app XXXは、Firebaseのプロジェクト名
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MessagePipe入門:破

前回のMessagePipe入門では、初投稿にも関わらず反響をいただけたので、味をしめた私は、続編を書くことを決定しました。 MessagePipeは布教したいですし、こうなったら主要な機能は一通り解説しておきたいですね。 今回は、前回残したEventFactoryと、RequestHandler機能を主にご紹介したいと思います。 EventFactory その名の通り、(MessagePipeによる)eventを作るものです。 通常のPub/Subでは、DIにより、型でワイヤリングされていますが、EventFactoryからイベントの個別のインスタンスを作り出すことが出来ます。 IDisposablePublisher<int> countPublisher; public ISubscriber<int> OnCount { get; } ctor(EventFactory eventFactory) { //戻り値がタプルなので、分割代入が便利。 (countPublisher, OnCount) = eventFactory.CreateEvent<int>(); //CreateEventAsyncもあります。 } 使い方は様々だと思いますが、このような状況であっても、MessagePipeに統一することが可能であるということを覚えておけば良いのではないかな~と思っています。 Request/Response これは、Mediatorパターンの実装に有効な機能なようです。 実は筆者、デザインパターンに明るくなく、Mediatorパターンについては知識が浅かったりします。また、GoFデザパタに関しては、実際に必要となる状況に当たり、実装を通して理解した概念に後から名前を付け、他のエンジニアとの会話を楽にするものだと思うぐらいがちょうど良いと思っていますので、Mediatorの説明は他の方におまかせしたいと思います。 とは言え、用途は検討がつきます。 またですが、メッセージアプリを作りたいとしましょう。 まず、MessagePipeにおけるRequest/Responce機能の実装とは、以下のようにIRequestHandler<TMessage,TResponse>を実装することです。 public class MessageHandler: IRequestHandler<MessageRequest,MessageResponce> { //コンストラクタ public MessageHandler(...) { ... //いろいろなものを使って色々やりたいとする。 //(ユーザー情報のリポジトリに接続、グループの情報を取得する等... } MessageResponce IRequestHandler.Invoke(MessageRequest req) { if(user.isNantoka) { ... //色々 var responce = ...; //色々あった。 return response; } throw new Exception("Somthing wrong has happened"); } } 次に、MessageをAPIから送るコントローラーが有るとしましょう。 (※妄想フレームワークです) public class MessageController: ControllerBase { IRequestHandler<MessageRequest, MessageResponse> handler //型情報から先程のMessageHandlerのインスタンスを受け取ることが出来る。 public MessageController(IRequestHandler<MessageRequest,MessageResponse> handler) { this.handler = handler; } [Get] public async ValueTask<MessageResponse> SendMessage(MessageRequest req) { var response = handler.Invoke(req); return response; } } この例はSendMessageがコールされたときに、送られたメッセージをハンドリングしてレスポンスを返すというケースで、MessageHandlerがあることにより、API側はそれを呼んで返すだけ、となっています。 メッセージのやり取りを経由するシンプルなサーバーであっても、ユーザー情報や、グループの情報など、多くの依存やビジネスロジックが絡んでくるかと思います。それらを隠蔽しつつ、「リクエストからレスポンス」と単純なインターフェースへ統一出来ることには価値があるのでは思います。なるほど、これがMediatorパターンか(多分違う) Request/Response All, Asyncについて 非同期版のAsyncについてはPub/Subの場合とほとんど同じ流れでいけます。名前にAsyncが付き、Requestの戻り値はValueTask<TResponse>になります。 ですが、RequestAllHandlerというものがあり、これについては言及しておいた方が良さそうです。 複数のハンドラを実行したい場合、I(Async)?RequestAllHandlerとしてインスタンスを受け取ることで、DIに登録されているすべてのハンドラを実行することが出来ます。 インターフェースは以下のようになっています。 public interface IRequestAllHandler<in TRequest, out TResponse> { TResponse[] InvokeAll(TRequest request); IEnumerable<TResponse> InvokeAllLazy(TRequest request); } public interface IAsyncRequestAllHandler<in TRequest, TResponse> { ValueTask<TResponse[]> InvokeAllAsync(TRequest request, CancellationToken cancellationToken = default); ValueTask<TResponse[]> InvokeAllAsync(TRequest request, AsyncPublishStrategy publishStrategy, CancellationToken cancellationToken = default); IAsyncEnumerable<TResponse> InvokeAllLazyAsync(TRequest request, CancellationToken cancellationToken = default); } IRequestHandler自体の実装は変わりませんが、複数のインスタンスが必要なので、以下のようなハンドラもあったとしましょう。 ///送られてきたメッセージを別のサーバーに転送する public class MessageTransferHandler: IRequestHandler<MessageRequest,MessageResponce> { //詳細略 } 例の用途はともかくとして、このようなケースはあるはずでしょう。 これらを、使用側で、IRequestAllHandlerとして受けます。 ctor(IRequestAllHandler<MessageRequest,MessageResponse> handlers) { this.handlers = handlers; } 呼び出し箇所で、InvokeAllまたは、InvokeAllLazyを呼び出すことが出来ます。Lazyの方はその名の通り遅延実行でforeachで回すまで実行されません。ちなみにAsyncの方だと、レアキャラの(?)AsyncEnumerableを使うことが出来ます! MessageResponse[] responses = handlers.InvokeAll(request); //or IEnumerable<MessageResponse> responses = handlers.InvokeAllLazy(request); 同じインターフェースの実装を複数DIに登録するパターンに慣れない方もいるかも知れません(私がそうでした) 複数を登録した場合にどうなるのか、気になって調べてみたところ(これはDIの実装に依るのですが)私の知る限りではMSのDIとVContainerは最後に登録されたものが渡されるようです。結果は未定義では無いということですね。RequestAllHandlerに関しては、実行順序はおそらく関知するところでは無いのでしょう。 Filter Pub/Subと同様、RequestHandlerにもFilterを挟むことが出来ます。 文字列のリクエストをすべて反転させてしまうはた迷惑なフィルターなら以下のように作れるでしょう。 using System.Linq; public class ReverseStringFilter: RequestHandlerFilter<string,string> { public override string Invoke(string request, Func<string,string> next) { return string.Concat(request.Reverse()); } } Async版。ValueTaskとCancellationTokenが加わる。 public class AsyncStringReverseFilter : AsyncRequestHandlerFilter<string, string> { public override async ValueTask<string> InvokeAsync(string request, CancellationToken cancellationToken, Func<string, CancellationToken, ValueTask<string>> next) { return string.Concat(request.Reverse()); } } RequestHandlerのFilterは、アトリビュートが基本となります。 [RequestHandlerFilter(typeof(ReverseStringFilter))] public class StringMessageHandler: IRequestHandler<string,string> { //略 } Asyncの場合は、 [AsyncRequestHandler]をつかいます。 今回はここまでです。 MessagePipeの主要な機能として、他にもRedisを使ったPub/Subなどが出来ます! 続編は書く、鴨!です。鴨。 ここまで読んでいただきありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Facemotion3dにおけるUaaLについて

最近、Facemotion3dというiOSアプリをリリースしました。 これは、iOSでフェイストラッキングが出来るアプリです。 このアプリの中には「UaaL」と呼ばれている技術が使われています。 目次 1.UaaLとは 2.UaaLが使用されているアプリ 3.SceneKitとの併用 4.SceneKitの面倒くささ 5.アプリ起動直後ではない方法でUaaLを使用する 6.UaaL実装においてうまく行かなかった点 7.Facemotion3dでUaaLを実装した理由 1. UaaLとは UaaLとは「Unity as a Library」の略で、その名の通りUnityをライブラリとして使うものです。 大元の記述をSwiftというiOSネイティブの言語で記述し、Unityをライブラリ(framework)として使用することで、UI部分はSwiftを使いつつ、3DCGの描画にはUnityを使うというようなSwiftとUnityの良い所取りが出来ます。 Unityでスクロール画面を作るよりも、Swiftでのスクロール速度の方が早かったりする利点があります。また、ARKitを使用する場合、「純正Unityだと30FPSしか出ないのに対して、Swiftだと60FPSを出せる...」という話をTwitter上で見かけたりしたので、本当かは分かりませんがそういう利点があります。 2. UaaLが使用されているアプリ 偉大な先人としてnoppeさんのvearや、Realityなどで使用されています。 そういった記事を参考にさせて頂いてFacemotion3dにもUaaLを使っています。 以下の記事などを参考にさせて頂きました。 https://qiita.com/noppefoxwolf/items/b43d8554142e69c2ada6 https://qiita.com/tkyaji/items/7dbd56b41b6ac3e72635 https://forum.unity.com/threads/integration-unity-as-a-library-in-native-ios-app.685219/ 3. SceneKitとの併用 Facemotion3dでは、Apple標準のビューポートを使用する「SceneKit」とUnityを使用する「UaaL」の併用ということをやっています。 これはどうしてこうなったかというと開発していく流れの中で偶然こうなったという感じです。元々、SceneKitでアプリを作っていて、SceneKitでも十分見栄えのするモデルをモデラーさんが作ってくれました。途中からUaaLについても調べ始めたので後付けでUaaLを載せました。 正確な比較はしたことないので分かりませんが、恐らくメモリの消費量であったり電池の消費量などの面ではUnityを起動しない方が長時間使用する場合、パフォーマンス的にはApple標準のSceneKitを使う方が良いのではないかという気がします。 4. SceneKitを使う上での面倒くささ SceneKitにも利点はありますが、Apple標準のビューポートは機能が少なく、プログラミングする上でのネット上の情報が少ないので3DCG部分について記述する上では圧倒的にUnity(UaaL)で記述する方が楽です。 また、UnityではVRMなどを使うとプログラマーからしてみると簡単にデータをアプリ内に読み込めます。 しかし、SceneKitを使う場合、MayaなどのDCCツールでデザイナーからデータを受け取り、それを良い感じに原点付近に来るようにデータを配置して、ピボット情報を修正し、スケール情報を整え、オブジェクトの命名規則をちゃんとチェックしたりした上で、DAE_FBXなどという聞きなれない拡張子でデータをエクスポートする必要があります。 それだけでなく、DAE_FBXを使うだけではデータをアプリ内に読み込めないので、DAE_FBXをテキストファイルで開き、内部の記述をちょっと書き換える必要があります。 これだけでも面倒くさいのですが、MacのXCodeにDAE_FBXを読み込んだ後に.scnというファイル拡張子に変換し、全てのテクスチャパスを手動で張り替えるという面倒くさい手順が必要でした。 モデル変更などでデザイナーさんが頻繁にデータを更新するので、この作業を何度か繰り返す必要があります。 ここら辺の作業は、今後、AppleとPixarが共同で開発しているUSDZという拡張子が台頭することで簡略化されるのだと思われます。 5. アプリ起動直後ではない方法でUaaLを使用する 参考にさせて頂いたこちらの記事では、アプリ起動直後にUnityが起動する方法が紹介されています。 Facemotion3dでも、前回アプリ終了時にUnityをロードしたまま終了していればアプリ起動直後にUnityが起動する仕組みになっていますが、基本はUaaLではなくSceneKitが起動するようになっています。 前置きが長くなりましたが、ここから実際にFacemotion3d内に記述されているコードを示します。基本的にはnoppeさんの記事の内容を拡張しながら場当たり的に書いていった上に、Swift歴も浅いので関数名の汚さや、記述の汚さが目立つかもしれません...。「適当に書いてたらなんか動いた...!」という類のコードであり、何ら参考にならないかもしれません。 AppDelegate.swift import UnityFramework @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var application: UIApplication? var launchOptions: [UIApplication.LaunchOptionsKey : Any]? var firstLaunchUnity = true var isUnityRunning = false func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) // see notes below for the meaning of Atomic / Non-Atomic SwiftyStoreKit.completeTransactions(atomically: true) { purchases in for purchase in purchases { switch purchase.transaction.transactionState { case .purchased, .restored: if purchase.needsFinishTransaction { // Deliver content from server, then: SwiftyStoreKit.finishTransaction(purchase.transaction) } // Unlock content case .failed, .purchasing, .deferred: break // do nothing @unknown default: print("unknown") } } } self.application = application self.launchOptions = launchOptions let storyboard = UIStoryboard(name: "ViewController", bundle: nil) window?.rootViewController = storyboard.instantiateInitialViewController() window?.makeKeyAndVisible() return true } lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "FACEMOJO for DAZ") container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) return container }() func saveContext () { let context = persistentContainer.viewContext if context.hasChanges { do { try context.save() } catch { let nserror = error as NSError fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } } } func firstStartUnity() { Unity.shared.secondInit() if let app = self.application { Unity.shared.application(app, didFinishLaunchingWithOptions: launchOptions) window?.makeKeyAndVisible() } } } class Unity: NSObject, UnityFrameworkListener, NativeCallsProtocol { static let shared = Unity() var unityFramework: UnityFramework //グローバル変数 var myVar = GlobalVar.shared override init() { let bundlePath = Bundle.main.bundlePath let frameworkPath = bundlePath + "/Frameworks/UnityFramework.framework" let bundle = Bundle(path: frameworkPath)! if !bundle.isLoaded { bundle.load() } let frameworkClass = bundle.principalClass as! UnityFramework.Type let framework = frameworkClass.getInstance()! if framework.appController() == nil { var header = _mh_execute_header framework.setExecuteHeader(&header) } unityFramework = framework super.init() }   //1度UnityをUnloadした後に読み込む場合に対応 func secondInit() { if !unityIsInitialized() { let bundlePath = Bundle.main.bundlePath let frameworkPath = bundlePath + "/Frameworks/UnityFramework.framework" let bundle = Bundle(path: frameworkPath)! if !bundle.isLoaded { bundle.load() } let frameworkClass = bundle.principalClass as! UnityFramework.Type let framework = frameworkClass.getInstance()! if framework.appController() == nil { var header = _mh_execute_header framework.setExecuteHeader(&header) } unityFramework = framework } } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { unityFramework.register(self) FrameworkLibAPI.registerAPIforNativeCalls(self) unityFramework.setDataBundleId("com.unity3d.framework") unityFramework.runEmbedded(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv, appLaunchOpts: launchOptions) } private func unityIsInitialized() -> Bool { return ( unityFramework.appController() != nil ) } func unloadUnity() { if unityIsInitialized() { unityFramework.unloadApplication() if let appDelegate = UIApplication.shared.delegate as? AppDelegate { appDelegate.firstLaunchUnity = true } } } //Unity側からSwiftに対して送られてくる関数-関数名はUaaLサンプルの初期設定のまま使用 func showHostMainWindow( _ color: String!) { //Unityを読み込み終わったことを示すフラグ self.myVar.UnityLoadFlag = true } func sendUnityMessage(objectName:String,functionName:String,message:String) { unityFramework.sendMessageToGO(withName: objectName, functionName: functionName, message: message) } var view: UIView { unityFramework.appController()!.rootView! } } class GlobalVar { private init() {} static let shared = GlobalVar() var UnityLoadFlag = false var unityFirstLoadFlag = true } ViewController.swift //グローバル変数 var myVar = GlobalVar.shared //Unityを起動する関数,適当なところで呼び出す func startUnity() { //GUI周りの表示切り替え DispatchQueue.main.async { if self.myVar.unityFirstLoadFlag == true { //Unityロゴを隠さないようにするための処理 //UaaLを使うとUnityロゴを隠せてしまうが、それを避ける処理 //self.tappedFunction() } //ロード時のくるくる開始 //self.activityIndicator.startAnimating() //self.activityIndicator.isHidden = false } //loadingUnity = true var waitTimeAfterStopUnity = 0.0 if self.myVar.UnityLoadFlag == true { waitTimeAfterStopUnity = 0.5 } stopUnity() DispatchQueue.main.async {        //SceneKitを非表示にする //self.faceView.isHidden = true } DispatchQueue.main.asyncAfter(deadline: .now() + waitTimeAfterStopUnity) { if let appDelegate = UIApplication.shared.delegate as? AppDelegate { appDelegate.firstStartUnity() DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { DispatchQueue.main.async { let unityView:UIView = Unity.shared.view unityView.isHidden = false self.view?.insertSubview(unityView, at: 0)              //ロード時のくるくる停止 //self.stop_activityIndicator() } /*        //SceneKitのタッチが反応しないようにする(画面回転用) self.faceView.isUserInteractionEnabled = false        //self.viewのタッチが反応するようにする(画面回転用) self.view.isUserInteractionEnabled = true self.view.isMultipleTouchEnabled = true self.unityView?.isUserInteractionEnabled = true self.unityView?.isMultipleTouchEnabled = true */ DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { if self.myVar.unityFirstLoadFlag == true { self.myVar.unityFirstLoadFlag = false } } } } } } //Unityをアンロードする関数,適当なところで呼び出す func stopUnity() { if self.myVar.UnityLoadFlag == true { DispatchQueue.main.async { //Bool型をStringで記述するというあほな記述、無視してください;; self.myVar.UnityLoadFlag = false let unityView = Unity.shared.view unityView.removeFromSuperview() Unity.shared.unloadUnity() unityView.isHidden = true      //SceneKitを非表示にする //self.faceView.isHidden = true } } } 6. UaaL実装においてうまく行かなかった点 1.AVAudioSessionやAVAudioRecorderなどを使った録音、再生周りがうまく動作しなくなった SceneKitのままだとAVAudioSessionなどがうまく機能しているのですが、UaaLを起動した途端に録音ができなくなりました。理由は不明です。 Unityの使用中だけでなく、Unityをアンロードした後でも同様に録音できないようです。 2.2本指ドラッグを反応させる方法が分からなかった Unityの画面のズームをさせるために、 self.viewに対してPinchGestureRecognizerを使用しています。 そして、Unity画面の回転のために、以下の記事を参考に1本指の画面ドラッグをself.viewに実装しています。 https://i-app-tec.com/ios/image-drag.html 上記の記事には、画面の2本指ドラッグについても書かれており、SceneKitでは2本指ドラッグが正常に反映されたのですが、UaaLになると2本指ドラッグによるカメラ移動が反応しないという問題が回避できませんでした。原因は不明です。 7. Facemotion3dでUaaLを実装した理由 単に、これは知っておかないとまずいやつだ、という感覚がありました。 また、既にVRMをiOS上に読み込む機能を持つフェイストラッキングアプリがいくつかあるのに、何でUaaLを使ってVRMを読み込む機能をFacemotion3dに実装しようと思ったかの理由を書いておこうと思います。 私がFacemotion3dの前身であるiFacialMocapというアプリを作った時に、1番最初に感じたことは、「周りがVRMを動かすものを作っている中で、自分のアプリだけVRMを動かすためのアプリではない」という点でした。 そんな中でVRMを動かす機能を自分のアプリにも実装してしまうと、アプリ間の差別化が出来ず、色んな方面と衝突してしまうのではないかという懸念が常にありました。しかし、VRMを動かすアプリを作れる人はいつでも非VRMを動かすアプリだって作れるので危機感とかもあったわけです。そんな中で、アプリの差別化をできないにしても、どうすれば「世の中にとって新規性のあるもの」を作れるかということは考えていました。 新規性の案の1つとして、自分のアプリは非VRMを動かせるわけなので、非VRMの動的ロードなども出来るのではないか...と考えたりしていました。実際に、TriLibというUnityの有料アセットを使用すればFBXを動的ロードの実装ができます。 私は、TriLibを動作テストをして、実際に使ってみました。FBXの動的ロードに成功し、ある程度動かせるレベルまで行けました。基本的に、軸情報とボーン等のオブジェクト名の情報さえあればFBXは動かせるわけなので、各ソフトウェアの軸情報をコピーしてUnity用に変換し、読み込むということを試してみたりしていました。FBXデータだと必ずしも原点付近にデータがあるとも限らないため、メッシュのバウンディングボックスを調べて、オブジェクトがカメラ内に収まるように移動させるようなことも試してみたりしました。 しかし、実装したものの、TriLibの完成度の問題なのか、自分の知識レベルの低さの問題なのか、いくつかの点でつまづいたわけです。 まず、TriLibは現時点でFBXを読み込むとメモリが変な食い方するのか何なのか、動きがのっそりしたおかしな挙動になります。また、テクスチャのロードなどがFBXのバージョンによってはうまく行かないなどの問題がありました。TriLibeによるFBXのロード時間は、VRMのロード時間より遥かに長く、10~20秒くらいかかった気がします。(2020年時点) そんなこんなで、FBXの動的ロードは置き去りになり、VRMの読み込み機能だけが残ったという背景があります。 FBXの動的ロードを実装したところで、将来的にはVRMやFBXではなく、Apple標準のUSDZがシェアを伸ばしてそちらが覇権を握ったりするかもしれません。その辺の考えについては、以下の記事に書いてみました。 ドワンゴが作るプラットフォームや規格について思うこと https://note.com/pekochun/n/n8c5a8f115645 こういうのも書きました。 モーションキャプチャアプリを作っていて色々思うこと https://note.com/pekochun/n/nf16643bd9f68
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】iOSネイティブプラグイン開発を完全に理解する - ネイティブプラグインの設計について

本編「【Unity】iOSネイティブプラグイン開発を完全に理解する」の付録記事です。 記事中での用語や略称についてはそのまま本編に倣う形で記載していきます。 ここでは実際にネイティブプラグインを実装/保守していく上でのオススメの設計手法について触れていきます。 あくまで自分がよくやる手法の話なので、一例として受け取っていただけると幸いです。 題材自体はiOSネイティブプラグインに限らずに適用できる内容になるかと思います。 TL;DR interfaceを切ってプラットフォームごとに実装を分けることをオススメ 可読性/保守性/拡張性が上がる DIフレームワークとの相性が良くなる かと言って慣習的に必ず切る必要は無い サンプル実装や簡単な機能検証程度ならプリプロセス命令で雑に分岐して直接呼び出してしまうのも有りかも 最小構成のサンプルを振り返ってみる 本編側にある最小構成のサンプルでは以下のようにEditor実行時との挙動はプリプロセス命令で分岐した上でボタン押下時にP/Invokeの処理を直接呼び出していました。 このようなサンプル実装や簡単な機能検証程度ならこの書き方でも良いかと思います。 sealed class Example : MonoBehaviour { [SerializeField] Button _buttonHelloWorld = default; void Start() { _buttonHelloWorld.onClick.AddListener(() => { #if !UNITY_EDITOR && UNITY_IOS // プラグインの呼び出し var ret = PrintHelloWorld(); Debug.Log($"戻り値: {ret}"); #else // それ以外のプラットフォームからの呼び出し (Editor含む) Debug.Log("Hello World (iOS以外からの呼び出し)"); #endif }); } #region P/Invoke [DllImport("__Internal", EntryPoint = "printHelloWorld")] static extern int PrintHelloWorld(); #endregion P/Invoke } マルチプラットフォーム想定の機能だとどうなるか? 例としてネイティブプラグインを活用した「iOS/Android標準のシェアUIからテキスト/画像/動画をシェアする機能」を実装するとしましょう。 もし上述のサンプルと同じくプリプロセス命令の分岐ベースで愚直に実装すると、場合によっては以下のような感じの実装になってくるかと思います。 クラス図 ※ExampleViewからシェア機能(ShareMedia)を呼び出すイメージ 1 コードはこちら ShareMedia.cs (クリックで展開) ShareMedia.cs namespace Examples.View { /// <summary> /// 各プラットフォーム標準のシェアUIからテキスト/画像/動画をシェア /// </summary> /// <remarks> /// NOTE: 全体的に参照透過性が高いのでstatic classとして纏めている /// </remarks> public static class ShareMedia { /// <summary> /// テキストのシェア /// </summary> public static void ShareText(string text) { #if !UNITY_EDITOR || UNITY_IOS ShareTextForIOS(text); #elif !UNITY_EDITOR || UNITY_ANDROID ShareTextForAndroid(text); #else Debug.Log("Editorからの呼び出し"); #endif } /// <summary> /// 画像のシェア /// </summary> public static void ShareImage(byte[] image) { #if !UNITY_EDITOR || UNITY_IOS ShareImageForIOS(image); #elif !UNITY_EDITOR || UNITY_ANDROID ShareImageForAndroid(image); #else Debug.Log("Editorからの呼び出し"); #endif } /// <summary> /// 動画のシェア /// </summary> /// <param name="moviePath">動画のパス</param> public static void ShareMovie(string moviePath) { #if !UNITY_EDITOR || UNITY_IOS ShareMovieForIOS(moviePath); #elif !UNITY_EDITOR || UNITY_ANDROID ShareMovieForAndroid(image); #else Debug.Log("Editorからの呼び出し"); #endif } #region iOSのP/Invoke #if UNITY_IOS // iOSのシェアUIからテキストをシェア [DllImport("__Internal", EntryPoint = "shareText")] static extern void ShareTextForIOS(string text); // iOSのシェアUIから画像をシェア [DllImport("__Internal", EntryPoint = "shareImage")] static extern void ShareImageForIOS(byte[] image); // iOSのシェアUIから動画をシェア [DllImport("__Internal", EntryPoint = "shareMovie")] static extern void ShareMovieForIOS(string moviePath); #endif #endregion iOSのP/Invoke #region AndroidのP/Invoke #if UNITY_ANDROID // AndroidのシェアUIからテキストをシェア static void ShareTextForAndroid(string text) { // ネイティブのシェアUI呼び出し (省略) } // AndroidのシェアUIから画像をシェア static void ShareImageForAndroid(byte[] image) { // ネイティブのシェアUI呼び出し (省略) } // AndroidのシェアUIから動画をシェア static void ShareMovieForAndroid(string moviePath) { // ネイティブのシェアUI呼び出し (省略) } #endif #endregion AndroidのP/Invoke } } ExampleView.cs (クリックで展開) ExampleView.cs namespace Examples.View { /// <summary> /// シェア機能の呼び出しサンプル /// </summary> sealed class ExampleView : MonoBehaviour { [SerializeField] InputField _shareText = default; void Start() { // 入力されたテキストのシェア _shareText.onEndEdit.AddListener(text => { ShareMedia.ShareText(text); }); } } } プリプロセス命令の分岐が増えてくると可読性が悪くなる 注目して欲しいのはShareMedia.csです。 至るところにプリプロセス命令による分岐が挟まってます。 例に示した機能程度であれば「これでも運用できなくはないかな...」と言う意見もあるかもしれませんが、ここから更に「対応プラットフォームにStandaloneとWebGLを追加」「テキスト/画像/動画以外にも、○○をシェアできるようにしたい」と言った要件が増えてくると、その分プリプロセス命令の分岐が増えてきて可読性や保守性が悪くなってくるかと思われます。 更に言うとサンプルコードのpublic methodはP/Invokeのメソッドを呼び出すだけと言った割とシンプルな構成で収まってますが、場合によってはプラットフォームごとに合わせたデータ変換と言ったロジックも挟まる可能性もあり、そうなってくると更にコードの量も増えていく懸念があります。 interfaceを切ってプラットフォームごとに実装を分けていく 「じゃあどう実装していくのが良いのか?」について話していきます。 個人的には表題にある通り、interfaceを切ってプラットフォームごとに実装を分けていく手法をオススメします。 前の章のサンプルを分けるとしたら以下のような形になります。 先にクラス図/コード合わせて載せておきます。 クラス図 コードはこちら IShareMedia.cs (クリックで展開) IShareMedia.cs namespace Examples.View { /// <summary> /// シェアUIからテキスト/画像/動画をシェア /// </summary> interface IShareMedia { /// <summary> /// テキストのシェア /// </summary> void ShareText(string text); /// <summary> /// 画像のシェア /// </summary> void ShareImage(byte[] image); /// <summary> /// 動画のシェア /// </summary> /// <param name="moviePath">動画のパス</param> void ShareMovie(string moviePath); } } ShareMediaForIOS.cs (クリックで展開) ShareMediaForIOS.cs #if UNITY_IOS namespace Examples.View { /// <summary> /// iOS用の`IShareMedia`の実装 /// </summary> public sealed class ShareMediaForIOS : IShareMedia { public void ShareText(string text) => ShareTextForIOS(text); public void ShareImage(byte[] image) => ShareImageForIOS(image); public void ShareMovie(string moviePath) => ShareMovieForIOS(moviePath); #region P/Invoke // iOSのシェアUIからテキストをシェア [DllImport("__Internal", EntryPoint = "shareText")] static extern void ShareTextForIOS(string text); // iOSのシェアUIから画像をシェア [DllImport("__Internal", EntryPoint = "shareImage")] static extern void ShareImageForIOS(byte[] image); // iOSのシェアUIから動画をシェア [DllImport("__Internal", EntryPoint = "shareMovie")] static extern void ShareMovieForIOS(string moviePath); #endregion P/Invoke } } #endif ShareMediaForAndroid.cs (クリックで展開) ShareMediaForAndroid.cs #if UNITY_ANDROID namespace Examples.View { /// <summary> /// Android用の`IShareMedia`の実装 /// </summary> public sealed class ShareMediaForAndroid : IShareMedia { public void ShareText(string text) => ShareTextForAndroid(text); public void ShareImage(byte[] image) => ShareImageForAndroid(image); public void ShareMovie(string moviePath) => ShareMovieForAndroid(moviePath); #region P/Invoke // AndroidのシェアUIからテキストをシェア static void ShareTextForAndroid(string text) { // ネイティブのシェアUI呼び出し (省略) } // AndroidのシェアUIから画像をシェア static void ShareImageForAndroid(byte[] image) { // ネイティブのシェアUI呼び出し (省略) } // AndroidのシェアUIから動画をシェア static void ShareMovieForAndroid(string moviePath) { // ネイティブのシェアUI呼び出し (省略) } #endregion P/Invoke } } #endif ShareMediaForEditor.cs (クリックで展開) ShareMediaForEditor.cs #if UNITY_EDITOR namespace Examples.View { /// <summary> /// Editor用の`IShareMedia`の実装 /// </summary> public sealed class ShareMediaForEditor : IShareMedia { public void ShareText(string text) { Debug.Log("Editorからの呼び出し"); } public void ShareImage(byte[] image) { Debug.Log("Editorからの呼び出し"); } public void ShareMovie(string moviePath) { Debug.Log("Editorからの呼び出し"); } } } #endif ExampleView.cs (クリックで展開) ExampleView.cs namespace Examples.View { /// <summary> /// シェア機能の呼び出しサンプル /// </summary> sealed class ExampleView : MonoBehaviour { [SerializeField] InputField _shareText = default; IShareMedia _shareMedia; void Start() { // プラットフォームに応じて実装を差し替える // NOTE: サンプルなので雑に分岐しているが、実際にやるならDIフレームワーク経由で注入しても良いかもしれない #if UNITY_EDITOR _shareMedia = new ShareMediaForEditor(); #elif UNITY_IOS _shareMedia = new ShareMediaForIOS(); #elif UNITY_ANDROID _shareMedia = new ShareMediaForAndroid(); #else // 非対応プラットフォームなら投げておく // NOTE: 非対応プラットフォームでも動かしたいなら`IShareMedia`を実装したダミークラスを用意して入れておくのも手 throw new NotImplementedException(); #endif // 入力されたテキストのシェア _shareText.onEndEdit.AddListener(text => { _shareMedia.ShareText(text); }); } } } 利点 この形式にすると実装の詳細がプラットフォームごとの実装クラスに委譲されるので、仮に「プラットフォームごとに合わせたデータ変換」と言ったロジックが挟まることになったとしても、処理のスコープを限定できるようになります。 (「iOSで必要な処理はiOSの実装クラス内に」「Androidで必要な処理はAndroidの実装クラス内に」と言った感じにスコープを限定できる) 他にもプリプロセス命令による分岐が初期化タイミングのみとなっているために、全体的に見通しも良くなっているかと思います。 保守性と拡張性 もし対応プラットフォームが増減したとしても既存のコードに対する影響範囲を抑えられます。 (増えた際には実装クラスを追加して初期化時のプリプロセス命令の分岐に追加するだけで済む。減った際にはプリプロセス命令の分岐から消すだけで済む) 他にもIShareMediaを実装したダミークラスを用意することで非対応プラットフォーム実行時/Editor実行時などの振る舞いを分けやすくなったり、ソフトウェアテスト用にMockクラスを実装して挟むと言ったことも対応しやすくなります。 DIフレームワークとの相性 今回の例ではプリプロセス命令でプラットフォームに応じた実装クラスを流し込む形になってますが、実装自体はinterfaceを切っているのでZenjectと言ったDIフレームワークとの親和性も上がります。 仮に他のViewでもシェア機能を呼び出したいとなったときに利便性が上がります。 サンプルプロジェクト ここまでの流れのおさらい用にサンプルプロジェクトを用意しました。 内容としては「端末のバッテリーレベル(容量)を取得して画面に表示する」と言ったものであり、iOS/Android/Editorに対応してます。 (※Editor実行時は常に100%が表示される) サンプルは「design-exanmple-objc++」ブランチにて管理してます。 詳細な実装内容は前の章にて解説した通りなので省きますが、宜しければ一例として御覧ください。 ※備考 ちなみに..こちらのサンプルは説明のために意図的に車輪の再発明をしたものであり、実際にバッテリーレベルを取得するぐらいであれば以下のAPIから取得可能だったりします。 SystemInfo.batteryLevel ここまでのまとめ 大事なポイントとしてはinterfaceを切ってプラットフォームごとに実装を分けることです。 可読性/保守性/拡張性が上がり、DIフレームワークとの相性が良くなっていくるかと思います。 後はinterfaceを切ると言っても慣習的に必ず切る必要は無く、例えば冒頭にも記載したようにサンプル実装や簡単な機能検証程度ならプリプロセス命令で雑に分岐して直接呼び出してしまうのも有りかと思います。 ViewからViewを呼び出すような構成になっているが、このサンプル中ではそこに強い意図は無い。各プロジェクトごとに採用しているアーキテクチャに合わせて呼び出し元をViewなりPresenterなりと変えていけば良いと思う。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む