【Unity】コルーチンの待ち時間の考え方【フレームで待つか時間で待つか】
フレームを跨いだ処理を行う場合はコルーチンを使ったり、あるいはasync/awaitを使ったり、UniRxを使ったりします。
個人的には最初にやるならコルーチンの方がとっつきやすいと思うので初心者の方にはコルーチンをおすすめしています。
このページではコルーチンの特徴でもある待ち時間に注目して、その違いを紹介します。やりたいことに合わせてある程度柔軟に処理を待つことができるので使い分けていくと便利です。
環境
macOS Catalina 10.15
Unity2019.4.4f1
コルーチンについて
コルーチンとは処理を中断した後、同じ部分から続けて再開できる処理のまとまりです。なので、指定時間待った後に後続の処理を続けたり、1フレームごとにImageのアルファの値を変化させて、徐々にフェードインさせる、なんてときに使える機能です。
コルーチンの詳細については以下の記事もご覧ください。
コルーチンの処理の待ち方
コルーチンでは「yield return」を使って、returnの後に指定した時間、あるいは処理の単位だけ処理を待つことができます。
例えば以下の方法で処理を待つことができます。
このスクリプトの中では、
- 1フレーム処理を待つ
- 指定した秒数だけ処理を待つ
- 別のコルーチンの終了を待つ
という3種類のウェイト処理を書いています。
1フレーム処理を待つ
1フレームだけ処理を待つ場合は
yield return null;
を使います。もし複数フレーム待ちたい場合は以下のようにfor文などを使って表現すると便利です。フィールドに「waitFrame」というint型のフィールドを定義している想定です。
「waitFrame」の値を変更すれば待つフレーム数を調整できるので、インスペクターウィンドウから設定できるようにしておくと確認しやすいです。
1フレーム処理を待つ方法は、Start()のタイミングで各スクリプトが呼ばれて負荷が高くなってしまうのを避けることにも使えます。通常、スクリプトの初期化処理をStart()のタイミングで行います。シーン内のスクリプト数がそこまで多くない場合は気にならない範囲ですが、スクリプト数が多くなると無視できない処理時間になったりします。
ゲーム中の動作として、シーンの読み込み時には暗転させていることが多いため処理落ちしていても気にならないこともありますが、例えばロード中のアニメーションを再生している場合は処理時間が長くなるとアニメーションの描画が遅れるので「カクついてるな」と目に見えてわかりやすくなります。
そこで、この「yield return null;」を使って処理を行うフレームをずらすことによって、各フレームでの処理時間を調整することができます。スクリプト間でメソッドの呼び出しがある場合は、呼び出されたタイミングで参照を確認するなどの処理は必要ですが、スムーズに画面描画を行うことができるのでカクツキが気になる場合はこの辺りも調整してみると良いと思います。
指定した秒数だけ待つ
指定した秒数だけ待つ方法はゲームプレイと連動していて分かりやすい方法です。どちらかというとユーザー目線での待ち時間設定かもしれません。
yield return new WaitForSeconds(<秒数>);
といった形で処理を待ちます。「WaitForSeconds」はコルーチンの実行を待つためのクラスで、引数に待ち時間の秒数を入れて指定します。
ここで指定した秒数を正確にカウントして待つのではなく、各フレームの処理が呼ばれたタイミングで指定した秒数を超えているかどうかを判定して、超えていたら後続の処理に進むようになっています。なので指定した秒数より1フレーム以内の遅れがあるかもしれません。1フレームの遅れを検知できるのはプロの格闘ゲーマーくらいでしょうから、そこまで厳密に気にしなくても良い場合がほとんどです。
newキーワードを使っていることから分かるように、yield returnで指定する際に新しいオブジェクトを作成しています。これはテラシュールブログさんで指摘されていたことですが、ループの中で毎回「new WaitForSeconds(<秒数>);」と指定するとGC(ガベージコレクション)の対象となるオブジェクトが発生してしまうことになり、パフォーマンスに若干の影響が生じます。
なので、ループ内で指定秒数待つ処理を入れる場合は、以下のようにループの外で待ち時間のオブジェクトを作成しておくと良いですね。
別のコルーチンの処理完了を待つ
別のコルーチンの処理完了を待つ方法は、処理完了の待ち合わせに便利です。
yield return StartCoroutine(<別のコルーチン>)
yield returnの対象としてコルーチンを指定します。上の方で紹介したサンプルの中では、「AnotherProcess」というコルーチンを起動してその処理完了を待っています。
例えば画面をフェードインさせてゲーム画面が表示されてから処理を行いたい時にはこの方法を使うと便利です。
実はStart()をvoidではなくIEnumeratorに変更することでコルーチンとして呼び出すこともできるので、初期化処理の中で処理を待ち合わせながら順番にメソッドを呼び出していくことができます。
AddressableAssetsを使っている場合、シーンで使うアセットのロードを待ち合わせたいこともあるので、そんな時にコルーチンを使って処理を待つこともできます。
FPSとの兼ね合いに注意
便利なコルーチンですが、PCやMacで開発しているときに気付きにくいのがFPSとの兼ね合い。
処理の待ち方としてフレーム数で待つか、指定秒数で待つかによってFPSの異なる環境では想定していない動きになったりします。
例えば、ちょっと前にリリースした『にゃんこ・ザ・スライダー』というスマホ向けのゲームでは、指定フレームごとに画像を切り替えてエフェクトを表示するようにしているのですが、Unityのエディタでの動作を見ながら調整していたらiPhone実機のエフェクトがえらく間延びした感じになりました。(「パーティクルを使え」の言葉はその通り過ぎるので何も言えませんが)
Unityのエディタでは60FPSで動作していたのに対し、iPhoneでは30FPSで動作しているので、単純にエフェクトの再生時間が倍になっていたのです。
エフェクトの切り替えでは上で紹介した
のように指定フレームだけ待つようにしていました。
これだと実行環境のFPSによって処理時間が変わってしまうので注意が必要になります。今回の例でいうと、エフェクトの再生と合わせて効果音も再生しているので、こちらとも合わせないといけません。
FPSと処理時間との関係については以下の記事でも触れているのでよかったら参考までに。
フレーム指定か、秒数指定か
「yield return null;」を使ってフレームで待ち時間を指定するか、「yield return new WaitForSeconds(<秒数>);」を使って秒数で待ち時間を指定するかはちょっと迷うところではあります。
上で紹介したゲームを作ってリリースしたときの個人的な経験から、FPSに依存させたくない処理なら秒数指定が便利だと思います。例えば画面のフェードインにかかる時間や、オブジェクトを移動させる時間などは時間で指定してプラットフォームが変わったとしても一定の時間で動作するようにすると良いでしょう。
フレーム指定であれば処理側に近い感覚、秒数指定であれば人間側に近い感覚になるので、実装したい処理の種類に応じてうまく使い分けていくと良いと思います。
フレーム指定は上でもちょっと触れましたがStart()の初期化処理をずらすのにも使えるので、処理負荷をいくつかのフレームに分散させることができます。ゲームの演出との兼ね合いもありますが、なるべくなら画面のカクツキはゲームを遊んでくれているユーザーさんには見せたくないので、コーディングする際にちょっと頑張ってみるのも良いかもしれません。
まとめ
コルーチンはいくつかの方法で処理を待つことができ、このページでは3種類の方法を紹介しました。
個人的には演出としての待ち時間は秒数指定で行い、処理負荷を分散させるための待ち時間はフレーム指定で行っています。
待ち方ひとつでユーザーさんのゲーム体験が変わるので、実機での検証を行いながら調整をしてみるとより良いゲームが作れるはずです。
ゲーム開発の攻略チャートを作りました!
-
前の記事
技術情報を探すときには原典(一次ソース)が大事です【ゲーム作り】 2020.09.20
-
次の記事
押入れの整理でドラクエ1の箱を見つけたので思い出を 2020.09.22
コメントを書く