うどんてっくメモ

技術的なメモをまったりと

UI Builderを活用したUnityのUI構築

Unityでは最近UI Toolkitと呼ばれる新しいUIシステムの開発が進んでいます。
今日はそのUI Toolkitの一つであるUI Builderの説明とその使い方について紹介します。

UI Toolkitの構成要素

UI ToolkitではUIの要素を分解したいくつかのファイルによって構成します。見た目をUSS、レイアウト構造をUXML、ロジックをUQueryとC#、といった形で責務を分解して実装を行うのが特徴です。 名前からもそれとなくわかるのですが、USSがCSS、UXMLがXMLやHTML、UQueryがJQueryから着想を得ており、Webライクな設計となっています。 責務が分かれていることによって、従来のエディタ拡張で使われていたIMGUIの難点だったロジックとUI構成が一つのC#に入り混じって煩雑になってしまうという点が解決されました。 現在はエディタ上のUI実装でのみ正式リリースとなっていますが、将来的にはランタイムのUIの実装にもUI Toolkitを使うことが公式の展望として発表されています。

UI Builderによる要素の構成

UI Toolkitの要素の中でもUSSとUXMLは見た目の部分であり、C#とは別に実装を行います。もちろん自前でコードを書いてもいいのですが、そこで便利なのがUI Builderです。 UI BuilderはUI Toolkitにおける見た目の部分をGUI操作で作成することができるオーサリングツールになります。 Unity2020まではpreviewの機能でPackage Managerで提供されていましたが、Unity2021からは標準で使えるようになっています。 Package Managerから導入する場合はpreviewのpackageを有効にして以下の画像のpackageを入れてください。

f:id:myudon:20210107215102p:plain

導入するとメニューのWindow -> UI Toolkit -> UI BuilderからUI Builderの画面が開きます。

f:id:myudon:20210105005155p:plain
UI Builder

左のメニューでUIの要素の追加や選択をGUI操作で行い、右のインスペクターで選択したUIの編集を行います。 中央のプレビューは即座に更新が反映されるようになっており、Prefabの編集のような感覚で扱うことが可能です。 順にUI Builderの使い方について説明します。

UIの要素と階層構造(UXML)の作成

まずは要素の追加です。使うのは左下のLibraryです。

f:id:myudon:20210107003323p:plain

Libraryから追加したい要素を選んでクリック、または任意の追加したい場所にドラッグを行います。 試しにButtonを追加します。選択してクリックします。

f:id:myudon:20210107003348p:plain

真ん中のプレビューを確認すると画像のようにButtonの要素が追加されます。

f:id:myudon:20210107003553p:plain

そして左のHierarchyにも追加した要素が反映されます。 Hierarchyはその名の通りでUXML上で定義される階層構造を調整できます。

f:id:myudon:20210107004341p:plain

基本的にはLibraryからContainerに分類されている親となる要素を敷いて、その子にControlsと分類されている要素を追加していくのがよくある工程かなと思います。 また、Libraryには自分の作成したUIもひとつのパーツとして扱うことが可能です。LibraryのタブをProjectにするとAssets配下にあるUXMLを参照することができます。

f:id:myudon:20210107004851p:plain

似たようなUIや派生系のUIを量産したい時に、パーツごとにUXMLを制作してLibraryから参照することで制作の手間を省くことができるのはかなり嬉しい機能です。 また、UIを選択すると右のInspectorで名前といったパラメータや使うUSSの編集を行うこともできます。インラインで埋め込む装飾もここで編集してプレビューで確認できるのでとても便利な機能です。

f:id:myudon:20210107013222p:plain

こうして制作したUIの階層構造はUXMLに変換されます。下のプレビュー表示で確認が可能です。

f:id:myudon:20210107005129p:plain

後はUXMLとして作ったUIを保存します。上部にあるFileメニューのsaveからもできますし、ctrl + sやcmd + sといったショートカットキーでも可能です。 UXMLの名前と保存場所を入力して保存しましょう。以上でUIの要素および階層構造の定義となるUXMLが作成できます。

