Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

Unityチームでの輪読会の取り組み紹介とUniRx/UniTaskクイズ

こんにちは。ミラティブUnityエンジニアの菅谷(tetsujp84)です。Unityチームの一員として、私たちが週に1時間を費やして行っている輪読会について紹介したいと思います。輪読会では、チーム内で読む本を都度決め、それを読み進めていきます。これまでにはパフォーマンスチューニングの本やUniRx/UniTaskの本を読みました。また、後半にはUniRx/UniTaskの本で学んだことを元にクイズを作成しましたので是非挑戦してみてください。

具体的な流れ

チーム内で読む本を決め、全員が本(現物 or 電子書籍)を用意します。輪読会では、前回の続きから読み始め、15分程度経ったら区切って次の人に交代します。ある程度のまとまりを読んだら、それぞれの意見や重要そうな点、気付きなどを議論します。1回の勉強会は1時間で、できるだけメンバー全員に参加してもらっています。

ポイント・メリット

輪読会には多くのメリットがあります。1人で読むよりも継続的に行えること、組織としてメンバーに知っておいてほしい基礎知識が共有できること、そしてメンバーの経験を元に議論できるため、具体的な知識や活用ポイントがわかることです。

特にパフォーマンスチューニングでは、これまでに行った事例や苦労した点を共有しました。Unityチームでは様々なバックグラウンドを持ったエンジニアが多いため、ニッチだがためになる話など、1人で本を読むだけでは手に入らない知識も獲得できました。

また、これまでの経験を語ることで、誰が何を知っているかをチーム内で共有でき、技術の相談先がわかりやすくなりました。

輪読会の重要性

輪読会では本を読むこと以上に、本の内容を元に経験を語り、議論し、実践に活かせるようにすることが重要です。以前はもくもくタイムのようにして各自の自習時間としたこともありましたが、集中が続かなかったり、結局理解できないまま過ごしてしまったりと効率が悪いように感じました。現在では足並みを揃えて、全員が理解しながら進めることで、チームとしての情報の共有と獲得を意識して実施しています。

輪読会で読んだ本

Unity パフォーマンスチューニングバイブル

CyberAgentさんがGitHub上で公開しており、Unityでのパフォーマンスの計測方法やチューニングのノウハウがまとまった本です。パフォーマンスは全員が意識しなければならない項目ですが、チューニングや改善ができるのは限定的なメンバーだけという状況がよくあります。それを解決すべく全員が一定の知識を持てるよう選びました。

github.com

UniRx/UniTask完全理解 より高度なUnity C#プログラミング

とりすーぷさん著のUniRx/UniTaskの解説本。これまでもUniRx/UniTaskは使用していましたが、簡単な機能しか使えておらず、各々の独学による知識のため習熟度にバラツキがあったためこれを機にチームとして学んでみることにしました。特にUniTaskは新規企画で積極的に活用しており、基礎の必要性が高まっていた背景もありました。そのため輪読会の際も、まずはUniTaskの項目を読み進めたあとにUniRxの項目を読んでいます。UniRx/UniTaskの原理の話や知らない機能の話などが非常にためになり、今後もリファレンスとして活用できそうです。

UniRx/UniTask完全理解 - アスキードワンゴ

2つとも、どのプロジェクトであってもミラティブのUnityエンジニアなら知っており、使えるようになってほしいとの願いから選定しました。UnityやC#をただ使用するだけでなく、より一歩進んだ実用的な知識を組織全員が持っている、そんな姿を目指しています。これら以外にも、グラフィックや設計などもとても重要です。次は何を学ぶべきかは都度メンバーと議論しています。おすすめの本があればぜひご紹介ください。




UniRx/UniTaskの本からの学びとUniRx/UniTaskに関するクイズ

上記のUniRx/UniTaskの本について読み終えたので、そこで獲得した情報を元にクイズを作成してみました。UniRxではMVPの実現のために利用していたり、一部のオペレーターだけしか知らないようなメンバーも多かったのですが、今回の勉強会で全員がしっかりとした理解に繋げられました。

