Unityに関連する記事です

 前回の手順につづき、ソースコードの記法学習を行います。こちらは中上級者向けの内容になっています。

 プログラムにおいては、処理の結果が同じ内容になる場合であっても、記述するソースコードの内容が異なるケースがあります。
ここでは1つの目的(処理の結果)をベースに、異なるソースコードの記法を学習します。



目的


 ゲーム内時間を計測し、1秒ごとに画面の表示を更新する

 画面の表示については別の手順で行いますので、ここでは、ゲーム内時間の計測の仕方について
様々なソースコードの記法を紹介します。

 いずれの記法においても、処理内には Debug.Log メソッドを用いて、Console ビューにて時間が正常に計測できているかを確認します。


<実行時の処理の確認>



 各ソースコードを記述したら、必ずデバッグを行います。
上記のように Console ビューに Debug.Log メソッドの実行結果が表示されることを確認してください。


記法3 ーUniRx の活用ー


 4つの方法を紹介します。
そのうちの1〜3については、いずれもファクトリ・メソッドを利用した処理になります。


1.Observable.EveryUpdate メソッド


 Observable.EveryUpdate は UniRx に用意されているファクトリ・メソッドです。
ファクトリ・メソッドとは、Subject を用意しなくても Observable を生成して利用できる static メソッド群のことです。
利用する場合には、UniRx の宣言に加えて、Observable クラスを記述することで、ファクトリ・メソッドを実行できます。

 Observable.EveryUpdate メソッドは、MonoBehaviour の Update メソッドを Observable に変換して利用できます。
ここではその機能を利用して処理を作成しています。


TimeSample_4.cs



<Observable.Every 〜 メソッド群の留意点>


 なお Observable.EveryUpdate には同様に Observable.Every 〜 で始まるメソッドがいくつか用意されています。
これらの Observable はいずれも OnCompleted によるメッセージの発行を行いません。そのため、Dispose を忘れずに行う必要があります
(忘れてしまうと、裏側でずっと処理が実行され続けてしまい、メモリリークを引き起こす原因になります。)

 今回は AddTo() メソッドを利用して、ゲームオブジェクトの OnDestroy のタイミングに連動して Dispose を行い、購読処理を停止しています

 以下の動画は、OnCompleted によるメッセージの発行を行わない Observable.EveryUpdate の購読処理において、
AddTo メソッドを記述していない場合と、記述した場合の処理の違いです。

 ゲーム実行中に Observable.EveryUpdate の処理が書いてあるスクリプトがアタッチされているゲームオブジェクトを削除したときに、
購読の処理が停止するか、停止せずに動き続けてしまうかをデバッグして確認しています。


<AddTo メソッドを記述していない場合に、ゲームオブジェクトを削除したケース>
動画ファイルへのリンク

 → すでにスクリプトがアタッチされているゲームオブジェクトが削除されているにも関わらず、Observable.EveryUpdate の購読処理が停止しません
   これがメモリリークの原因の1つになります。



<AddTo メソッドを記述した場合に、ゲームオブジェクトを削除したケース>
動画ファイルへのリンク

 → ゲームオブジェクトの削除に合わせて AddTo メソッドが Dispose を行い、Observable.EveryUpdate の購読処理が停止しています
   これが正しい処理になります。



 このように実際に自分のプロジェクトでも処理を記述したり、コメントアウトしたりして、購読の停止処理にどのような違いが発生するかを確認しておくことをお勧めします。
特に記事を読み解いただけでは処理の内容を具現化してイメージすることが難しいので、実際に自分で試して体験することがプログラムにおいてはスキルアップにつながります

 常にそういったスタンスでの学習を心掛けるようにしましょう。


2.Observable.Interval メソッド


 Observable.Interval もファクトリ・メソッドの1つです。(Observable クラスの宣言から処理が始まります)

 Observable.Interval メソッドは、第1引数に指定した値を時間の間隔として、無限にメッセージを発行します。
このメソッドは OnCompleted メッセージの発行を行いませんので、購読を停止させるためには止めたいタイミングで Dispose を行うか、
AddTo メソッドなどによるゲームオブジェクトの破棄に紐付けて、購読の停止処理を記述する必要があります。


TimeSample_5.cs


 上記のように Interval メソッド内の第1引数の指定(System.TimeSpan 型)を1秒に指定(FromSeconds(1))することで、
1秒間隔でメッセージを発行することにより、Debug.Log メソッドも1秒ごとに実行されます。

 Interval メソッドは Subscribe したタイミングから計測が始まりますので、最初に1秒待ってから、1秒間隔でのメッセージ発行になります。
そのため利用する場合にはそれを加味した上で処理を構築する必要があります。そのため、Debug.Log メソッドの記述も他のものと異なっています。

 Subscribe したときには long 型の値が発行されます。値にはメッセージを発行した累計数が代入されています。