UIの見た目(USS)の作成

次にUIの装飾となるUSSの作り方を説明します。基本的には参照するUSSの設定と、Inspectorによる要素ごとの見た目の操作になります。 使用するUSSの設定は左上のStyleSheetsのメニューで行います。

f:id:myudon:20210107010514p:plain

左上の+ボタンから新しくUSSを作成したり、Assets配下にあるUSSを読み込むことができます。 USSの記述として装飾の効果の対象を表すセレクタの追加もGUIで行えます。 対象のUSSを選択して右のInspectorからセレクタの名前を入力してボタン一つで追加されます。

f:id:myudon:20210107011530p:plain

追加したセレクタはStyleSheetのメニューに反映されます。それぞれのUSSの下に表示されるので分かりやすいです。 セレクタごとの効果はInspectorで設定します。プレビューに即座に反映されるので見た目の試行錯誤はかなり捗ります。

f:id:myudon:20210107011950p:plain

そして、UXMLと同様に設定に合わせてUSSに変換されます。下のプレビュー表示で確認が可能です。

f:id:myudon:20210107220111p:plain

後はUXMLの時と同様に保存を行うとUSSが保存されます。 以上がUI BuilderでUI構築を行う手順になります。

最後に

UI ToolkitによってUIの見た目とロジックの実装を分離できるようになり、UI Builderを活用することでその見た目をGUIベースで構築できるようになりました。 まだまだUI Builderは発展途上なところもありますが、中規模な開発でUIの再利用や量産が必要な時には活躍する時もくるツールじゃないかなーと感じています。 UI ToolkitはUnityは力を入れて開発を進めている部分の一つでもあるので、今後の動きにも注目していきたいですね。

2020年の振り返りと2021年の抱負

あけましておめでとうございます。2020年は仕事にリアルにいろいろなことがありました。 2020年にやったことを振り返りつつ、2021年頑張ることを書いていきます。

2020年やったこと

Qiita投稿 1本

Unity Game SimulationによるUnityアプリケーションの高速シミュレーション - Qiita

Qiita投稿はこの1本のみでした。ちょっと怠けてしまったなー、という感じです。 ただ、この記事自体は結構自信があった内容で、記事を投稿した後にUnity公式の方からメールでお話をいただいたりしました。 Unity Game Simulationは結構可能性を感じる機能なので今後も注目していきたいと思います。

会社ブログ 2本

creator.game.cyberagent.co.jp

creator.game.cyberagent.co.jp

SGE(CyberAgentのゲーム・エンタメ系の子会社のグループ)の公式ブログにて2本記事を書かせていただきました。 今年はこの社内ブログの寄稿を結構頑張っていて、所属する子会社の技術ブログがなかったのを立ち上げたりしてました。 2本とも記事としてはなかなかガッツリとしたものが書けたのではないかなと思っております。

技術書典への執筆 2回

技術書典で「UniTips」というUnityの技術書を定期的に会社で出しているのですが、2020年も2冊執筆に参加しました。

creator.game.cyberagent.co.jp

creator.game.cyberagent.co.jp

Airtestによる自動テスト、UPRによるプロファイリングといったちょっとニッチな内容について執筆しました。 技術書典には「UniTips」立ち上げから本を出させてもらっており、昔に比べて技術を文章化する速度や精度も上がっているのを感じています。 今後も「UniTips」の執筆は続けていく予定です。後述していますが、個人的な本の執筆も予定しています。

CEDEC公募に挑戦

業務で色々と技術的に挑戦していることがあって、せっかくならこれを元に公募チャレンジしたい!と思って頑張ってました。 結果的には通らなかったのですが、公募の感触や社内で登壇経験がある人のヒアリングなど、得るものは多かったです。 もちろん全然諦めていないので、挑戦し続けていきたいです。

Rustを始める