クイズはほとんどが本に載っていた内容を参考に作成しています。完全理解のために全問正解を目指してほしいところ。誤りがあればすぐ直します(汗)UniRx/UniTaskはこれからの開発の基礎知識に思えます。Unityエンジニアであれば是非とも読んでほしい1冊でした。

クイズ

前半はUniRx編(@tetsujp84作)、後半はUniTask編(@adarapata作)です。各答えに本の参照先を載せています。

UniRx編

Q1

Observer、Observable、Subjectのそれぞれについてを説明せよ。特に、ObserverとObservableの違い、ObserverとSubjectの違いがわかるように説明せよ。

A . クリックで答えを表示

Observerはイベントメッセージの観察者。イベントメッセージを受信してハンドリングを行う Observable(IObservable)はメッセージの受信ができるようになる口のことで、Observerを受け入れることができるようになっている。 Subjectはイベントメッセージの発行者。 (2.1 Observableとは何か)

Observerはメッセージの受信口のインターフェースとしてIObservableを実装している。 Observerはメッセージの観察者で、Subjectはメッセージの発行者である。 一方でSubjectはIObserverとIObservableの両方を実装しているため、メッセージの観測者としても振る舞うことができる。

Q2

OnErrorメッセージが発行された際、OnNextメッセージおよびOnCompletedメッセージは発行されるか。

A . クリックで答えを表示

ともに発行されない。また、OnErrorメッセージが発行されると購読も終了するためOperatorはすべて停止する。 (2.2.3 Observableを破棄する方法)

Q3

Q2ようなOnError発生後にエラーハンドリングを行い、メッセージの購読をやり直すOperatorは何か

A . クリックで答えを表示

Catch + RetryやOnErrorRetry。 (4.6 エラーハンドリング系 Operator)

Q4

Hot ObservableとCold Observableの違いについて説明せよ。

A . クリックで答えを表示

Coldは誰も購読しておらず稼働していないObservable、Hotはすでに稼働しているObservable。 (2.3.3 Hot ObservableとCold Observable)

Q5

最初からHot Observableのみを作らず、Cold Observableを作っておいて、後にHot変換するメリットは何か。Hot変換のメリットは何か。

A . クリックで答えを表示

Operatorは購読されるたびに新しいインスタンスを生成する。Hot変換を用いることで新しいインスタンスの生成を防ぎ既存のOperatorをそのまま使いまわしつつ複数回の購読が可能になる。 (2.3.3 Hot変換を用いるメリット)

Q6

Hotに変換するためのOperatorは何か。

A . クリックで答えを表示

Cold Observableに対してSubjectを用いてSubscribeすることでHotに変換する。また、Hot変換用OperatorにはMulticast/Publish(Multicastの省略形)/Replay(Multicast+ReplaySubjectの省略形)などがある。 (4.7 Hot変換用 Operator)

Q7

Publish().RefCount()の省略記法のOperatorは何か。

A . クリックで答えを表示

Share。 (4.7.6 Share)

Q8

Schedulerには複数種あるが、ImmediateSchedulerとCurrentThreadSchedulerについて考える。 Observable.Return(1, [Schedulerの指定]).Repeat().Take(1).Subscribe(); はどちらかが無限ループ、どちらかが正しく停止する。正しく停止するほうはどちらか。また、その理由を説明せよ。

A . クリックで答えを表示

正しく停止するのはCurrentThreadScheduler。ImmediateSchedulerは直ちに実行するが、CurrentThreadSchedulerは一度キューに詰めてから順番に実行する。ImmediateSchedulerではRepeatによるSubscribeの再実行がOnCompletedメッセージによるObservableの解体より先に行われてしまう。 (2.4.3 ImmediateSchedulerとCurrentThreadSchedulerの違い)

Q9

MainThreadSchedulerとメインスレッド上でCurrentThreadSchedulerを使う際の違いや問題になるパターンはあるか。

A . クリックで答えを表示