上記の処理であれば、x の値は long 型になっており、メッセージを発行されるたびに、0、1、2 と x の値が加算されています。


3.Observable.Timer メソッド


 Observable.Timer もファクトリ・メソッドの1つです。(Observable クラスの宣言から処理が始まります)

 Observable.Timer メソッドと Observable.Interval メソッドは似ていますが、
こちらの方は TimeSpan 型の引数を1つ、あるいは2つのいずれかで指定可能であり、処理の動きが変わります。
そのため、よりが柔軟な処理が記述出来るようになっています。

 第1引数(System.TimeSpan 型)に指定した時間を待機時間として利用します。System.TimeSpan.Zero と記述すれば、待機時間はなしになります。
このとき、第2引数の指定がなければ、その時点で処理は終了し、OnCompleted メッセージが発行されます
つまり、待機時間の経過をまって、1度だけ処理を行うことになります。(繰り返しません。)

 第2引数(こちらも System.TimeSpan 型)を利用する場合、第1引数に指定した待機時間後に、指定した時間の間隔でメッセージを発行します。
第2引数を記述している場合、この時間の間隔で無限に処理を繰り返し、OnNext メッセージが発行されます
そのため、第2引数まで記述すると、それは Interval の第1引数と同じ機能になります。
その場合、OnCompleted メッセージが発行されませんので、第2引数まで記述した場合には、Observable.Interval と同じように Dispose を行う必要があります。


TimeSample_6.cs



<Observable.Interval と Observable.Timer の違いと共通の注意点>


 Interval と Timer を利用して繰り返しの処理を行う場合には、最初のメッセージを発行するタイミングが異なることがポイントです。

 共通の注意点としては、Interval と、上記のように Timer に第2引数まで記述した場合、どちらも無限に繰り返すので、OnCompleted メッセージが発行されません
よって手動で Dispose する必要があります。これを忘れないようにしてください。

 AddTo メソッドを利用できますので、ゲームオブジェクトの破棄のタイミングで購読を停止したい場合には、AddTo メソッドを利用して Dispose するのがよいでしょう。


4.UpdateAsObservable メソッド


 こちらは ObservableTriggers に含まれるメソッドです。
namespace としては UniRx だけではなく、using に UniRx.Triggers の宣言が必要になります。

 ObservableTriggers は、Unity の用意しているイベント関数を Observable として扱えるようにする機能です。
GameObject 型に関連したイベント関数を元にしたメソッドが多数用意されており、
その中の1つに Update メソッドを Observable として利用できる UpdateAsObservable メソッドがあります。

 こちらは【1.Observable.EveryUpdate】と同じように利用しますが、UpdateAsObservable メソッドには OnCompleted によるメッセージの発行があります。
そのため、AddTo メソッドなどによる Dispose の処理は記述不要です。(書き忘れによる Dispose 漏れの心配がありません。)


TimeSample_7.cs



<Observable.EveryUpdate と UpdateAsObservable の違い>


 Observable.EveryUpdate と UpdateAsObservable はどちらも Update メソッドを Observable 化(変換)したものですが、多くの部分で違いがあります。
特に重要な部分としては、以下のような部分です。
Observable.EveryUpdate
UpdateAsObservable
1.メッセージのデータ型longUnit
2.OnCompleted メッセージ発行されないゲームオブジェクトの破棄時に発行される
3.イベント関数の Observable 変換グローバルなイベントGameObject に関連したイベント

 他にも、Observabe を管理するゲームオブジェクトの違いや、イベント発行のタイミングの違いなどもあります。
Observable.EveryUpdate は特定のゲームオブジェクトに紐づかなくても利用できます(MonoBehaviour を継承していないクラスでも利用できる)が、
UpdateAsObservable はゲームオブジェクトの Component(MonoBehaviour)に紐づき、その Update メソッドのタイミングでイベントを発行します。

 下記のサイトに詳細説明が載っています。
それぞれのメリット・デメリット、利用するケースなどを確認してみてください。


<参考サイト>
Qiita @toRisouP(株式会社 バーチャルキャスト) 様
UniRx入門 その4 -Updateをストリームに変換する方法とメリット-



記法4 ーUniTask の活用ー


 UniTask を利用した処理のケースについて、2つ提示します。

 どちらも UniTask に用意されているファクトリ・メソッドを活用し、async / await の機能を利用して時間の計測処理を行うことが出来ます。
その場合、非同期処理を行うメソッドであることを示すため、メソッド名の最後には 〜 Async と記述するようにすることを推奨します。


<1.UniTask.Delay メソッド>


 UniTask.Delay メソッドを利用したケースです。
引数には4種類のオーバーロードが用意されていますが、今回は第1引数に int 型を利用したケースで利用しています。
これは millseconds 単位で値を指定できます。今回は 1000 millseconds とすることで 1秒を指定しています。

 このとき、初期値のある値はそのまま利用し、特定の初期値だけを上書きしたい場合には、
