20201124のUnityに関する記事は6件です。

【Unity】UnityLearnおすすめ講座 -初心者用-

Unity Learnとは

リンク: Unity Learn
Unity公式が提供している、Unity学習サイト。
初心者から上級者まで幅広く講座があるにもかかわらず、全て無料。
本記事はUnity学習歴1年になる私が、受講して良かったと思えるものを抜粋(コースタイプのみ)。当然まだ受けれていないものもあるので、随時更新する。他にもこれがいいよってのがあれば教えていただけると幸いです。

※Unity Learnでの学習は基本的に英語です。とはいえ、自動翻訳を使ったり、字幕を見ながら学習すれば大した支障はないと思われる。英語の勉強にもなるので、気負わずに。

1.0 基礎講座

1.1 Create with Code

講座リンク: Create with Code

これからUnityを始める初心者に最もおすすめ!
Unityのインストール方法から、簡単な3Dゲームを作成するまでを動画付きチュートリアル形式で学習できる。
この講座を受講すればUnityの基本的な動作、基本的な注意点はマスターできる。
image.png

1.2 Beginning 3D Game Development

講座リンク: Beginning 3D Game Development

3Dゲームを作成したい人におすすめ!
GameKitを含めた3つのプロジェクトを作成していきながら、3Dゲームの基礎が学べる。
Create with Codeと違い、こちらは簡易的なゲームを丸々一本作りながら学習ができる。
image.png

1.3 Beginning 2D Game Development

講座リンク: Beginning 2D Game Development

2Dゲームを作成したい人におすすめ!
こちらは2D版。3Dのものと同様に3つのプロジェクトを作成していきながら学習できる。
image.png

2.0 プログラミング講座

2.1 Unity C# Survival Guide

講座リンク: Unity C# Survival Guide

プログラミング初心者な人におすすめ!
Unityでゲームを作りたいけど、そもそもプログラミングができない人や、C#に触れたことのない人におすすめする講座。C#の基本的な機能から、応用的な機能までを動画でサンプルを作りながら学べる。
また、学習の途中で小テスト(Challenge)もあるので、アウトプットもできる万能講座。
ゲームでのC#機能(ラムダやLINQ等)の使用方法も学べるので、中級者にもおすすめ。
image.png

2.2 Beginner Programming: Unity Game Dev Course

講座リンク: Beginner Programming: Unity Game Dev Course

Unity C# Survival Guideを終えた人におすすめ!
Survival Guideよりは若干高度な内容となっているプログラミング講座。
座学的に学べる動画もついており、UnityでC#をどのように使うかを掘り下げられる。
難点は英語が聞き取りにくい講師の方もいること。画面を見ながら雰囲気を掴むように学習すると良い。
image.png

3.0 ゲームデザイン講座

3.1 Beginner Design: Unity Game Development Course

講座リンク: Beginner Design: Unity Game Development Course

ゲームデザインを学びたい人におすすめ!
ゲームを作りたいけれど、どのように構想を膨らましていけばいいかわからない、どの作業から進めればいいのかわからない場合に強く受講をおすすめする講座。
アイデアの固め方、タイムラインとリソースの作成方法、デザインドキュメントの作成方法、アウトラインの作成方法を学習することができる。
image.png

4.0 マネジメント講座

4.1 Growing your mobile game

講座リンク: Growing your mobile game

モバイルゲームを作りたい人におすすめ!
こちらはチュートリアル形式ではなく、どのようにモバイルゲームを作成するかの設計思想等が学習できる。初心者が闇雲にモバイルゲームを作っても非効率的だし、収益性が考えられてなかったりする。最初は先人たちの知恵を借りて、どのように作られているのかを学習するのが大事だと思う。
モバイルゲームで稼ぐという目標のある方には非常におすすめの講座になっている。
image.png

終わりに

本記事の注意点としては、チュートリアルだけをやっても身につかないということです。ここまで書いてきて何を言ってるんだと思われるかもしれないけど、チュートリアルだけをやるのは「できた気になっている」段階だと思ってます。実際に自分でゲームを作ってみて、壁にぶつかりながらリリースまで持っていくのが1番の学習になる気がします(もちろんバランスは大事だけどね...)。

今回は自分が学習してきたものだけに絞りましたが、
他にもいい講座があったら是非教えてくださいね!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity PlayGround Referenceガイド④

Unity Playgroundリファレンスガイド日本語訳版の内容をもとに、スクリプトの内容を一つずつ確認していきます。

Conditon RepeatとOn-Off

ConditionRepeatスクリプトは一定の頻度で処理を繰り返すスクリプトです。

Initial Delay:シーンファイルが読み込まれて何秒後に処理を開始するか設定できます。
Frequency;処理を繰り返す頻度を秒数で指定できます。

On-Offスクリプトはオブジェクトの表示と非表示を反転させるスクリプトです。よりプログラム的に言うと、Activeフラグをtrueまたはfalseに設定できます。