Observable.Timerでの計測時にMainThreadSchedulerは内部のコルーチンで時間計測を行うため秒数を計測できるが、CurrentThreadSchedulerはThread.Sleepを用いて計測するためUnityのメインスレッドを止めるためフリーズさせてしまう。 (2.4.4 Schedulerの使い方)

Q10

BehaviourSubjectのようにメッセージをキャッシュできるが、BehaviourSubjectとは異なり複数のキャッシュが可能なSubjectは何か。

A . クリックで答えを表示

ReplaySubject。 (3.1.3 ReplaySubject)

Q11

ReactivePropertyについて、直前の値と同値でもメッセージを発行する方法(①)および購読時のメッセージを無視する方法(②)は何か。

var p = new ReactiveProperty<int>(1);

p.Value = 1; // これでは同値なので発行しない
正しく①の方法を示せ

p.Subscribe(v => Debug.Log(v)); // これではSubscribe直後にもOnNextメッセージが発行されてしまう
正しく②の方法を示せ

A . クリックで答えを表示

直前の値と同値でもメッセージを発行する方法:p.SetValueAndForceNotify(1); 

購読時のメッセージを無視する方法:p.SkipLatestValueOnSubscribe().Subscribe(v => Debug.Log(v))
(3.2.1 ReactiveProperty)

Q12

Listにメッセージ発行機能が追加されたものは何か?また、どんなことができるようになっているか。

A . クリックで答えを表示

ReactiveCollection。要素の追加や削除といったリスト操作時にメッセージが発行される。 (3.2.3 ReactiveCollection)

Q13

何も発行しないObservableを生成するファクトリメソッドは何か。またその使い道はどこか。

A . クリックで答えを表示

Observable.Never (3.3.6 Observable.Never)。テストやデバッグで強制的に止めたい場合に使う。

Q14

1,2,3を出力して、CompleteするCold Observableを作れ。使用するファクトリと書き方があっていればよい。(外部のUniRxのQuizから出題)

A . クリックで答えを表示

var observable = Observable.Create<int>(_observer =>
{
    _observer.OnNext(1);
    _observer.OnNext(2);
    _observer.OnNext(3);
    _observer.OnCompleted();
    return Disposable.Empty;
});

参考:UniRxQuiz/Assets/Editor/ColdObservable/AnswerTest.cs at master · mattak/UniRxQuiz · GitHub

またはObservable.Range(1, 3)。

Q15

Observable.TimerとObservable.Intervalの違いは何か。使い分けは何か。

A . クリックで答えを表示

Timerは1度だけの発行も可能。両方とも繰り返しの発行が可能だが、Timerは最初に待機する時間が指定できる。 (3.3.13 Observable.Timer、3.3.15 Observable.Interval)

Q16

Observable.EveryUpdateやObservable.EveryFixedUpdateはlong値を取得できるが、これは何の値か。

A . クリックで答えを表示

Subscribeしてからのメッセージの発行回数。 (3.4.3 Observable.Every〜シリーズ)

Q17

Observable.EveryUpdateに比べ、UpdateAsObservableのほうが安全に使用できるが、Observable.EveryUpdateのメリットは何か。

A . クリックで答えを表示

MonoBehaviourに依存しない処理が実行できることや、複数のGameObjectから大量にSubscribeしても実行負荷が増えないこと。 (3.4.4 Observable Triggers)

Q18

StateMachineBehaviourはUnityのAnimatorにアタッチでき、Animator上のステートマシンのコールバックが取得できるコンポーネントである。StateMachineBehaviourからのコールバックをObservaleに変換するUniRxの機能/クラスは何か。また、この機能を利用して、ステートがAttackの場合にのみイベントを実行するサンプルを記述せよ。

A . クリックで答えを表示

ObservableStateMachineTrigger。具体的な使用例は (3.7.1 ObservableStateMachineTrigger) の使い方を参照。

Q19

入力されたメッセージ値が直前のものと同値であった場合はメッセージを遮断し、値に変化があった場合にのみメッセージを通過させるOperatorは?

A . クリックで答えを表示

DistinctUntilChanged。 (4.2.5 DistinctUntilChanged)

Q20