【初期値で用意されている変数名 : 値】と記述することで、指定した初期値のみを上書きして利用することができるテクニックがあります。

 今回は第2、第3引数は初期値を利用し、第4引数のみを上書きして利用しています。


TimeSample_8.cs



<初期値のある引数をそのまま利用し、特定の引数だけを上書きして利用したい場合>


 Delay メソッドの引数には4種類のオーバーロードがあり、いずれも、第2〜第4引数には初期値が代入されています。
今回利用しているのは、第1引数から順番に、int 型、bool 型、PlayerLoopTiming 型、CancellationToken 型のものです。
ここでは利用している定義のみ記述しておきます(よって引数の指定が違う同名の Delay メソッドが、他に3種類あります)。


<Delay メソッドの定義>
public static UniTask Delay(
    int millisecondsDelay,
    bool ignoreTimeScale = false,
    PlayerLoopTiming delayTiming = PlayerLoopTiming.Update,
    CancellationToken cancellationToken = default(CancellationToken))

 第2〜第4引数には初期値があるため、第1引数のみ指定すれば処理はエラーなく記述出来ます。

<第1引数のみ指定>
 // 1秒待機
  await UniTask.Delay(1000);



 また、すべての初期値を上書きして利用したい場合、あるいは初期値と同じ値を指定したい場合には、次のようになります。

<すべての引数を記述する場合>
 // 1秒待機
  await UniTask.Delay(1000, false, PlayerLoopTiming.Update, token);   // 第1引数〜第4引数までを指定している。



 最後に、今回活用している、初期値はそのまま利用しつつ、特定の初期値だけを上書きして利用したいケースです。
その場合、定義で用意されている変数名と値を記述します。


<初期値のある引数をそのまま利用し、特定の引数だけを上書きして利用したい場合>
 // 1秒待機
  await UniTask.Delay(1000, cancellationToken: token);   // 第1引数と、第4引数を指定している。第2、第3引数は初期値を利用。

 初期値のある引数の記述を省略しつつ、特定の引数だけを活用したい場合に利用できる記述テクニックになります。
引数の数が多いメソッドの場合に役立つ手法になります。

 特に UniTask に用意されているファクトリメソッドにはキャンセル用の CancellationToken が指定できますので
こちらは忘れずに記述し、キャンセル処理に備えるようにしてください。


<2.UniTask.Yield メソッドを利用した、コルーチンの yield return null の代替処理>


 UniTask.Yield メソッドを活用し、実行コンテキスト(コードを実行する際の状況)を PlayerLoopTiming.Update に指定して await することで、
コルーチンの yield return null と同じように、1フレーム待機の処理を作り出すことが出来ます。


TimeSample_9.cs


 Yield メソッドには引数のオーバーロード機能があり、指定なしでの記述も可能です。(実行コンテキストの指定初期値があります。)
 
 第1引数には実行コンテキストのタイミングを指定して、タイミングを切り替えることも出来ます。
例えば、Update から FixedUpdate のタイミングに変更することも可能です。

  await UniTask.Yield(PlayerLoopTiming.Update, token);
  
  Debug.Log(Time.frameCount);

  await UniTask.Yield(PlayerLoopTiming.FixedUpdate, token);

 第2引数は CancellationToken 型です。
そのため、こちらは必ず指定するようにしてください。


終わりに


 以上になります。

 1つの目的(1秒を計測する)に対して、色々な処理の実装パターンがあることが見えてくると思います。
これらを覚えておくことで、ロジックを考えるとき、処理を構築する際のソースコードの記述の引き出しを増やすことが出来ます。

 ケースに応じて使い分けられるように、設計力、実装力を養っていきましょう。

 実装にあたり、どんな方法があるのかを常に考えて視野を広くもつようにすることも大切になります。

コメントをかく


「http://」を含む投稿は禁止されています。

利用規約をご確認のうえご記入下さい

Menu



技術/知識(実装例)

2Dおはじきゲーム(発展編)

2D強制横スクロールアクション(発展編)

3Dダイビングアクション(発展編)

2Dタップシューティング(拡張編)

レースゲーム(抜粋)

2D放置ゲーム(発展編)

3Dレールガンシューティング(応用編)

3D脱出ゲーム(抜粋)

2Dリアルタイムストラテジー

2Dトップビューアドベンチャー(宴アセット使用)

3Dタップアクション(NavMeshAgent 使用)

2Dトップビューアクション(カエルの為に〜、ボコスカウォーズ風)

VideoPlayer イベント連動の実装例

VideoPlayer リスト内からムービー再生の実装例(発展)

AR 画像付きオブジェクト生成の実装例

AR リスト内から生成の実装例(発展)

private



このサイト内の作品はユニティちゃんライセンス条項の元に提供されています。

管理人/副管理人のみ編集できます