色々と技術的に手広くなりたいなと思って、前々から興味のあったRustを始めました。 公式のチュートリアルから始めて、bitFlyercliを実装してみたり、簡単なAPIサーバーを実装してみたり、ゲームを実装してみたり、と実際に物を作りながら学んでいます。 特にゲーム作りで今は勉強を進めており、AmethystというRust製のゲームエンジンをよく触っています。

amethyst.rs

まだまだ初心者ですが、GitHubにもこっそり成果物を上げています。

github.com

2021年の抱負

アウトプット強化

これまでもQiita投稿、技術書典参加、会社ブログの執筆など色々とアウトプットを続けてきましたが、以下の点を2021年は頑張ろうと思います。

  • 技術書典への「UniTips」の執筆継続
  • 技術書典で個人的にRustの本を出す
  • 個人ブログ蘇生、技術記事のアウトプットを2020年よりも増やす

大きいのは個人的に本を出したいという点です。「UniTips」の執筆を通じて得たノウハウを個人的な研鑽にもつなげたいと思ってRustの本を出してみよう!となりました。 技術書を出すことで知識精度をより高めたり、技術的な名刺になったりなどのさまざまなメリットがあるため、個人的にはおすすめなアウトプットの一つです。

また、この2018年から2年ほど更新が止まっていた技術ブログを蘇生したのも思い切った点です。 2020年色々な方のアウトプットを見たり、社内で活発にアウトプットされている方と話す機会があったりして、自分のアウトプットが定点的で見直さないとなと感じることが多かったです。 そこで、今年から技術ブログを蘇生して自分の名刺としてもアウトプットを積み重ねていくことにしました。記事も増やしつつ、より有益な情報をお届けできるよう頑張ります。

技術領域を広げる

自分は学生の頃から2019年までずっとUnity中心の勉強と業務を続けていました。 2020年になって社会人3年目ともなると仕事の幅も広くなり、Jenkinsおじさんになったり、自動のUIテスト環境を構築したりなど、Unity以外の知識を多く使うようになりました。 結果として自分の技術領域が狭く、もっといろいろな技術に触れた方がいいなーと感じる機会がかなり増えました。そこで、2021年には技術でやれることを増やす、という意識をしっかり持っていきたいです。 直近ではAirtestでの自動テストだったり、Rustの習得だったり黙々と勉強中です。アウトプットと絡めていきたいですね。

最後に

2020年は業務の幅も広がり、色々なエンジニアの方のアウトプットを目にしたりするようになって、色々と自分の活動を見直すいい年でした。 まだまだひよっこエンジニアですが、少しでも技術で世の中に貢献できるよう頑張っていきます。 今年もよろしくお願いいたします!

会社の有志の方と技術書典に本を出してきました

執筆日でもある10月8日にに技術書典5が開催されました!
参加者も出展サークルもぐんぐん増えて、会場が秋葉原UDXから池袋サンシャインシティ文化会館にお引越し、来場者1万人出展サークルの人数も800人越えと今までに無い大きな規模での開催となりました。

f:id:myudon:20181008231806j:plain

ありがたいことに自分は会社のエンジニアの有志の方々と共著を出せる機会があったため、出展サークル側として参加してきました。
UniTipsというUnityの開発にまつわるTips集の執筆と、サークルとしての運営及び進行をさせていただきました。

当日は多くの方が自分たちのブースに来てくれました!前回のUniTipsについての感想などをくださる方もいて、運営としてとても嬉しかったです!

f:id:myudon:20181008231638j:plain

会社として本を出すということで、せっかくなら仕事で学んだ知見をアウトプットしたいなと考え、以前自分が参加していたプロダクトで使用していたZenjectについての章を執筆しました。

現在のプロダクトでのタスクが忙しい時期ということもあり、切羽詰まったりもしましたが、早い時期から書き溜めていた結果何とか執筆を完了することが出来ました。
この技術書典を通して良かった点と反省点があったので少し書きとめようと思います。


まず1つ良かった点は、アウトプットを通した学びが出来たことです。