First、FirstOrDefault、Singleはどれも1つのメッセージを取得するOperatorである。それぞれの違いを説明せよ。

A . クリックで答えを表示

Firstは1つも条件を満たさずにOnCompletedメッセージが入力されたらOnErrorを出す。FirstOrDefaultはOnErrorを出さずにdefault(T)をメッセージ値としてOnNextメッセージを発行する。Firstは条件を満たしたら途中でOnNextとOnCompletedをセットで出力するが、SingleはOnCompletedが入力されるまでOnNextメッセージを発行せず、その間で複数条件を満たした場合にOnErrorを発行する。 (4.2.6 First、4.2.7 FirstOrDefault、4.2.10 Single)

Q21

大量に入力されたOnNextメッセージを間引くOperatorは何か。また、Operatorを用いて「GameObjectが静止するまで待ってからその座標を取り出す」コードを書け

A . クリックで答えを表示

Throttle。コードは (4.2.21 Throttle) を参照。

Q22

MergeとConcatは共に複数のObservableを結合するOperatorだが、その違いを説明せよ。

A . クリックで答えを表示

Mergeは並列結合、Concatは直列結合。そのためメッセージの順番が変わる場合がある。 (4.4.1 Merge、4.4.2 Concat)

Q23

以下の処理に対し、適切なOperatorを使用して、Observable.Rangeにて発行された値すべてを足し合わせるコードを完成させよ。

Observable.Range(0, 10).オペレーター.Subscribe(x => Debug.Log(x));
// 出力は45となる

A . クリックで答えを表示

Aggregate。 (4.4.6 Aggregate) また、Scanは途中経過もOnNextで出力できるので、途中経過の確認などで使用する。

具体的な記述方法

Observable.Range(0, 10).Aggregate(0, (pre, cur) => pre + cur).Subscribe(x => Debug.Log(x));

Q24

1つのObservableの、複数メッセージをまとめて1つのOnNextメッセージに変換するOperatorは何か。また、1個前と最新のメッセージをセットにして出力するOperatorは何か。

A . クリックで答えを表示

BufferとPairWise。 (4.4.8 Buffer、4.4.10 PairWise)

Q25

以下のコードにおいてメッセージはどのように出力されるか?

var obs1 = Observable.Interval(TimeSpan.FromSeconds(1)).Select(value => "Observable 1: " + value);
var obs2 = Observable.Timer(TimeSpan.FromSeconds(0.5)).Concat(Observable.Interval(TimeSpan.FromSeconds(1)).Select(value => "Observable 2: " + value));

Observable.Amb(obs1, obs2).Subscribe(value => Debug.Log(value));

A . クリックで答えを表示

"Observable 2: 0", "Observable 2: 0", "Observable 2: 1", "Observable 2: 2",...

TimerのOnNextのあとにIntervalのOnNextになるため、"Observable 2: 0"が2つ出力される。 Ambは最速のObservableを採択して、以降は採択したObservableからのみ取得する。 (4.4.16 Amb)

Q26

OperatorのMaterializeを説明せよ。

A . クリックで答えを表示

すべてのメッセージをOnNextメッセージに変換するOperator。 (4.9.8 Materialize)

UniTask編

Q27

UniTaskでは同じオブジェクトに対して2回以上awaitを行うと例外が発生するが、これを回避するためにはどのような方法があるか

A . クリックで答えを表示

Preserve(); を呼び出すと何度でもawaitできるUniTaskになる (7.2.2 UniTaskVoid)

Q28

Buttonのクリックイベントをawaitする際に、 以下の二つの方法はどのような違いがあるか

var handler = _button.GetAsyncClickEventHandler(token);
await handler.OnClickAsync();
await _button.OnClickAsync(token);

A . クリックで答えを表示

前者のAsyncHandlerを取得する方法の場合、handlerを呼び出し側で明示的にDisposeする必要がある。 後者の場合は内部的にAsyncHandlerを生成して、awaitし終わったら即時でDisposeをされている。 そのため繰り返しawaitを行う場合はAsyncHandlerを取得したほうがパフォーマンスが向上する。 (7.3.4 uGUIの各種イベントのawait)

