UniRxの後処理まとめ
UniRxの後処理についてザックリまとめて整理。ようやく少し分かってきたレベルの人間が書いているので、ミスが含まれている可能性がありますことを先に忠告しておきます。
参考: qiita.com
そもそもなんで後処理するの?
メモリリークするから
不要になった購読を解除しないとメモリリーク*1が発生します。リークが目に見えるレベルになるのは稀なので、丁寧に後処理しても恩恵が少ないように感じるかもしれませんが、それでもメモリリークがない方がマシなのは自明でしょう。
バグの原因になるから
不要な購読が残っていると、間違ってその購読処理が行われてしまう可能性があります。
また、誤って購読が重複してしまい、大変なことになることもあります(2敗)。
具体的な後処理方法
AddTo(this)
MonoBehaviourに依存しているクラスがOnDestroy()
する際に購読をやめたい場合に使えます。
最も簡単です。
厳密にはReactiveProperty<T>
やSubject<T>
等の発行側も実装しないといけない点に注意です!
using UniRx; using UnityEngine; namespace UniRxDisposeExample { public class PlayerPresenter : MonoBehaviour { private Player _player; private void Start() { _player.Hp.DistinctUntilChanged().Subscribe(x => Debug.Log(x)) .AddTo(this); // ← OnDestroy() で Dispose() が呼ばれ、購読が破棄される } } public class Player : MonoBehaviour { public IReadOnlyReactiveProperty<int> Hp => _hp; private ReactiveProperty<int> _hp = new(100); private void Start() { _hp.AddTo(this); // ← ReactiveProperty等もAddToを忘れずに! } } }
AddTo(gameObject)
ほとんど一緒ですが、GameObject
そのものの寿命に対応させたい場合はこちらを利用しましょう。
こんな感じで、AddTo()
の引数にとれるものは様々あります。
IDisposableの実装
MonoBahaviourを継承したクラスなら大抵AddTo(this)
でどうにかなりますが、非Monoで書かれたクラスに対してはそうはいきません。明示的にDispose()
を実行してやる必要があります。
そのためにIDisposable
を実装し、適切なタイミングで破棄できるように準備しておきます。
namespace UniRxDisposeExample { public class Score : IDisposable // 非MonoでUniRx使うときはほぼ必ず実装! { public IReadOnlyReactiveProperty<int> Current => _current; private readonly ReactiveProperty<int> _current = new(); private readonly ScoreView _scoreView; // GUIにScoreを表示するクラス private CompositeDisposable _disposables; // Disposableをまとめることが出来るクラス public Score(ScoreView scoreView) { _scoreView = scoreView; _current.Value = 0; Register(); } private void Register() { _current.DistinctUntilChanged() .Subscribe(_ => { _scoreView.SetCurrent(_current.Value); }) .AddTo(_disposables); } public void Add(int value) { _current.Value += value; } public void Dispose() { _disposables?.Dispose(); // まとめてDispose() _current?.Dispose(); // ReactivePropertyもしっかりDispose() } } }
このとき、きっちりDispose()
まで実装しても、このDispose()
が呼ばれなかったら何の意味もありません。適切なタイミングで呼び出し元がDispose()
するように注意しましょう。
AddTo
は引数に対象のICollection<IDisposable>
(CompositeDisposable
とか)を入れて紐づけることが出来るので、うまく活用しましょう。
namespace Scripts.EntryPoints { /// <summary> /// ゲームのエントリーポイント例 /// </summary> public class GameEntryPoint : MonoBehaviour { [SerializeField] private ScoreView scoreView; private CompositeDisposable _disposables; private void Awake() { var score = new Score(scoreView); score.AddTo(_disposables); // もしくは _disposables.Add(score); // その他諸々のDI } private void OnDestroy() { _disposables.Dispose(); // ←ここまで書いてようやくMonoBehaviourのライフサイクルと紐づく } } }
TakeUntilDestroy()
オペレーターの一種です。
Destroy()
するまで購読します。つまり、Destroy()
されたら購読は解除されます。
このオペレーターを利用すると、購読側がそれぞれ義務付けられていたAddTo()
を発行側に組み込むことが出来るので、書き忘れによるミスを減らすことが出来そうです(それでも基本AddTo()
を使った方がいいのは確か)。
using UniRx; using UnityEngine; using System; namespace UniRxDisposeExample { public class SubjectEmitter : MonoBehaviour { public IObservable<Unit> Subject => _subject.TakeUntilDestroy(gameObject); private Subject<Unit> _subject = new(); } public class User : MonoBehaviour { [SerializeField] private SubjectEmitter subject; private void Start() { subject.Subject.Subscribe(_ => Debug.Log("SubjectEmitter")) .AddTo(this); // ここのAddToは最悪呼ばなくてもメモリリークにはならない } } }
*1:使ってないのに割り当てられている無駄なメモリ領域が生じてしまうバグ
LucidAudioをOnDestroyで止めたい場合はSetLinkを使おう
基本的にOnDestroyで止める方法は危険っぽいです。何回か以下のようなエラーが発生し、その次のPlayMode実行時に音が鳴らなくなりました。
Error: Some objects were not cleaned up when closing the scene. (Did you spawn new GameObjects from OnDestroy?)
PlayMode実行時にLucidAudioオブジェクトがDontDestroyOnLoadに入っていないことから、LucidAudioオブジェクトの破棄に失敗しているものと考えられます。
失敗例
public class 良くない例 : MonoBehaviour { [SerializeField] private AudioClip clip; private AudioPlayer _player; private void Start() { _player = LucidAudio.PlayBGM(clip).SetLoop(); } private void OnDestroy() { _player?.Stop(); // LucidAudioが適切に破棄されない場合がある } }
解決策
SetLinkを利用してMonoBehaviourのライフサイクルと紐づけましょう。
public class 多分こっちの方がいい : MonoBehaviour { [SerializeField] private AudioClip clip; private void Start() { LucidAudio.PlayBGM(clip).SetLoop().SetLink(gameObject); } }
LucidAudioのStopに引数を入れるときの注意点
※追記(同日):以下の記事の通り、恐らくOnDestroyでStopを使うこと自体が誤りのようです。
以下本文。
LucidAudioのStop()
メソッドでは引数を取ってフェードアウトを演出することが出来ます。
しかし、次のような使い方は出来ません。
public class LucidAudioExample : MonoBehaviour { [SerializeField] private AudioClip clip; private AudioPlayer _player; private void Start() { _player = LucidAudio.PlayBGM(clip); } private void OnDestroy() { _player.Stop(1.0f); // Error _player?.Stop(1.0f); // これもError } }
Consoleのエラーを見てみると、Null参照になっています。
少し考えてみると分かることですが、フェードアウトしている間はLucidAudioが動いている必要があるので、OnDestroy時にそれを実行してもエラーになるというわけです。
Unityのイベント関数が呼ばれないとき
エラーが出ない & イベント関数(Update
,Awake
,Start
,OnDestroy
等)が呼ばれないときは、
- MonoBehaviour継承しているか見る
- メソッド名が合っているか確認
- 対象オブジェクトがアクティブになっているかどうかを見る
【SOLID原則】OCPについてちゃんと考えたい
About
個人開発のゲームにてswitch文が増え、コードがヤバくなってしまい、リファクタリングをしようと考えた今日この頃。
もう1度OCPについて考える機会だと思い、この記事を書いています。
↓前回のOCP紹介記事
OCPについて再整理
2度目なのでサラッと。
OCPとは、「クラスは拡張に対して開かれており、修正や変更に対しては閉じていなければならない」という原則です。
これは以下のような目的を含んでいます。
- クラスの機能を拡張する際、既存のコードに手を付ける行為を最小限にする
- 依存関係を最小限化し、コードの意図を分かりやすくする
OCPに違反してるコードってどんなの?
switch文
分かりやすさのために主語がデカいです。
やむを得ずswitchを使うこともありますが、switchを使う際はOCPに違反していないかを充分考える必要があります。
闇雲にswitch文を書くと、パターンを修正した際にすべてのswitch文を修正する必要に駆られてしまいます。
前の記事で紹介した図形の面積問題をもう一度取り上げてみます。
using UnityEngine; namespace DesignPatterns.OCP { public class AreaCalculator { public float GetRectangleArea(Rectangle rectangle) { return rectangle.Width * rectangle.Height; } public float GetCircleArea(Circle circle) { return circle.Radius * circle.Radius * Mathf.PI; } } // AreaCalculatorは以下とほぼ同じ意味。 public class AreaCalculateManager { public float GetArea<T>(T obj) { return obj switch { Rectangle rectangle => rectangle.Width * rectangle.Height, Circle circle => circle.Radius * circle.Radius * Mathf.PI, _ => 0f }; } } public class Circle { public float Radius { get; set; } } public class Rectangle { public float Width { get; set; } public float Height { get; set; } } }
前記事にAreaCalculateManagerを追加してみました。このクラスはAreaCalculatorとほぼ同じようなクラスになっています。GetArea()を呼び、その引数のクラスをパターンマッチングによるswitch式で分岐しています。このようなコードは一見直感的なのですが、開発を進めていくうちに破壊的なコードに変貌します。
AreaCalculateManagerは、むやみやたらなswitch文が危険であることを説明するためのものです。switch文を書くときはもっといいコードがないかを考えるのは結構大事な気がしています。
なんちゃらCalculatorとかなんちゃらProcessorとか
個人的にOCP違反のクラス設計に出てきがちなクラス。個々のクラスが処理できておくべきことを外部に移譲してしまうと「なんちゃらCalculatorとかなんちゃらProcessorとか」ができてしまいます。結果的にOCPに違反している可能性が高いです。
関数的対応をするモノ
違反例にて後述します。
実際の違反例
音ゲー制作にて過去に自分が違反した例を紹介。判定結果に応じてスコアを加算する部分です。
using System; using UnityEngine; namespace Scores { /// <summary> /// 判定の結果 /// </summary> public class JudgementResult { public readonly JudgementResultType Type; // enum public readonly int LagMs; // 判定からのズレ(ms) public JudgementResult(JudgementResultType type, int lagMs) { Type = type; LagMs = lagMs; } // ... } } namespace Scores { public class ScoreModel : MonoBehaviour { // ... private void AddScore(JudgementResultType type) { var addition = type switch { JudgementResultType.Perfect => 15, JudgementResultType.Great => 10, JudgementResultType.Good => 5, JudgementResultType.Bad => 1, JudgementResultType.Miss => 0, _ => 0 }; _score.Value += addition; } } }
がっつりswitch文を使ってますが、これはOCP違反でしょう。
Scoreの加算だからScoreModelクラスがやる!という思考回路で書いたのだと思いますが、スコア自体はJudgementResultクラスが持っていた方がよさそうです。
もうちょっと踏み込んで考える
そもそもJudgementResultType
とそれに対応したscore
は本質的には同じことではないでしょうか。
JudgementResultType
がPerfect
ならscore
は15
といったことが関数のように決定されることが期待されているのです。
つまり、JudgementResultType
とscore
は関数的なのです。
1対1対応の物を変換するだけのメソッドは、OCPに違反している可能性が高いです。
なぜなら、その部分をそのままクラスの中に定義することが可能だからです。
もっと言うと、lagMs
とJudgementResultType
も関数的ではないでしょうか。lagMs
が決まればJudgementResultType
は一意に定まります。
そんなわけで、このクラスは以下のようになっています。
こう見ると、そもそも必要な情報は、LagMs
、もしくはJudgementResultType
だけなのではないでしょうか...?
(この辺りの話は、DBの主キー等に通ずる部分があると思います。参考までに。)
改善例
実際にクラス内に定義してみましょう。今回はScore
のみを必要な情報としてとらえ、書いてみました。
using UniRx; using UnityEngine; namespace Scores { public enum JudgeType { Perfect, Great, Good, Bad, Miss, } public interface IJudgement { int Score { get; } } public class Perfect : IJudgement { public int Score => 15; } public class Great : IJudgement { public int Score => 10; } public class Good : IJudgement { public int Score => 5; } public class Bad : IJudgement { public int Score => 1; } public class Miss : IJudgement { public int Score => 0; } } namespace Scores { public class ScoreModel : MonoBehaviour { public IReadOnlyReactiveProperty<int> Score => _score; private readonly ReactiveProperty<int> _score = new ReactiveProperty<int>(0); // ... public void AddScore(IJudgement judgement) { _score.Value += judgement.Score; } } }
interfaceを利用して上手く実装を分離することが出来ました。また、不必要な情報も減らすことができ、より本質的なコードになったと思います。
ここまできたらstruct
とかrecord
とかにした方が良いかも...と思いましたがそこは別の話になるので今回は割愛(まだあんまりstruct
理解できてないし)。
いっぱいクラスできるから嫌!と言う方は、Judgementを5回newしてDIする、みたいな使い方もありかと思います(具体的な実装は割愛します)。
数値のハードコーディングが気になる場合は、ScriptableObject
を利用してクラスの生成時にそこから情報を取ってくるといいのかもしれません。
【Unity】uGUIが見えない系のバグの話【バグ発見簡易フローチャート付】
しょうもないミス
Imageが見えない!!!→30分経過...→「あっColorのAlphaが0だった...」
もう、こういうミスで時間をかけたくない。
uGUIが見えない系のバグは、考えられる原因1つ1つ自体は全部しょうもないのですが、その量自体は多いので、1つ1つ白みつぶしに確認していくしかないのが辛いところです...
以下パッと考えられる原因集:
- Alphaが0
- Scaleが小さすぎる
- width, heightが小さすぎる
- setActiveがfalseになっている
- 変なアニメーションを追加したせいで上記の値が変化して見えなくなってる
- そもそも生成できてない(コード側のミス)
- 画面外に生成されている
- (2Dの場合)z座標の関係でカメラから見えていない
- CanvasのOverlayの描画順がミスってる
ムカつくので、こいつらをフローチャートにまとめました。
フローチャート
漏れもあると思いますが大体こんなイメージ(拡大して閲覧下さい)。また思いついたら修正します。
おわりに
InspectorからColorを設定する場合、なぜかAlphaの初期値が0になっていることがあるので十分注意しましょう。30分コースの可能性があります。
Unityの便利な無料ライブラリ(パッケージ)を紹介する
今回はUnityの便利なライブラリ(パッケージ)を列挙していきます。
全部GitHubに公開されています。
⚠️注意
- 非常に情報の確度が低い内容になっています。間違い等があればコメントにて指摘ください。
- 個人の感情が多分に含まれます。ご注意ください。
目次
- ⚠️注意
- 目次
- 結論
- 1. UniTask - 非同期処理何でも屋さん
- 2. UniRx - リアクティブの広辞苑
- 3. DOTween - アニメーションがコードで書ける
- 4. VContainer - らくらくDIコンテナ
- 5. uPalette - UIの色を管理できる
- 6. LucidAudio - 分かりやすいAudioManager
- 7. LucidEditor - 無料のEditor拡張系の中では最強?
- 8. MessagePipe - Pub/Subが綺麗に実装できる
- 9. SoftMaskForUGUI - Unityのダメダメソフトマスクを改善
結論
以下は全部独断と偏見です。異論は当然認めます。
パッケージ名 | 個人的好き度 | 学習難易度 | 汎用性 |
---|---|---|---|
UniTask | ★★★★☆ | ★★★★★ | ★★★☆☆ |
UniRx | ★★★★★ | ★★★★☆ | ★★★☆☆ |
DOTween | ★★★★★ | ★★☆☆☆ | ★★★★★ |
VContainer | ★★☆☆☆ | ★★★☆☆ | ★★★★☆ |
uPalette | ★★☆☆☆ | ★☆☆☆☆ | ★★☆☆☆ |
LucidAudio | ★★★☆☆ | ★☆☆☆☆ | ★★★☆☆ |
LucidEditor | ★★☆☆☆ | ★★☆☆☆ | ★★☆☆☆ |
MessagePipe | ★☆☆☆☆ | ★★☆☆☆ | ★★☆☆☆ |
SoftMaskForUGUI | ★★★★☆ | ★☆☆☆☆ | ★☆☆☆☆ |
1. UniTask - 非同期処理何でも屋さん
超有名シリーズ。非同期処理(async/await
)を使いたい場合はまず導入したいです。
簡単に複雑な非同期処理(処理を同時並行で行うこと)が記述できます!
一応、UnityにもCoroutine
やAwaitable
といった非同期処理用の機能が存在しますが、より応用が利くのはUniTaskといったイメージです。
ですが、後述の理由から、無理してUniTaskを使うまでもない機能はCoroutine等で対応した方が安全な場合も多いです。
✅Good
- 非同期処理ができる
- ロード中にグルグル表示したり、ターン制ゲームの待ち状態を作ったり、条件達成したら動くエネミーを作ったり...
- ゲームジャンルによってはかなり重宝する
- 何でも待てる
- UniTask, Coroutine, Async○○, WaitWhile(Func)...
- 後続のUniRx等とも相性がいい
- 文法自体は分かりやすい
- Coroutineよりも直感的
❌Not Good
- 学習コストが超絶高い
- UniTaskを使った非同期処理を適切に書くのがめちゃムズイ
- 偉そうな口調であれだけど、自分もまだうまく使えてない...
- UniTaskを使った非同期処理を適切に書くのがめちゃムズイ
- 待ちの処理を止めるのが非常に面倒
- 止めないと大変なこと(=メモリリーク)になる可能性大だけど、正しく止めるのが難しい
使用例
(厳密には下の使用例は無作法です。メモリリークの危険性があります。全メソッドにCancellationToken
を渡しながら例外処理で破棄するようにしたり、MonoBehaviour
のライフタイムに合わせて破棄するGetCancellationTokenOnDestroy()
を使うべきです。)
public class UniTaskTest : MonoBehaviour { // 1秒待ったり private async UniTask Wait1Second() { await UniTask.Delay(1000); Debug.Log("1秒経過"); } // ロードを待ったりできる private async UniTask WaitLoad() { var hoge = await Resources.LoadAsync("Prefabs/Hoge"); // ファイルをロードする Debug.Log("ロード完了"); } // 実際に使う場合 private void Awake() { WaitLoad().Forget(); Debug.Log("ロード中の処理"); } }
2. UniRx - リアクティブの広辞苑
超有名シリーズその2。
よくUniRx/UniTask
として紹介されることもありますが、元々一緒だったライブラリが分離した、という経緯があります。
こちらも非同期処理を扱えるほか、値の変化を監視するObserverパターンを実現するのが非常に簡単になります。
何でも屋さん感もかなり高いです。頼りすぎるとコードがぐちゃぐちゃになっちゃうかも。
✅Good
- Observerパターン,MVP系パターンの実装が非常に楽になる
❌Not Good
- 非同期処理ができる→それUniTaskでいいじゃん!
- 学習コストが高い
- 結構クセが強い
- 肝心のMVPパターンやObserverパターンのメリットが体感できなかったら学習意欲が出ない
使用例
個人的に「MVPパターンといえばUniRx!」と言っていいくらい毎回利用しています。
キー入力等を読み取る際もかなり便利です。
public class PlayerCollision : MonoBehaviour { [SerializeField] private HpModel hpModel; private void OnTriggerEnter(Collider other) { if (other.CompareTag("Enemy")) hpModel.Add(-1); if (other.CompareTag("HealItem")) hpModel.Add(1); } } public class PlayerPresenter : MonoBehaviour { [SerializeField] private HpView hpView; [SerializeField] private HpModel hpModel; private void Awake() { hpModel.Hp .Subscribe(hp => hpView.Set(hp)) // Hpが変わったら勝手にhpView.Set(hp)が実行される .AddTo(this); // 購読解除処理 } } /// <summary> /// Hpを表示するView /// </summary> public class HpView : MonoBehaviour { [SerializeField] private TextMeshProUGUI hpText; public void Set(int hp) { hpText.text = hp.ToString(); } } /// <summary> /// Hpを管理するModel /// </summary> public class HpModel : MonoBehaviour { public IReadOnlyReactiveProperty<int> Hp => _hp; // <= ここの値が変わったら勝手に通知される private readonly ReactiveProperty<int> _hp = new(); public void Add(int score) { if (_hp.Value + score < 0) _hp.Value = 0; _hp.Value += score; } public void Reset() { _hp.Value = 3; } public int GetScore() { return _hp.Value; } }
3. DOTween - アニメーションがコードで書ける
GitHub - Demigiant/dotween: A Unity C# animation engine. HOTween v2
簡単なアニメーションがコードで書けます!
メソッドチェーン(LINQとかUniRxの記法とか)に慣れていれば学習難易度もかなり低いです。
✅Good
- コードでアニメーションが書ける
- 一々Timelineとかを使わなくても簡単なアニメーションは書ける喜び
- Callback等にも対応している
- アニメーションが終わった後にメソッドを実行、などの簡単な非同期処理もOK
- 複数のアニメーションを連結することも
❌Not Good
- 強いて言うなら、DebugのWarningがイマイチ分かりづらい(Warning消そうとしたら結構面倒)
4. VContainer - らくらくDIコンテナ
DI(依存性注入、Dependency Injection)を簡単に行うためのパッケージ。DIコンテナです。DIについてはここでは踏み込んで説明しませんが、非常に有用な概念です。誤解を恐れずにメリットを一言でまとめると、
Awake,Startの順番やオブジェクトの参照に迷うことが無くなる
↑こんな感じ。
✅Good
- 簡単にDIできる
- DIはいちいちコンストラクタや自前のInit()メソッドから参照を注入しないといけないので書き忘れが多発したり何よりちょっと面倒だけど、DIコンテナを使えばそれは軽減する
- そもそもDIは頼もしい
- 軽い(らしい)
- 競合のDIコンテナであるZenjectよりもかなりパフォーマンスがいいらしい
❌Not Good
- 学習コストが高め
- そもそもDIの概念を理解するのに結構勉強しないといけない
- VContainer自体も若干ムズカシイ
- 難しい参照関係を解決しようとすると、自前で書いた方が分かりやすい感がある
- (本当に個人の感覚です)
- そもそもVContainerに慣れてないので、こういう感覚になってる気がする(そもそも複雑なパターンは自分で実装できない)
5. uPalette - UIの色を管理できる
uPaletteはUIの色を一括で管理して、編集しやすくするパッケージです。本当にそれだけなので書くことは無いのですがオススメです。
6. LucidAudio - 分かりやすいAudioManager
Unityの音管理は誰もが面倒だと感じるはずです。たいていの人は自分でテンプレートを作ったり、何らかのパッケージに頼ったりしているのではないでしょうか。
そんな頼れる音管理系のパッケージの候補の1つに、LucidAudioが挙げられます。
✅Good
- 超シンプル
- 基本的な機能は簡単なメソッドチェーンで実行できる
❌Not Good
- 複雑な音管理はできない
- これは良い点でもあるが...
- 多分再生開始時間などは設定できない
- 音管理にこだわるならfmodとかなのかな?
7. LucidEditor - 無料のEditor拡張系の中では最強?
Editor拡張と言えば有料のOdin等が有名ですが、無料でここまで多機能なのは珍しいと思います。
以下できる機能(README見た方が早い):
8. MessagePipe - Pub/Subが綺麗に実装できる
Pub/SubはUniRxでも実装可能な機能ですが、DIコンテナと一緒に実現する場合には、MessagePipeを使うという選択肢もあります。
前提としてVContainer/Zenjectのいずれかに加え、UniTaskの導入が必要です。
9. SoftMaskForUGUI - Unityのダメダメソフトマスクを改善
Unity標準のソフトマスクは結構汚いです。できればSoftMaskForUGUI等を利用しましょう。