何かをアウトプットすることは、自分がインプットしたことをアウトプットできる形にする工程を通すことで初めて出来ることです。
そのアウトプットの成型の過程で、自分の理解の中で何が曖昧かを見つけることが出来ます。そして曖昧である部分を丁寧にインプットしなおすことで理解を深めることにつながります。自分もこの執筆を通してZenjectに対する理解をより深めることが出来ました。

また、先輩エンジニアと共著する形をとることで、自分の技術的な知見を言語化する良い経験をすることが出来ました。

今回は執筆フローとして全体のリポジトリgithubで管理し、各々が記事を執筆してPRを出し、全員のレビューを経てからマージするという形を取りました。 自分は技術的な部分を言語化し相手に伝えることに苦手意識があったため、先輩エンジニアの方々に自分の文章にレビューをいただくのはとてもいい勉強の機会になりました。

反省点は、イベント運営としての立ち回りです。

今回は執筆と合わせて、運営もやらせていただいたのですが、自分の至らない点が多く、先輩のエンジニアの方々に手間を取らせてしまうようなことがありました。
自分はエンジニア寄りのクリエイターを目指しており、こういったアウトプットをする場での運営や進行などはしっかりこなせないとなあと目指す自分に足りない部分をしみじみと自覚しました。
こういったことで割としょげがちなのですが、次のこういったイベントで経験を活かして行きたいです。

今後もこういったアウトプットの機会には積極的に参加していきますし、自分も会社の技術活性化のためにいろんな施策を提示したり運営出来るよう頑張っていこうと思います。
次の技術書典もこういった形で参加したい!と画策中です。エンジニアとしての経験が足りないからこそ、どうしたら少しでも早く経験値を得られるかを模索していきます。
とりあえず今日は1日中立っててへとへとなので、買った本を読みながら寝ようと思います。

ShaderGraphを使って頂点シェーダーで遊ぶ

この記事はUnityゆるふわサマーアドベントカレンダー 2018の22日目の記事になります
21日目の記事は@am1tanakaさんのNavMeshAgentでよい感じにキャラクターを歩かせるでした!

qiita.com

Unity2018.2に付随するShaderGraphのアップデートとしてついにvertex shaderの適応が可能になり、頂点情報がいじれるようになりました
今回は早速その機能を使い、簡単な頂点シェーダーをShaderGraphで作って遊んでみたいと思います


ShaderGraph

もうすでに多くの人が知っているかと思いますがShaderGraphはUnity2018から導入されたノードベースのシェーダーエディタです
従来のUnityではシェーダーを実装するにあたってShaderLabによるコーディングが必要でしたが、これによってビジュアルベースでシェーダーを構築できるようになりました
UEは勿論のこと、MayaやSubstanceBlenderなどのDCCツールを活用していた方などにとっては喜びの声が大きいものでした。最初はノードの種類も少なく表現の幅や使い心地に難があったのですが、様々な改修があり、Unity2018.2ではバージョンは3.0となりました。今後も期待ができる機能の一つです

使ってみる

f:id:myudon:20180822002929p:plain

使い方は簡単で、マスターノードに追加されたPositionに頂点情報となるVectorをつなぐだけです

f:id:myudon:20180822002202p:plain

まずは簡単な例です、ワールド座標に対して時間変数の正弦余弦を加算させてぐるぐるさせます

f:id:myudon:20180822020604g:plain

ボールがぐるぐる動いてますね、もうちょっと遊んでみます

qiita.com

上記のサイトにかなり簡易的な草の動きを実装した頂点シェーダーがあるので、ShaderGraphで組んでみます

f:id:myudon:20180822015303p:plain

草が風でそよそよする感じの動きですね
頂点情報について、UV座標で重みをつけたアニメーションを行います
頂点情報をいじっている部分をピックアップしてみましょう

f:id:myudon:20180822223821p:plain

ノードとしてはUVからパラメータを引っ張ってきて、定義したプロパティノードから各種必要な定数を引っ張ってくる感じです
UVから引っ張ってきたy値と定数パラメータを用いて頂点のx成分の移動値を作成し、座標に加算しています

