#1 誰?
はじめまして!
株式会社ミクシィの開発本部インフラ室映像技術グループのまんてらです!
ミクシィでは、新競輪「PIST6」で用いられる映像の自動編集ツールである「Uros」の作成をしております!
PIST6に関しては下記サイトを見てみて下さい!
本記事は「ミクシィグループ Advent Calendar 2021」の20日目の記事です。
#2 何やるの?
今回は私がUnityを用いて開発する際、必ず入れるUnityAssetを紹介したいと思います!
読者対象はUnityで1,2本程度アプリを開発して、ある程度Unityの扱いに慣れてきた人向けとなっております。
そろそろ少しは開発を楽にしてみたいな~と考えている方は是非見ていって下さい!
ちなみに、本記事ではWindows10下のUnity2020.3.24f1を用いた環境となりますので、他の環境で不具合等があった場合のご質問には対応致し兼ねますのでご了承下さい。
#3 アセットのインストール方法について
アセットの紹介の前に、アセットのインストール方法には2種類あります。
unitypackageファイルを直接インポートする
パッケージの配布サイトやUnity Asset Storeで.unitypackageファイルをダウンロードする事で、unitypackageを入手する事が出来ます。入手したunitypackageはUnityEditorのProjectウィンドウにD&Dさせてインポートします。
この方法は、非常に直感的でわかりやすいです。しかし、Assetsフォルダ内にファイルが増えて管理が煩雑になり、アセットを更新する際も煩雑な手作業をする事になります。上記の様な問題から、後述するUPMが公開されているアセットの場合は、UPMを用いてインストールする事を推奨します。
UPM(Unity Package Manager)でインポートする
UPMは配布者が指定するURLを指定するだけでインポートが可能です。UPMでインポートしたアセットは、Packages/以下にインストールされます。Packages/にインストールされる為Assetsフォルダ内にフォルダが増えず、管理が容易になります。
また、Packages/以下のファイルはキャッシュから参照されているだけな為、様々なプロジェクトにアセットが複製されて管理が煩雑になる事態も防ぐ事が出来ます。配布サイト等でUPMとページ内検索した際、このリンクの様な内容があればUPM配布に対応しているアセットです。
特別な理由が無ければ、UPMを用いたインポートで問題無いでしょう。
UPMでインストールする際は、
- UnityEditorのツールバーから、Window > Package Manager を選択します。
- 左上の「+」マークをクリックし、「Add package from git URL…」を選択します。
- 表示されたテキストボックスに、https://github…から始まるURLを入力してAddをクリックします。
- Projectウィンドウ内のPackagesフォルダにインポートしたアセットが追加されている事を確認します。
#4 私が必ず使うUnityAssetについて
・UniRx
一言で簡単に説明すると…
C#におけるeventのすごいやつです!!
そもそもeventって?
eventというのは…
using UnityEngine;
public class KeyService : MonoBehaviour
{
public int Score;
public delegate void OnPressKey(int score);
public event OnPressKey OnPressedKey;
private void Update()
{
// スペースキーが入力されると…?
if (Input.GetKey(KeyCode.Space))
{
// イベントが発火!!
OnPressedKey(score);
}
}
}
using UnityEngine;
public class KeyView : MonoBehaviour
{
[SerializeField] private KeyService _keyService;
private void Awake()
{
// ボタンを押された時にスコアが表示される!
_keyService.OnPressedKey += score => { Debug.Log("現在の得点は" + score + "です!"); };
}
}
こんな感じで記述する事で、「何かが起きた時にどういう動作をさせるか」をベースにして記述するコードの事です!
UniRxになると?
一方で、UniRxになると何が良いのでしょうか?
それは、eventよりも高機能な記述が出来る所です!
次のサンプルコードが基本的な書き方です。
using System;
using UniRx;
using UnityEngine;
public class KeyService : MonoBehaviour
{
public int score;
// イベントが発火した時の処理(「購読」という)を記述出来る機能だけを公開
public IObservable<int> OnPressKey => _onPressKey;
// イベントの発火と購読ができる機能(発火機能は公開しないのでprivate)
private readonly Subject<int> _onPressKey = new Subject<int>();
private void Update()
{
// スペースキーが押されると…?
if (Input.GetKey(KeyCode.Space))
{
// イベントが発火!
_onPressKey.OnNext(score);
}
}
}
using UniRx;
using UnityEngine;
public class KeyView : MonoBehaviour
{
[SerializeField] private KeyService keyService;
private void Awake()
{
// ボタンを押された時にスコアが表示される!
// (OnPressKeyのイベントをSubscribe(購読)する。
keyService.OnPressKey.Subscribe(score => Debug.Log("現在の得点は" + score + "です!"));
}
}
さて、これだけだとイベントの発火と購読を分離出来ただけですね。
フィルタリングが出来る!
UniRxだと次の様な記述でイベントをフィルタリングする事が出来るんです!
using UniRx;
using UnityEngine;
public class KeyView : MonoBehaviour
{
[SerializeField] private KeyService keyService;
private void Awake()
{
// ボタンを押された時にスコアが表示される!
keyService.OnPressKey.Subscribe(score => Debug.Log("現在の得点は" + score + "です!"));
// ボタンを押された時にスコアが100点を超えてると表示される!
keyService.OnPressKey.Where(score => score > 100).Subscribe(score => Debug.Log("現在の得点は100点を超えています!"));
}
}
上記の様に記述する事で、100点以上が通知された時にのみSubscribeが実行されます!
Where以外にも様々なフィルタリング関数があるので、色々と試してみて下さい!
不要になった時の破棄し忘れ問題も解決できる!
また、eventに+=で動作を登録した際、GameObjectが無くなった時に動作を-=で破棄し忘れる問題も容易に解決する事が出来ます!
using UniRx;
using UnityEngine;
public class KeyView : MonoBehaviour
{
[SerializeField] private KeyService keyService;
private void Awake()
{
// ボタンを押された時にスコアが表示される!
keyService.OnPressKey.Subscribe(score => Debug.Log("現在の得点は" + score + "です!")).AddTo(gameobject);
}
}
Subscribeの後に.AddTo(gameobject)を記述する事で、対象のGameObjectが破棄された際にSubscribe内容も同時に破棄してくれます!なんと便利な事か!
更なる拡張機能も!
上記のIObservableとSubjectと同じくらい利用する便利記述がありまして…
using UniRx;
using UnityEngine;
public class ScoreService : MonoBehaviour
{
public int Score
{
get => _scoreProperty.Value;
set => _scoreProperty.Value = value;
}
public IReadOnlyReactiveProperty<int> ScoreProperty => _scoreProperty;
private readonly ReactiveProperty<int> _scoreProperty = new ReactiveProperty<int>();
private void Update()
{
if (Input.GetKey(KeyCode.Space))
{
Score++;
}
}
}
using UniRx;
using UnityEngine;
public class ScoreView : MonoBehaviour
{
[SerializeField] private ScoreService scoreService;
private void Awake()
{
// スコアが更新されると、スコアが表示される!
scoreService.ScoreProperty.Subscribe(score => Debug.Log("現在のスコアは" + score + "です!")).AddTo(this);
}
}
なんと!ReactivePropertyを使うと数値更新の管理まで出来てしまうのです!
他にもReactiveCollectionやReactiveDictionary等々、様々な機能があるので試してみて下さいね!
・UniTask
一言で簡単に説明すると…
非同期処理を(比較的)簡単に扱えるすごいやつです!
そもそも非同期って?
非同期処理というのは、複数の処理を同時並行させる事を言います。
ロード画面が代表的な例で、アセットをロードをしながらNowLoading…等の画面を表示させる処理をさせたりできます。
従来それらを実装する為には、コルーチンという煩雑な手段を使ったり、Taskという激重かつマルチスレッド前提なUnityに合わない機能を使ったりする必要がありました。しかし、UniTaskは両方の問題を解決したライブラリとなっています!
コルーチンで書くとどうなるの?
コルーチンで画像をダウンロードしようとすると大体以下の様なソースになるかと思います
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
public class ImageDownloader : MonoBehaviour
{
[SerializeField] private string downloadUri;
[SerializeField] private RawImage targetImage;
private void Awake()
{
StartCoroutine(DownloadImageAsync(downloadUri,
texture2D =>
{
targetImage.texture = texture2D;
}));
}
private IEnumerator DownloadImageAsync(string uri, Action<Texture2D> onLoaded)
{
var request = UnityWebRequestTexture.GetTexture(uri);
yield return request.SendWebRequest();
onLoaded(DownloadHandlerTexture.GetContent(request));
}
}
コルーチンを使った場合Texture2Dを返す際にネスト化してしまったりして、少し面倒な記述となってしまいます。
このネストが重複したり、数が増えてくるとかなり直感的ではないソースになってしまいがちです。
UniTaskになると?
UniTaskを使用した場合、下記の様なソースコードになります。
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
public class ImageDownloader : MonoBehaviour
{
[SerializeField] private string downloadUri;
[SerializeField] private RawImage targetImage;
private async void Awake()
{
var texture = await DownloadImageAsync(downloadUri);
targetImage.texture = texture;
}
private async UniTask<Texture> DownloadImageAsync(string uri)
{
var request = UnityWebRequestTexture.GetTexture(uri);
await request.SendWebRequest();
return DownloadHandlerTexture.GetContent(request);
}
}
どうでしょう?かなりソースが洗練されたかと思います。
asyncが付いている関数は「これは非同期処理を行う関数ですよ!」という旨を宣言する物で、awaitを宣言する事で、「asyncが付いた関数の終了を待つよ!」という旨を宣言する物です。
これらを組み合わせる事で、いつもの同期処理をしている様な形で記述する事が出来ます。
複数の処理を待機する
またUniTaskだと、複数の非同期処理が全て終わるまで待機する処理も容易に記述できます。
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
public class ImageDownloader : MonoBehaviour
{
[SerializeField] private List<DownloadImageEntity> downloadImageEntities = new List<DownloadImageEntity>();
private async void Awake()
{
var tasks = new List<UniTask<Texture>>();
foreach (var downloadImageEntity in downloadImageEntities)
{
var task = DownloadImageAsync(downloadImageEntity.downloadUri);
tasks.Add(task);
}
var textures = await UniTask.WhenAll(tasks);
for (var i = 0; i < textures.Length; i++)
{
downloadImageEntities[i].targetImage.texture = textures[i];
}
}
private async UniTask<Texture> DownloadImageAsync(string uri)
{
var request = UnityWebRequestTexture.GetTexture(uri);
await request.SendWebRequest();
return DownloadHandlerTexture.GetContent(request);
}
[Serializable]
private struct DownloadImageEntity
{
[SerializeField] public string downloadUri;
[SerializeField] public RawImage targetImage;
}
}
UniTaskの返り値を呼び出し側で保持する事ができ、UniTask.WhenAll()に保持したUniTaskを投げる事で、全てのUniTaskの処理が終わってから画像を表示させる事も可能です!
UniTaskの便利機能
上記以外にもUniTaskは様々な便利機能が搭載されています!
using System;
using Cysharp.Threading.Tasks;
using UniRx;
using UnityEngine;
public class ScoreManager : MonoBehaviour
{
public int Score
{
get => _scoreProperty.Value;
set => _scoreProperty.Value = value;
}
public IReadOnlyReactiveProperty<int> ScoreProperty => _scoreProperty;
private readonly ReactiveProperty<int> _scoreProperty = new ReactiveProperty<int>();
private async void Awake()
{
// 10秒待機する
await UniTask.Delay(TimeSpan.FromSeconds(10));
// スコアが100点になるまで待つ
await UniTask.WaitUntil(() => Score > 100);
// ReactivePropertyをawaitすると、値が変化するまで待機する。
// 他にもUniRxと連携も出来る機能があるぞ!
await ScoreProperty;
}
}
・VContainer
一言で簡単に説明すると…
オブジェクトの参照関係/依存関係を管理する事ができます!
何に使うの?
クラス同士の参照関係が複雑になりやすいゲーム開発において、実はMonoBehaviourという存在は厄介だったりします。
MonoBehaviour同士の参照を解決するには、SerializeField変数にEditor上のインスペクタから指定したり、GameObjectの動的な生成/破棄を常に念頭に置いた参照処理をしなくてはならなかったり等々…結構複雑な問題を抱えてたりします。
VContainerを用いると、そういった参照関係のゴタゴタ処理を一手に引き受けてくれます!
つまりどういう事だってばよ?
正直な所めちゃくちゃ説明が難しいので、VContainerのページと作者の講演動画を見ながら作者の記事を読んだ方が圧倒的に有意義かと思います。幸い資料やドキュメントが全て日本語な為、熟読すれば理解が早いと思います。
昔はZenjetというアセットを使用していましたが、VContainerは最適化と機能の選別が優秀な為最近はこちらを使っております。
・Odin
一言で簡単に説明すると…
エディタ拡張が超楽に出来る!!
どう楽になるの?
例えば、あるGameObjectにおける全ての子オブジェクトをインスペクタ上のドロップダウンリストに表示させ、選択したオブジェクトをログに出力する処理を作成するとしましょう。
実際標準のエディタ拡張で作ってみると分かるのですが、これが意外と面倒臭いのです…
しかし…Odinを使うと…?
using Sirenix.OdinInspector;
using UnityEngine;
public class OdinTest : MonoBehaviour
{
[SerializeField, ValueDropdown("@GetComponentsInChildren<UnityEngine.Transform>()"), OnValueChanged("OnChangedFood")]
private Transform selectedTransform;
private void OnChangedFood()
{
Debug.Log(selectedTransform.name + "を選択しました。");
}
}
なんと!DropDownを表示する処理と関数コールバックが一行で書けてしまいます!!
今までエディタ拡張に苦しめられていたのが嘘のよう…
当然これだけではなく…
using System;
using Sirenix.OdinInspector;
using UnityEngine;
public class OdinTest : MonoBehaviour
{
public enum Food
{
[Obsolete("実はこれ毒リンゴだよ")] Apple,
Orange,
Banana,
Tomato,
[InfoBox("これ美味しいよ")] Hamburger,
Beef,
Milk
}
[ColorPalette, LabelText("食べ物の色")] public Color selectedColor;
[EnumToggleButtons] public Food selectedFood2;
public Food selectedFood1;
}
本当に色々な事が出来るので是非購入して試してみて下さい!
もっとOdinについて知りたい人は…?
過去にOdinについて約10分で紹介する動画配信をしているので、是非見てみて下さい!
#5 おわりに
以上がUnityプロジェクトを作った際に私が必ずインポートするアセットです!
これらを使いこなせればQOL爆上がり間違いなしなので、是非使ってみて下さい!
また、ミクシィでは様々な事業/ポジションを積極採用中です!
是非ミクシィの採用情報をご覧ください!
最後まで見て下さりありがとうございました!