Q29

UniTaskCompletionSourceAutoResetUniTaskCompletionSourceの違いは何か

A . クリックで答えを表示

AutoResetUniTaskCompletionSourceは内部でオブジェクトプールされている。また、複数回のawaitができない。 そのため、1回きりの使い捨てとしてのTaskを作りたい場合はAutoResetUniTaskCompletionSourceの方が適している。(7.4.5 UniTaskCompletionSourceから生成する)

Q30

Cancelが発生した時に例外を発生させず、キャンセルの状態をboolで取得する方法は何か

A . クリックで答えを表示

SuppressCancellationThrow (8.2.9 SuppressCancellationThrow)

Q31

1フレーム待機したいときに使えるメソッドは何があるか(複数可)

A . クリックで答えを表示

UniTask.Yield(); 
UniTask.Yield(PlayerLoopTiming.Update); 
UniTask.NextFrame();
UniTask.DelayFrame(1);

などなど

Q32

UniTaskAsyncEnumerable.ForeachAwaitAsyncUniTaskAsyncEnumerable.Subscribe の違いは何か

A . クリックで答えを表示

MoveNextAsync() が呼ばれるタイミングが違う

  • ForeachAwaitAsync はawaitが呼ばれた後
  • Subscribeは同期的に呼ばれる

デリゲート内でasync/awaitを使った場合の挙動が違う

  • ForeachAwaitAsyncはUniTask 型になる
  • SubscribeはUniTaskVoid型になる

(9.2.4 Subscribe)

Q33

UniTask.EveryValueChanged で生成したIUniTaskAsyncEnumerableが完了する条件は何か

A . クリックで答えを表示

  • 監視対象が UnityObject.Object の場合はDestroy時に完了する
  • System.Objectの場合はGCで回収された場合に完了する

(9.3.1 ファクトリメソッド)

Q34

以下のコードを実行した場合、ログはどのような違いが出るか Time.frameCount は 0からスタートするものとする

UniTaskAsyncEnumerable  
    .EveryUpdate()
    .Select(_ => Time.frameCount)  
    .ForEachAwaitAsync(async count =>  
    {
        Debug.Log(count);  
        await UniTask.DelayFrame(5);  
    });
UniTaskAsyncEnumerable  
    .EveryUpdate()
    .Select(_ => Time.frameCount)  
    .Queue()  
    .ForEachAwaitAsync(async count =>  
    {  
        Debug.Log(count);  
        await UniTask.DelayFrame(5);
    });

A . クリックで答えを表示

前者は5フレームごとに5刻みで出力される

0
5
10
15
20

後者は5フレームごとに順番に出力される

0
1
2
3

QueueメソッドはUniTaskAsyncEnumerableを一度消費し、その結果をキューに蓄積する。 そのため ForeachAwaitAsyncの非同期処理とは独立して動いている。

(9.4.1 特殊なLINQメソッド)

Q35

Channelはメッセージの消費と購読は1対1が前提となっている。複数箇所で購読する場合はどのようにするとよいか

A . クリックで答えを表示

Publish() を使う (9.5 Channel)

Q36

UniTaskのキャンセルについて、以下の質問に答える形で考えてみましょう(自由記述)

  • awaitするメソッドにCancellationTokenを渡さないとどのような危険性が考えられるか
  • UniTaskのファクトリメソッドを用いない自作のTaskであってもキャンセルされたときにはOperationCanceledException例外が発生するか

以上でUniRx/UniTaskのクイズは終了です。何問分かったかな?

We are hiring!

今回はUnityチームで行っている輪読会について紹介しました。ミラティブでは輪読会などの組織として学ぶ機会や環境を大切にしています。ミラティブに入ったらプロダクトやチームを通してスキルが向上し、それによりサービスの成長にも貢献できたと実感してもらえるよう、日々組織づくりに取り組んでいます。また、一緒になってを組織を拡大し、チームを成長させていけるメンバーを大募集しています!

www.mirrativ.co.jp

speakerdeck.com

mirrativ.notion.site