これを適当な板に貼り付けて動かしてみるとこうなります、寂しいので3つぐらい並べてみました

f:id:myudon:20180822020625g:plain

時間に合わせて草が揺れるようになりました
手軽にササっと作れて、動きとしてアウトプットを確認できるので学びとして楽しいですね
今回は単純な動きを作成しましたが、物理挙動に紐づく波の動きだったり動的な凹み表現だったり遊べるものは多いので興味が湧いた方は遊んでみてください


いかがでしたでしょうか、少し緩めの記事ですが、シェーダーって触ったことないけどShaderGraphちょっと面白そう!などちょっとしたきっかけになればいいかなと思っています
個人的にShaderGraphでかなりシェーダーの技術的な敷居は落ちたかなーと感じているので、これを機にエンジニアは勿論、クリエイターからの表現のアプローチが増えていくことを期待しています、自分もいろいろ遊んでいる最中なので面白いものがあったら共有していきたいですね

Unity新Prefabワークフロー、Prefab Variantsの紹介

この記事はUnityゆるふわサマーアドベントカレンダー 2018の13日目の記事になります
12日目の記事は@sonoichi-60さんのリフレクションについてまとめたでした!

qiita.com

Nested Prefabの標準搭載など、Unity BerlinにてPrefab周りについてのワークフローが改修が発表されました
機能としては2018.3に搭載予定らしく、プレビュービルドが公開されています

unity3d.com

今回はそのPrefabシステムの一つ、Prefab Variantsについての記事です


Prefab Variants

Prefab variantsは所謂プレハブの継承的なシステムです
あるPrefabからのパラメータ継承と、要素のオーバーライドを実現することが出来ます
ベースプレハブの変更に伴う派生先の更新や、複数のPrefab継承先なども可能で、Unityにおける開発フローで効率を上げることができます
UnityがPrefabシステムのマニュアルを公開しているので、興味がわいた方は下記のリンクから読んでみてください

docs.google.com

実践

使う場合は上記のプレビュービルドのリンクからプレビュー版を落としてきてください

まずベースプレハブを作成します、今回は適当にUIプレハブを作成しベースにします
プレハブ上で右クリックし、Create→Prefab Variantsで作成できます、作成したプレハブは矢印の模様がついたプレハブアイコンになります

f:id:myudon:20180805043806p:plain

作成したPrefab Variantsは同じようにシーン上で自由に設定ができます
ベースとの差分などはプラスアイコンなどで表示され、無論variantとしての保存ももちろんメニューからそれをベースに反映することもできます

f:id:myudon:20180805044321p:plain

ベース側が変更をかけた場合もしっかりvariantへと反映されます、上記のスクショみたいに差分でApplyを行うのですが、これまでみたいに変更を一括でインスペクタから反映させることもできます
その場合はApplyの時に変更点が出るようになったので確認しましょう

f:id:myudon:20180805045225p:plain

違う派生元からのベース修正もしっかりと反映して運用できます、逆に言えば様々なプレハブへの変更なども考慮しないといけないので使い方に注意は必要になってきます
派生先でNested Prefabなど複雑になってくると事故が起きるかもですね

また、ここまでのプレハブの編集について、シーン上でコンポーネントなどをいじることも可能ですが基本的にはPrefab Modeでの編集をすることになります
シーン上でプレハブの関係を壊すような操作は許可されていません、削除などの操作を試みようとするとUnityに怒られます

f:id:myudon:20180805051322p:plain

プレハブをOpenする操作を行えばPrefab Modeでプレハブを開いてくれます
Prefab ModeではPrefabの関係操作や更新を行うことが出来ます、Nested Prefabの場合はその個々のプレハブについて編集画面にジャンプすることが出来ます

f:id:myudon:20180805052516p:plain

Nested Prefabで差分を出した場合は反映先を選択することが出来ます、複雑化してくると管理が難しいかもですが操作の意味合いが明示化されていて便利ですね

f:id:myudon:20180805052710p:plain