Object to Affect:対象となるオブジェクトを指定できます。
Just Make Invisible:このチェックをオンにすると、SpriteRendererのオン/オフを切り替えられます。つまり、目には見えないが実際には存在するという、透明人間的なオブジェクトにすることができます。

それでは、この2つのスクリプトを組み合わせて、一定時間で存在と消滅を繰り返す魔法の鍵をつくって見ましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity&Spine】Bounding Box Followerの情報がリセットされた時の対処法

過去に仕事でUnityでSpineを使用し当たり判定を付けたことがあったので思い出しながら実装していたが、なんかうまくいかなかったので備忘録として残しておこうと思いましたまる

UnityにSpineを使えるようにする説明をしたいところだが、文章量が多くなってしまうので割愛
参考リンク①
参考リンク②
Spineに当たり判定を付ける(公式

ここから本命

Unity上でSpineデータに子オブジェクトを追加し、Bounding Box Follower、Bone Followerを追加してゲームを実行してみるとなぜか当たり判定が表示されている画像と大きくずれている。というか座標がリセットされている。
調べてみると設定していたはずのプレハブのBounding Box Followerがリセットされている。

やってみたこと
・ボタンを押してBone Followerを追加しなおしてもなぜかゲームを始めるとリセットされる。
・数回子オブジェクトを削除して追加するという方法もやってみた。(ダメ)
1.png

対処法

結果としていうと予測の範囲でしかないが、シーンを保存していなかったからだと思う。
シーンをリロードしてゲームを始め直すとプレハブの当たり判定が正しくセットされていた。
プレハブが原因というのもあるかもしれないがよくわからなかった。
また再現などしたら調べて追記したいと思う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

あなたは魔法使いになれる。そう、Magic Leapならね

はじめに

みなさんどうもこんにちは。MRエンジニアのharuya_i と申します ?

ここでは、いま東京タワーで絶賛開催中?、そして12月26日(土)から新宿小田急百貨店で開催予定のcode name: WIZARDで体験できる魔法の一部である、妖精魔法をMagic Leap でどのように実装したかを書かせていただいております。

この記事は、Magic Leap Advent Calendar 2020 の9日目でございます。

目次

1. code name: WIZARD について
2. プレイヤーを魔法使いにさせる術
  2-1. あなたが魔法を当てたい物に手を伸ばしたその時、果たしてどのような線が描かれる?
  2-2. オブジェクトの選択はコライダー? それとも 、 、 、
  2-3. 「無意識で使えてしかも楽しい」を目指した魔法のデザイン
3. さいごに

1. code name: WIZARD について

code name: WIZARD について
「code name: WIZARD ってなんぞや」と思われる方がいるかと思いますのでサラリと説明させていただきますと、以下公式サイトの言葉をお借りしますと

「現実世界で、魔法使いになれる!?」がコンセプトの、XR謎解きエンターテインメント!!

会場を実際に歩いて本格的な謎解きに挑戦するアナログ謎解きパートと、Magic Leap1を使ったMR謎解きパートをお楽しみいただけます!

と、いうものでして「プレイヤーに魔法使いになって欲しい」という思いで作られたXR謎解きコンテンツです。
そしてこのcode name: WIZARD で使っているデバイスがMagic Leap です。

? のGIF画像が実際の体験イメージです
WIZARD_EP1体験イメージ.gif
プレイヤーは各妖精から提示される謎や試練を、妖精から与えられる魔法を駆使しながらクリアしていきます。

本記事でお話しするのは、イメージの途中にも出てくる妖精魔法(以下画像)の実装方法についてです
WIZARD_EP1魔法_イメージ.png
このピンクとかグリーンのパーティクル状の魔法です。
魔法陣の出ているプレイヤーの手から魔法が発射されます ✋

2. プレイヤーを魔法使いにさせる術

さて、ここからはどのように妖精魔法を実装したかを具体的にお話しします ?

code name: WIZARD の妖精魔法では、プロトタイプ作成時のさまざまな苦節を経た結果、魔法を飛ばすオブジェクトの選択と魔法の実行を同時 におこなっています。
そのため、妖精魔法を構成している要素は大きく以下の3つとなります。

 ① オブジェクト選択用のベクトルを計算する
 ② 魔法がオブジェクトに当たるか否かおよびどのオブジェクトを選択するかを計算する
 ③ 目標の場所に向かってどのような魔法を飛ばすか

これから話す2-1, 2-2, 2-3 の3章は、これらの構成要素にそれぞれ紐付けてお話ししています。

また、「プロトタイプ作成時のさまざまな苦節」につきましては、3. さいごにで説明させていただくMagic Leap Meetup vol.2 in Japan にてその詳細もお話しできればと思っております。

2-1. あなたが魔法を当てたい物に手を伸ばしたその時、果たしてどのような線が描かれる?

例えば、あなたが「どんな物でも引き寄せることのできる魔法」を使えるとしましょう。
手のひらをかざすだけで、どんな物でもあなたの近くに引き寄せることができます。

あなたがテレビを観ようとリモコンに魔法を当てたい時、あなたの手とリモコンの間にはどのような線が描かれるでしょうか?
もっと端的に言えば、あなたの手からどのような線を計算すればリモコンに当てることができるでしょうか?
thomas-griesbeck-9WWQWYmHBCk-unsplash.jpg
この疑問に対する1つの最適解が、Leap Motion社のブログに書かれた1つの記事で紹介されています。(この記事をはじめ、このブログで書かれている記事はとても面白く参考になることが多いので、XRに関わる方々には特にオススメいたします ?)

この記事では「VRにおける離れた物体とのインタラクションに関するデザイン」について書かれています。
そのなかで、手を入力にして遠くのものを選択する場合「単に手の向きを使うのではなく、頭(HMD)からおおよその肩の位置を推測してその肩から手のひらを通る線を使う」という旨の記述があります。

https://blog.leapmotion.com/summoning-superpowers-designing-vr-interactions-distance/ より)

実際に、手の向きや頭(Magic Leap)の向きをもとに計算したことがありますが「手の向きだとノイズが多すぎる上に全然狙ったところに飛ばない」「頭の向きだとある程度狙ったところに飛ぶが、狙ったオブジェクトが体に対して正面にないときに当たらない」
といった問題がありました。

code name: WIZARD でもLeap Motion社の記事で紹介されているように、頭の位置からおおよその肩の位置を推測してその肩と手を結ぶベクトルを利用することで、狙った物と手を結ぶおよそ理想的なベクトルを得ることができました。

2-2. オブジェクトの選択はコライダー? それとも 、 、 、

さて、プレイヤーの肩の位置を推定することで① オブジェクト選択用のベクトルを計算する ことができました。
次は、② 魔法がオブジェクトに当たるか否かおよびどのオブジェクトを選択するかを計算する 方法についてです。

code name: WIZARD では当初、先ほどのLeap Motion社の記事と同じように、手から出る選択用のベクトルの方向にレイを飛ばし、そのレイに当たったコライダーを持つオブジェクトを選択するという方法を使っていました。

しかし、この方法だといくつか問題があったのです。

1つは、オブジェクトが重なった時に奥のオブジェクトに当てにくくなるということです

図で説明します。
以下のようにcode name: WIZARD を体験中のマジクリくんの前に、コライダーを持つオブジェクトAとオブジェクトBがあったとします。(以下図1参照)

図は上から見た図であり、XZ平面になります。

アートボード – 1.png

コライダーを当たり判定とした場合、コライダーが小さすぎると魔法を当てることが困難になるため、オブジェクトがよほど大きくない限り実際のオブジェクトのメッシュよりもマージンをとって大きくする必要があるでしょう。
その際、マジクリくんの手に対し、狙っているオブジェクトBの手前にオブジェクトAが重なるケースが起こりえます。

この場合、たとえマジクリくんが魔法の天才で完璧にオブジェクトBの中心に魔法を飛ばせたとしても、当たり判定はオブジェクトAになり、魔法はオブジェクトAに飛んでしまいます。(以下図2参照)

アートボード – 2.png

このように、狙っているオブジェクトの手前に他のオブジェクトがあると、たとえコライダーが完全に重なっていなかったとしても、手前のオブジェクトの方が当たりやすくなります。場合によっては、奥にあるオブジェクトに当てることができなくなる可能性が高くなります。

これではプレイヤーの魔法の放ち方の習得に影響が出るでしょう。

原因はコライダーの大きさにマージンを持たせていることにありますが、マージンが小さくなることはすなわち魔法が当たりにくくなることにつながります。



もう1つ、オブジェクトの選択でコライダーを使う問題はオブジェクトが遠い場合、当てるのが一気に難しくなるということです。

こちらも、マジクリくんに実践してもらいましょう。

オブジェクトとの距離が近い場合、オブジェクトに魔法を当てることは簡単です。
魔法を当てることのできる範囲は大きく、マジクリくんも安心です。(以下図3参照)

アートボード – 3.png

しかし、オブジェクトとの距離が遠い場合、魔法を当てることのできる範囲は非常に小さくなります。(以下図4参照)
アートボード – 4.png
プレイヤーは体験中に動き回ることができるため、狙いたいオブジェクトが他のオブジェクトの影に隠れることやオブジェクトが遠くなることは十分に考えられます。

他にも、コライダーの内側で魔法を放ったときの処理などが厄介になりそうです。
オブジェクトAのコライダーの内側だけどオブジェクトAを狙ったときや、オブジェクトAのコライダーの内側だけどオブジェクトBを狙ったときの区別など。処理が複雑になりそうな予感がプンプンします ?


結論として、オブジェクトを選択する方法に内積を使いました。

内積を用いたオブジェクトの選択方法の流れは以下になります。

① オブジェクト選択用のベクトルと妖精魔法の効くオブジェクトと手を結ぶベクトルを正規化して内積をとる
 ↓
② 内積の値が当たり判定の閾値よりも大きいか小さいかを比べる
 ↓
③ 閾値より大きいオブジェクトのなかで一番値の大きいオブジェクトに妖精魔法を飛ばす。全て閾値より小さい場合外れの妖精魔法を飛ばす

実際のコードを一部抜粋すると以下になります。

// ①〜③ のうち、実際に魔法を飛ばす前の部分
// 魔法が当たっているか否かと、当たったオブジェクトを返す関数
private ( bool, ITickyMagicableObject ) CalculateMagicBeamBasedInnerProduct( MagicBeamStatus status, float angleThresholdToTickyMagic )
{
   MLHandTracking.HandType whichHand = status.whichHandVisualizer.handType;
   List<ITickyMagicableObject> shouldTickyMagicTickyMagicableObjects = TickyMagicableObjectsManager.Instance.tickyMagicableObjectsInScene
      .Where( tickyMagicableObjectInScene => Vector3.Dot( status.direction, (tickyMagicableObjectInScene.GetMyTransform().position - status.startPoint).normalized ) >= angleThresholdToTickyMagic )
      .ToList();
   bool isMagicBeamHit = shouldTickyMagicTickyMagicableObjects.Count() != 0;

   ITickyMagicableObject shouldTickyMagicTickyMagicableObject = null;
   if (isMagicBeamHit)
   {
      List<float> shouldTickyMagicTickyMagicableObjectsInnerProduct = shouldTickyMagicTickyMagicableObjects
         .Select( tickyMagicableObject => Vector3.Dot( status.direction, (tickyMagicableObject.GetMyTransform().position - status.startPoint).normalized ) )
         .ToList();
      int index = shouldTickyMagicTickyMagicableObjectsInnerProduct.IndexOf( shouldTickyMagicTickyMagicableObjectsInnerProduct.Max() ); // 一番内積の値が大きいやつ -> 一番魔法ビームの角度に近いやつ
      shouldTickyMagicTickyMagicableObject = shouldTickyMagicTickyMagicableObjects[index]; 
   }

   return ( isMagicBeamHit, shouldTickyMagicTickyMagicableObject );
}

内積を使うことで先ほどコライダーを用いたオブジェクトの選択方法の2つの問題も解決できます。

先ほど問題になっていた、狙っているオブジェクトが他のオブジェクトの奥にある場合については、もし以下図5のようにオブジェクトAが内積の閾値の範囲内(魔法の有効範囲内)に入っていたとしても、③でおこなっている内積が一番大きいオブジェクトを1つ選択することで解決します。

マジクリくんの魔法が正確である限り、魔法は狙ったオブジェクトに当たります。
アートボード – 5.png

次に、オブジェクトの距離が遠い場合の問題も解決できます。

コライダーをもとに魔法を飛ばした場合は、魔法の有効範囲自体がコライダーに左右される一方、内積をもとに魔法を飛ばすと、魔法の有効範囲を固定することができます。
アートボード – 6.png
無論、遠い位置からそれぞれのオブジェクトを狙い分けることは依然として難しいですが、魔法が当たらないということは少なくなります。

しかし一方で、コライダーの時にはなかった問題も起こりました。
それは、オブジェクトが十分大きい場合にかえって上手く狙えないことです。

オブジェクト選択用のベクトルと妖精魔法の効くオブジェクトと手を結ぶベクトルの内積をとる際、妖精魔法の効くオブジェクトの中心座標を参照します。
そのため、オブジェクトの中心座標から離れた場所を狙った場合「魔法を当ててるはずなのに当たらない」という現象が起こります。

この問題に関しては、的専用のオブジェクトを用意することで解決しました。
オブジェクトが非常に大きくなるケースは限られているため、この方法で対処できました ?

このように、code name: WIZARD で魔法を使うために内積を使ったオブジェクトの選択を採用しました。
もちろん、コライダーを用いた方法も内積を用いた方法もお互いメリットデメリットはあるかと思いますが、結果として複雑な処理になっていない内積を用いた方法は、魔法を使うのに合っていたのだと思います。

「sin、cos とかいつ使うんだよ(笑)」「ベクトルとか社会人になってからいらなくね?」と思ってる学生の皆さん。気持ちはよくわかります。僕も昔そう思っていた時期がありました。

でもそれ、「魔法で使うんです」

2-3. 「無意識で使えてしかも楽しい」を目指した魔法のデザイン

さて、ようやくおおよそ思った通りのところに魔法を飛ばすことができるようになりました。
残るは、③ 目標の場所に向かってどのような魔法を飛ばすか です。

プレイヤーが魔法を使って楽しむためには、魔法が「無意識でつかえてしかも楽しい」ものであるべきだと考えています。

それを実現するには、プレイヤーに「魔法を自分の意のままに使いこなせている」と感じさせることがきっと重要になるでしょう。渡邊恵太さん著作の「融けるデザイン」のお言葉を借りるとすれば「自己帰属感」を意識して魔法もデザインするべきだと僕は考えています。
そのためには、プレイヤーの体の一部のように魔法が振る舞うべきでしょう。

魔法をプレイヤーの体の一部のように振る舞わせるための工夫の1つとして、ベジェ曲線を用いて魔法を曲げました。

2. プレイヤーを魔法使いにさせる術 で述べたように、code name: WIZARD の妖精魔法では、プロトタイプ作成時のさまざまな苦節を経た結果、魔法を飛ばすオブジェクトの選択と魔法の実行を同時 におこなっているため、魔法が放たれた瞬間にどのオブジェクトに当たるのかがわかっています。
そのため、オブジェクトに魔法が当たる場合、魔法が放たれた時点でベジェ曲線に必要な始点と終点は求まっているのです。あとは制御点さえ求めてあげれば、魔法を曲げることができます。

制御点は、始点(手の座標)から2-1. あなたが魔法を当てたい物に手を伸ばしたその時、果たしてどのような線が描かれる? で求めたオブジェクト選択用のベクトルの方向に対して、始点(手の座標)と終点(魔法が当たるオブジェクトの座標)の距離の4分の3だけ進ませた点としています。
魔法が放たれてから後半でグイッと曲がる方が物理的に自然な動きに見えるため後ろ半分という意味で4分の3にしています。(以下図7参照)
アートボード – 7.png

実際のコードは以下になります。

private void RayMagicBeam( MLHandTracking.HandType whichHand, GameObject KIKOHAObject, Vector3 endPoint, float duration, Action onCompleted )
{
   float startToEndDistance = (KIKOHAObject.transform.position - endPoint).magnitude;
   Vector3 firstControlPoint = KIKOHAObject.transform.position + KIKOHAObject.transform.forward * startToEndDistance * 0.75f;
   KIKOHAObject.transform
      .DOPath( new Vector3[]{ endPoint, firstControlPoint, firstControlPoint }, duration, PathType.CubicBezier )
      .SetEase(Ease.InQuad)
      .SetLookAt(0.0001f, KIKOHAObject.transform.forward)
      .OnComplete( () => onCompleted() );
}

この関数が呼ばれる前のKIKOHAObject の座標は手の座標と同じです。
DOTween のDOPath の第1引数に終点を1つと同じ制御点を2つ代入することで、2次ベジェ曲線を描いて魔法を遷移させています。
制御点を増やした3次ベジェ曲線でも試したのですが、2次ベジェ曲線の方がシンプルで軌跡がキレイに見えたためこの方法を採用しました。

以下のGIF画像のように、魔法が曲がります。
TickyMagic_OnHit-min.gif
魔法は本来飛ぶ方向に飛び始めてから徐々にオブジェクトに向かって曲がっていきます。そのため、プレイヤーが狙った方向と実際に魔法が飛んだ方向が認識しやすくなり、プレイヤーの魔法の放ち方の学習を補助する役割も期待できます。
また、曲げることで魔法の動きに幅ができるので、魔法が単調になりにくく、オブジェクトに向かって真っ直ぐ直線的に飛ぶよりも活き活きと感じられるかと思います。

魔法がオブジェクトに当たらなかった場合は、外れた魔法として放たれます。

外れた魔法の場合は、魔法が放たれた瞬間の手の速さに応じて変わる終点をもとにベジェ曲線を描きます。
そのため、以下のGIF画像のように魔法が放たれる時に手を動かしていると、その方向へ魔法が曲がります。
TickyMagic_OnOut-min.gif
これも魔法を活き活きさせるためのちょっとした工夫です。

また、魔法のねじれ具合やエフェクトの揺らめきなどにもランダム性や三角関数を用いて同じ動きにならないようにして、できるだけ魔法を単調に感じさせないようにしています。

あと、GIF画像を見ても確認できますが、魔法がオブジェクトに当たる場合と外れた場合とでエフェクトを分けています。当たった場合は渦巻くエフェクトが増えます。
こうすることで、魔法が当たっているか外れているかを暗に示せるため「魔法が当たったっぽいけど効かない」とプレイヤーが誤認識する可能性を下げる狙いがあります。

3. さいごに

以上、Magic Leap を使って魔法使いになるための術をお話しさせていただきました。

より多くのプレイヤーを理想的な魔法使いにさせるための課題はまだまだ山積みですが、今後も改良を続け、みなさんによりよい魔法使いの体験をお届けできればと思っております ?

Magic Leap Meetup vol.2 in Japan に登壇します

Magic Leap Meetup vol.2 in Japan に登壇します!! ?‍?

ここではお話ししていないプロトタイプ段階での開発の苦悩や、今の妖精魔法に至った経緯などを写真や動画を使ってビジュアルベースにお話しできればと思っております ?

参加無料のオンライン開催だそうです ?

Magic Leap Meetup vol.2 in Japan

東京タワー?で、XR謎解きエンターテインメント 「code name: WIZARD」絶賛開催中

先ほど書かせていただいた魔法が東京タワーで実際に使えます!!?
当初は12月末までの開催予定でしたが、ご好評につき3月末までの延長が決定いたしました ?

Magic Leap によるXR体験だけでなく、紙の謎解きもありますので謎解き好きな方も是非とも。

詳しくは公式ページまで
東京タワー code name: WIZARD

新宿小田急百貨店でも、クリスマス明けの12月26日(土)から開催します ?

東京タワーにつづき、新宿小田急百貨店でもcode name: WIZARD 開催予定です!

新宿小田急百貨店 code name: WIZARD

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

あんスタ!!Music のUIにおけるNested Prefabの活用事例

はじめに

この記事はHappy Elements Advent Calendar 2020の2日目の記事になります。
あんスタ!!Music のUIにおけるNested Prefabの活用事例を紹介します。

NestedPrefabとは

Unity2018.3 より導入された、Prefab のワークフロー改善に伴う新機能です。

3つの新機能を提供しています。

  • Nesting:Prefab を構造化できる
  • Prefab Variants:Prefab のプロパティをオーバーライドできる
  • Prefabモード:プレハブを個別編集できる(シーンに置かなくても編集可能)

これらの新機能は、ユーザーに対する調査に基づいて実施された改善で、Prefab のワークフローをより柔軟なものとし、Prefab を複数のエンティティに分割することによる効率化や、コンテンツの再利用などを可能にし、生産性が向上されることが期待されています。

Unity:プレハブワークフローの改善

UIにおける活用方法

Nested Prefab は Prefab のワークフローに関する用途であれば、大小に関係なく、様々な用途で使用することができます。

UI においても Nested Prefab を利用して画面を制作することが可能です。
例えば、各画面で頻出するようなパターンを Prefab として使い回す、というユースケースは想像しやすいのではないでしょうか。

Nested Prefab以前のワークフロー

従来の Nested Prefab 以前のワークフローでは Prefab の管理方法に悩むことが多かったのではないでしょうか。
例えば View としての Prefab と 共通部分であるヘッダーの Prefab をスクリプトで組み立てる・・みたいなことは、よくある形式だったのではないかと思います。
また、悩みどころのポイントとして、1つの View を Scene として扱うか Prefab として扱うか、というところもあったのではないかと思います。

Nested Prefabの具体例

突然ですが、こちらの画像を御覧ください。
あんスタ!!Music の1画面になります。

所持アイテム画面

1.png

この画面で Nested Prefab で階層構造になっている箇所が8つあります。
さて、どの部分を Nested Prefab にしているでしょうか?

・・答えを言う前に、もう1つの画面をお見せします。

お仕事選択画面

2.png

2つの画面を見比べてみると、共通のパターンがあることが分かりますか?

共通部品として切り出しておくと使いやすそうなもの・・
様々な画面で出てきそうなもの・・

などの観点で探してみると見つかりやすいかもしれません。

・・・

というわけで、正解はこちら。

Nested Prefab の使用箇所

3.png

赤枠で囲まれている箇所が、共通部品として別 Prefab に切り出されているところになります。

画面上部は、お仕事選択画面とも一致する部品が多かったので、ピンと来た方が多かったのではないでしょうか。

アイテムのアイコンは、比較材料がなかったので、わかりにくかったかもしれません。
こちらは後で少し補足します。

内訳

  • 戻るボタン
  • メニューボタン
  • Homeボタン
  • BPバー
  • お仕事チケット
  • アイテムのアイコン(3箇所)

アイテムのアイコンの構造

アイテムのアイコンは、厳密にいうと Prefab 化したものをスクリプトで Instantiate して配置しているのですが、その内部構造で Nested Prefab が使用されています。

「アイコン画像と個数を表現しているテキスト」を共通部品としています。

4.gif

共通部品をNested Prefabで表現するメリット

UI において共通部品を Nested Prefab として扱うことにより、どのようなメリットがあるでしょうか。

こちらは冒頭に記載していた、新しい Prefab ワークフローがもたらす効用です。

Prefab のワークフローをより柔軟なものとし、Prefab を複数のエンティティに分割する
ことによる効率化や、コンテンツの再利用などを可能にし、生産性が向上されること

これを共通部品の話にあてはめると、共通部品のデザインを修正するときに、1個の Prefab を編集するだけで複数の画面に反映できるので、 デザインや構造の変更の柔軟性が上がった といえそうです。

また、同じような構造をふたたび製作する手間を省いたということで、コンテンツの再利用ができ、生産性の向上につながった といえるのではないでしょうか。

従来の共通部品のとある考え方

1つの部品の変更を複数箇所に反映する、という話で、画像を共通部品と見立てて、画像を差し替えることで複数画面に反映する、みたいなテクニックを思い出しました。

そう考えると、Prefab の構造ごと共通部品として変更を加えられるようになった今は、良い時代になったなあと思いました・・。

Nested Prefabを使用する際に気を付けたこと

ここまで書いてきた通り、Nested Prefab を導入するメリットが十分あることは分かったのですが、開発プロジェクトに導入するときは慎重に検討を行いました。

導入を検討した時は、まだ Nested Prefab が出だした頃で、ゲームUIへの活用事例も少なく、何が正解で何が不正解か分からない状況でした。

そして、Nested Prefab 自体がどのような進化を遂げていくかも未知数なところもあり、もし仮に大きな仕様変更などがあった場合の影響を考えると、あまり依存しずぎるのは良くないのではないか、という懸念を考慮した結果、シンプルな使い方に留めることになりました。

シンプルな使い方というより、あまり難しいことをしない、という表現の方が近いかもしれません。具体的には、以下のような点を意識していました。

  • 深い階層を作らないようにする(ネストのネストを極力避ける)
  • パラメータのオーバーライドは極力避ける

深い階層を作らないようにした意図

深い階層を作らないのは、Prefab の構造の複雑性が上がってしまうことを避けたい、という意図がありました。また、どこを基準に Prefab を分けるべきか、基準が曖昧になってしまうというところも懸念点のひとつでした。

パラメータのオーバーライドを避けた意図

Prefab Variants は Prefab の柔軟性を高める素晴らしい機能ですが、これはメリットのひとつである「1個の Prefab を編集するだけで複数の画面に反映できる」という良さを相殺してしまう懸念がありました。

もし仮に親側の Prefab で子の Prefab のあるパラメータをオーバーライドしていて、子の Prefab の方で同じパラメータを変更しても、親側には反映されない、ということです。

今回は共通部品としてのユースケースということで「複数の画面で同じ構造が使い回せる」メリットを優先したい、という理由からパラメータのオーバーライドは避けるようにしました。

実際に運用してどうだったか

リリースしてから半年が経過しましたが、Nested Prefabに起因するトラブルは殆どなく、ワークフロー上も Nested Prefab をあまり意識せずに使用できたと思います。

Nested Prefab自体、機能の変更がほとんどなく、Unityのバージョンによる違いなどもなく安定している印象があります。そのため、もう少し積極的に Nested Prefab の機能を使用してもよかったかもしれないという気持ちもありました。ただし、今後も大きな変更がないかどうかは分からないので、特に開発の規模が大きいプロジェクトはやや慎重に採用するくらいが、安全といえば安全そうです。

おまけ:もしNested Prefabをより積極的に使用するなら

今回、あまり活用ができなかったと感じている点のひとつに、「コンテンツの異なる部分に対して同時に作業ができる」という点があります。

再度、冒頭の新しい Prefab ワークフローがもたらす効用です。

Prefab のワークフローをより柔軟なものとし、Prefab を複数のエンティティに分割する
ことによる効率化や、コンテンツの再利用などを可能にし、生産性が向上されること

ここで着目したいのが「Prefab を複数のエンティティに分割することによる効率化」という点ですが、Nested Prefab の活用方法として、Prefab の同時編集が実現する可能性があるかもしれないと考えました。

具体的には、デザイナーとエンジニアが Prefab を触るときに、両者の作業領域という観点で Prefab を分離する、というアイデアです。

エンジニアが触る Prefab :デザイナーが触るPrefabをラップして使用する
  デザイナーが触る Prefab

これにより何が可能になるかというと、デザイナーは見た目に関するパラメータを触る、エンジニアは実装に関するパラメータを触る、などの取り決めを設けておくことにより、1つの画面を両者が触ることができるようになる(かもしれない)という説です。

これはこれで課題もありそうな気がしていますが、もし Nested Prefab を積極的に活用するならこういう方法も考えられますね。

まとめ

  • Nested Prefabとは:Unity2018.3 より導入された新機能
  • あんスタ!!MusicのUIでは共通部品をNested Prefabで表現した
  • 共通部品をNested Prefabで表現するメリットとして、次の2点が挙げられる
    • デザインや構造の変更の柔軟性が上がる
    • コンテンツの再利用ができ生産性の向上につながる
  • Nested Prefabを使用する際に気を付けたこととして、次の2点が挙げられる
    • 深い階層を作らないようにする(ネストのネストを極力避ける)
    • パラメータのオーバーライドは極力避ける
  • 実際にリリースから半年間運用したがNestedPrefabによるトラブルは殆どなかった
  • Nested Prefabは現状は大きな仕様変更がないため安心して使用できる。 ただし、今後もそうとは限らないので、大規模開発ではやや慎重に採用するくらいがいいかもしれない
  • 共通部品としての利用以外にも、有効な活用方法が見いだされることに期待したい

参考リンク

メンバー募集

Happy Elements株式会社 カカリアスタジオでは、
いっしょに【熱狂的に愛されるコンテンツ】をつくっていただけるメンバーを大募集中です!
もし弊社にご興味持っていただけましたら、是非一度
下記採用サイトをご覧ください。
Happy Elements株式会社 採用特設サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

よく使うシーンなどを登録して便利に…

はじめに

Unityで作業をしていると、シーンやフォルダを探すことがよくあります。
今回は自分が昔作ったお気に入り機能をちょっと見栄えを整えて紹介します。
まあ Two Column Layout を使ってる方は標準のFavorites機能を使えば全て解決なんですが…

使い方

・メニューのWindow/Favoriteでウィンドウを開きます。
・お気に入り登録したいものをDrag&Dropします。
登録名:選択、開く:シーンを開く、Show:フォルダを開く、除外:登録を外す
 という感じで使用します。
2020-11-24_10h56_08.png

動作環境

動作を確認した環境です。
・Unity2017.4.24f1
・Unity2019.4.12f1
・Windows10

コード

以下のスクリプトをEditorフォルダに入れて使用します。

FavoriteWindow.cs
using UnityEngine;
using UnityEditor;
using UnityEditor.SceneManagement;
using System.Collections.Generic;
using System.Linq;
using System.IO;

public class FavoriteWindow : EditorWindow
{
    Vector2 scrollPos = Vector2.zero;
    const string saveFile = "/Workspace/JumpFavorite.txt";
    HashSet<string> hashGuid = null;

    // お気に入り選択
    [MenuItem("Window/Favorite")]
    static void ShowWindow()
    {
        var win = GetWindow<FavoriteWindow>();
        // お気に入りのBuilt-in Icon取得して設定
        var fav = EditorGUIUtility.IconContent("Favorite Icon");
        fav.text = "Favorite";
        win.titleContent = fav;
    }
    void OnGUI()
    {
        // NULLだったら読み込み
        if(null == hashGuid)
            LoadFavoriteGuid();

        GUILayout.Space(5);
        Object obj = null;
        obj = EditorGUILayout.ObjectField("お気に入り登録", obj, typeof(Object), false);
        if(null != obj) {
            // お気に入りを登録
            var path = AssetDatabase.GetAssetPath(obj);
            var guid = AssetDatabase.AssetPathToGUID(path);
            hashGuid.Add(guid);
            // 保存
            SaveFavoriteGuid();
            return;
        }

        if(0 == hashGuid.Count)
            return;

        using(var scroll = new GUILayout.ScrollViewScope(scrollPos)) {
            scrollPos = scroll.scrollPosition;
            string remove = "";
            foreach(var n in hashGuid) {
                using(new EditorGUILayout.HorizontalScope()) {
                    var path = AssetDatabase.GUIDToAssetPath(n);
                    var name = Path.GetFileNameWithoutExtension(path);
                    var ext = Path.GetExtension(path);
                    // 選択
                    var icon = AssetDatabase.GetCachedIcon(path);
                    GUILayout.Label(icon, GUILayout.Height(20), GUILayout.Width(20));
                    if(GUILayout.Button(name, GUILayout.Width(200)))
                    {
                        if (!string.IsNullOrEmpty(n))
                            Selection.activeObject = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(n), typeof(Object));
                        break;
                    }
                    // 開く(シーンなら)
                    using(new EditorGUI.DisabledGroupScope(ext != ".unity"))
                    {
                        if(GUILayout.Button("開く", GUILayout.Width(40))) {
                            if(EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
                                EditorSceneManager.OpenScene(path);
                            break;
                        }
                    }
                    // Show in Explore
                    if(GUILayout.Button("Show", GUILayout.Width(50))) {
                        if(Directory.Exists(path)) {
                            var fullPath = Application.dataPath + "/../" + path;
                            System.Diagnostics.Process.Start(fullPath);
                        }
                        else
                            EditorUtility.RevealInFinder(path);
                        break;
                    }
                    // 除外
                    if(GUILayout.Button("除外", GUILayout.Width(40))) {
                        remove = n;
                        break;
                    }
                }
            }
            if(!string.IsNullOrEmpty(remove))
            {
                // 除外
                hashGuid.Remove(remove);
                // 保存
                SaveFavoriteGuid();
            }
        }
    }

    void SaveFavoriteGuid()
    {
        string path = Application.dataPath + saveFile;
        string dir = Path.GetDirectoryName(path);
        // フォルダがなかったら作る
        if(!Directory.Exists(dir))
            Directory.CreateDirectory(dir);
        // 保存
        string[] arrGuid = hashGuid.ToArray();
        var str = string.Join("\n", arrGuid);
        File.WriteAllText(path, str);
        AssetDatabase.Refresh();
    }
    void LoadFavoriteGuid()
    {
        hashGuid = new HashSet<string>();
        string path = Application.dataPath + saveFile;
        if(!File.Exists(path))
            return;

        var str = File.ReadAllText(path);
        var arrGuid = str.Split("\n"[0]);
        // 無かったら無視
        foreach(var n in arrGuid) {
            if("" != AssetDatabase.GUIDToAssetPath(n))
                hashGuid.Add(n);
        }
    }
}

解説

注意が必要なのは、OpenSceneを使用すると現在編集中のシーンが破棄されてしまうのでEditorSceneManager.SaveCurrentModifiedScenesIfUserWantsToで編集中のシーンを保存するかの確認をとった方がいいです。

あと、EditorUtility.RevealInFinderでフォルダを開く場合、ファイルを指定していればそのフォルダを開いてくれますが、フォルダを指定していると親フォルダが開かれます。
なので、Process.Startを使用してフォルダは開いています。

管理はGuidをHashSetで行ってるだけなので並び順は気にしてません。まあ、並び替えが必要なほど登録する時点でもはやこの機能を使う意味がないと思いますが…

おわりに

すでに便利なアセットや似たようなことをやってる方たちがいるので今更感がありますが
自分の好みな感じに仕上がっています。
もし好みに合えば使っていただければと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む