また普通にオブジェクトとしてBreak Prefab Instanceして使いたいなってときはUnpack Prefabを使用しましょう
UnpackだけだとNested Prefabは無視してプレハブ接続を解除、Completelyのオプション付き操作を行うことで全てのプレハブ接続を解除します

f:id:myudon:20180805053114p:plain

ざっと現状のPrefab Variantsの機能を説明しました、詳しくは公式Manualを見ながら自分で触ってみるといいでしょう


Nested Prefabの公式導入、Prefab Variants、Prefab Modeなど触っていて大分ワークフロー変わったなぁと感じました
便利なのですが、しっかりと操作や効率化を念頭に入れて学習していかないとなって感じです
今後も変更が入っていくと考えられるのでこの先の動向に注目です

IncrementalCompiler時代のUnityC#Tips(C#7.2)

この記事はUnityゆるふわサマーアドベントカレンダー 2018の6日目の記事になります
5日目の記事は@splas_boomelangさんのUnity用のパッケージ(アセット)を配布する時のお作法でした!

qiita.com

Unity2018.1以降からIncrementalCompilerがPreview版として公開されるようになりました
まだUnity開発界隈では6.0の機能を徐々に会社の実プロダクトレベルで実験的に活用し始めたぐらいのフェーズかなと思いますが(async/awaitがじわじわと広まりだしたぐらいかな?) 、IncrementalComplierを導入することでC#7.2までの機能を使用することができます
6.0のasync/awaitレベルのUnity開発に対する革新的な変更はないため、そんなにインパクトがあるわけではないのですがちょっとしたTipsとしてUnity開発に使えそうなものをちょこちょこっと紹介します


IncrementalCompiler

まずIncrementalCompilerについて軽くお話します
IncrementalCompilerとは2014年よりMicrosoftオープンソース化した次世代コンパイラ「Roslyn」をUnity内で用いることでコンパイル時間の削減とC#7.2までの機能の使用を可能にするものです
上述にもあったようにまだPreview版でバージョンも0.0.42-preview,16と試験段階かなと言わざるを得ない状態です、これにより今のところ運用を見送っているプロジェクト等もあるのではないでしょうか
自分が個人規模の開発で使用してみた感じは特に問題は起きていないため、使用を続けています、その内Unityのアップグレードに従って標準環境になっていくことは見えているので早いうちから慣れておくのは悪くない選択肢かなと

Unity Incremental C# Compiler - Unity Forum

Tips

それではIncrementalCompiler導入によって使用できるC#機能の紹介です

タプル

タプルとは簡潔に説明すると複数のオブジェクトを1つのオブジェクトとしてまとめたものです 元々.Net4で導入されていたもので、パッケージとして導入し使用することは出来たのですがC#7よりC#の正式な機能として導入されるようになりました
Tuple<T1, T2>みたいな感じでジェネリクス保有する型を定義して使用します
複数の値を戻り値として受け取れるため便利な機能の一つです、自分はよく使用しています
.NetのTupleだとアクセサが固定でitem1といった抽象名でしか扱えずコードが見づらくなりがちでしたが、C#では名前を定義して使うことができます

// タプルの定義
(int age, string name) tuple = (10, "udon");

// タプル値の参照
Debug.Log(tuple.age); //10
Debug.Log(tuple.name);

// タプルで返す
(bool success, int result) Sum(int x, int y)
{
    if(x > 100 || y > 100)
    {
        return (false, -1);
    }
    else
    {
        return (true, x + y);
    }
} 

またタプルで受け取った複数のオブジェクトに対してそれらを分解する機構も作られています
上記のSumのように定義したタプルを変数宣言することなく個々の戻り値を使えるわけですね、タプルを使用するケースではタプル自体に命名する意義がない場合が多いので嬉しい配慮です

(var success, var sum) = Sum(100, 100);
Debug.Log(success); //true
Debug.Log(sum); //200

出力変数宣言

出力引数を受け取る際、事前に変数を宣言し受け取り口を作ってからそれを指定する必要がありましたが、C#7からは式の中で受け取り口の変数宣言を行うことが出来るようになりました
これに関しては劇的に使用するわけではありませんが、スマートに書けるので身に着けときましょう

Dictionary<string, string> hogeDictionary;

hogeDictionary.TryGetValue("hoge", out var value);
Debug.Log(value);

async T

async/awaitは確かに強力なソリューションの一つですが、そのネックの一つとして返り値がTaskに縛られてしまうというものがありました
特にTaskとしての機能が要らないようなケースでもasyncさせるためにはTaskとして宣言する必要があり割とパフォーマンス的な観点で見るとおやっ?ってなったり、シンプルな値返還の形を作るだけでも若干await操作がめんどくさかったりしました
C#7.0からは任意の型に対してasync宣言が行え、それをawaitで待機できるようになりました
ここで具体的な説明をしてしまうとそれだけで一つの記事になっちゃうかもなので詳しくは下記のリンク先のtask-likeについての項目を読んでみてください

非同期メソッド - C# によるプログラミング入門 | ++C++; // 未確認飛行 C


ざっくり自分が使用したり、知識として抑えているものを紹介しました
この他にも様々な機能の追加がされているので、IncrementalCompilerを個人で試しに使ってみてください(特にUniRx6等はバリバリ使用しているので見てみるといいかも)

C#MessageBoxのようなダイアログをコルーチンみたいな感じで作ってみる

この記事はUnityゆるふわサマーアドベントカレンダー 2018の2日目の記事になります
1日目の記事は@splas_boomelangさんのOculusGoのコントローラーをUnity上でエミュレートするでした!

qiita.com

ゲーム開発を行う上で、思わぬ部分でネックになりやすいのがアウトゲームです
中でもゲーム中でもよく表示するダイアログ、ソーシャルゲームを開発する上で数えきれないほど実装することになると思います
どんな場面でも必要になり様々なロジックを担保するダイアログ、この設計が手を抜くと落とし穴(カオスコード)にはまりやすいです
今回はそのダイアログの作りの一つとしてC# MessageBoxを参考にしたダイアログについて説明します


C# MessageBox

C# MessageBoxは.NETFrameworkに搭載されているダイアログ表示用のクラスです

MessageBox クラス (System.Windows.Forms)

Windows民ならよく見るこれを作ってるやつです

f:id:myudon:20180801015734g:plain

MessageBoxでは、ダイアログ自体はロジックを持たず呼び出す側が担保します
呼び出し側がダイアログを展開し、DialogResultで結果待ちをし、その結果を利用してロジックを持つ呼び出し側が処理を行うわけです
聞いてみると普通じゃんって感じがすると思うのですが、アウトゲームを作っていくうえでロジックの所在などをあまり気にせず動くものを作る意識で開発を進めていくと割とダイアログがロジックを持つパターンで実装しがちかなって思います
あまり責務を分散させずにスマートなUI処理がしたいなと思い、これをコルーチンチックに実装してみました

実装

今回紹介するのはシンプルなダイアログのケースです
ダイアログを展開し、ユーザーからのポジティブ/ネガティブな結果を受け取りダイアログをクローズ、処理に移行するという形になります
具体的なケースとしては処理確認系ダイアログが近いでしょう、〇〇しますか?と聞いてはい・いいえで答えるようなやつですね

下記が実装コード、まずはダイアログ本体です
シンプルに形成したものなので各々が使う形に実装をアレンジしていただければと思います
実装の特徴としては簡単なダイアログの実装に加えて待機用のクローズフラグとダイアログのリザルトを含めてあります

public class ExampleDialog : MonoBehaviour {

    /// <summary>
    /// ダイアログそのもののRectTransform
    /// </summary>
    [SerializeField]
    private RectTransform _dialogBody;

    /// <summary>
    /// ダイアログに表示する文章
    /// </summary>
    [SerializeField]
    private Text _description;

    /// <summary>
    /// はいボタン
    /// </summary>
    [SerializeField]
    private Button _positiveButton;

    /// <summary>
    /// いいえボタン
    /// </summary>
    [SerializeField]
    private Button _negativeButton;

    /// <summary>
    /// 処理を終えてクローズしたかの判定
    /// </summary>
    private bool _isClosed;
    public bool IsClosed => _isClosed;

    public DialogResult Result { get; private set; }

    /// <summary>
    /// プレハブからダイアログ作る
    /// </summary>
    /// <param name="parent">吊るす親</param>
    /// <param name="description">ダイアログに表示する文章</param>
    /// <returns>ダイアログのインスタンス</returns>
    public static ExampleDialog CreateDialog(RectTransform parent, string description)
    {
        var dialog = Instantiate(Resources.Load<ExampleDialog>("ExampleDialog"), parent);
        dialog.Initialize(description);
        dialog.Open();
        return dialog;
    }

    /// <summary>
    /// 初期化処理
    /// </summary>
    /// <param name="description">ダイアログに表示する文章</param>
    private void Initialize(string description)
    {
        _description.text = description;
        Result = new DialogResult();
    }

    /// <summary>
    /// ダイアログオープンアニメーション
    /// </summary>
    private void Open()
    {
        _dialogBody.DOScale(1f, 1f).OnComplete(() => SetEvent());
    }

    /// <summary>
    /// ダイアログクローズアニメーション
    /// </summary>
    private void Close()
    {
        _dialogBody.DOScale(0f, 1f).OnComplete(() => 
        {
            _isClosed = true;
            Destroy(gameObject);
        });
    }

    /// <summary>
    /// ダイアログボタンのイベント処理、リザルト確定とクローズ
    /// </summary>
    private void SetEvent()
    {
        _positiveButton.onClick.AddListener(() =>
        {
            Result.ResultType = DialogResult.ResultTypeEnum.POSITIVE;
            Close();
        });

        _negativeButton.onClick.AddListener(() =>
        {
            Result.ResultType = DialogResult.ResultTypeEnum.NEGATIVE;
            Close();
        });
    }
}

次にダイアログの結果となるクラスです
ここネガポジをリザルトとして定義して持たせていますが、個々のダイアログで得体結果は違ってくるのでそこは応用が必要になってきます
各々のUIの基盤やシステム構想、ダイアログの使用箇所などと相談して工夫を加えると良いでしょう

// ダイアログの結果
public class DialogResult
{
    public enum ResultTypeEnum
    {
        NONE, // 何もなし
        POSITIVE, // はい
        NEGATIVE, // いいえ
    }

    /// <summary>
    /// ダイアログの結果タイプ
    /// </summary>
    public ResultTypeEnum ResultType;

    // ほかに必要そうなリザルトはここに置く、インターフェイスを定義して応用しても良い
}

最後にダイアログのクローズ及び結果を待つカスタムコルーチンです
特に変わった実装はしておらず、シンプルにダイアログのクローズを監視しています
ここもダイアログの仕様などでアレンジが入る箇所ですね

// ダイアログを待つカスタムコルーチン
public class WaitForDialogClose : CustomYieldInstruction
{
    public DialogResult Result => _dialog.Result;

    private ExampleDialog _dialog;

    public override bool keepWaiting
    {
        get
        {
            // ダイアログがクローズするまで待つ
            return !_dialog.IsClosed;
        }
    }

    public WaitForDialogClose(ExampleDialog dialog)
    {
        _dialog = dialog;
    }
}

以上が今回のダイアログのシンプルな形での実装例です
全てのダイアログに対して汎用的に組み込むことができるかと言われると怪しい部分もありますが、臨機応変に応用しましょう


実験

private IEnumerator Test()
{
    var dialog = ExampleDialog.CreateDialog(_canvas, "ほげほげ?");
    var result = new WaitForDialogClose(dialog);

    yield return result;

    Debug.Log(result.Result.ResultType);
}

上記のようなコルーチンを走らせて挙動を見てみます

ダイアログの結果を受け取ることができました、後は受け取った結果を使って処理を行いましょう
また機会があればアウトゲームについてのこういった小さな技術共有をしていきたいと思います