BLOGブログ

2017.01.27UE4UE/ C++

[UE4] 非同期処理を実装する

今回はちょっとプログラム寄りのお話です。

非同期処理の話になりますが、概念を理解していて実装だけを知りたい人は前半部分は読み飛ばして下さい。

 

ゲームの更新処理にかけられる時間について

ゲームやインタラクティブコンテンツではリアルタイムで状況が変化していきます。

それらはプレイヤーの入力によって変わる事も多く、そういったものは事前に処理を行う事ができないため、その時々で更新処理を行う必要があります。

ゲームの場合だとモニターの再描画間隔(リフレッシュレート)に合わせて秒間60回もの画面更新を行うため、1回の描画にかけられる時間は 16.6ms 程度です。

この「単位時間辺りの画面更新回数」をフレームレートと呼び、「秒間に何回更新できるか」を fps(frames per second)で表現します。

秒間で60回画面更新を行う場合は 60fps と表現できます。

また、最近流行りのVRでは 90fps が必要です。

つまりは1回の描画にかけられる時間はたった 11ms 程度ということになります。

 

ではプレイ中に何か重い処理を実行してしまって、画面の描画更新が間に合わなくなったらどうなるでしょうか。

その場合は画面が更新されず、ゲームが一瞬止まったように見えます。

これがいわゆる “処理落ち” と呼ばれるものです。

ParallelTest01

処理落ちはプレイヤーの体験に影響を与え、没入感を削いでしまいます。

ゲームが面白いというのが一番大事な事ですが、どれだけ面白くても画面が何度もカクついてしまうゲームはプレイヤーも遊びたくなくなります。

 

処理をメインスレッドの外に逃がす

各種ハードウェアは別々の処理を同時に実行する仕組みを持っています。

1つの処理の実行単位のことをスレッドと呼びますが、ゲームで画面描画処理を実行するスレッドをメインスレッドと呼びます。

メインスレッドはリフレッシュレートに合わせて60秒に1回ゲームループを実行します。

先程、「画面の描画更新が間に合わなくなったら」と書きましたが、これは「メインスレッドの処理が間に合わなくなったら」に言い換えることができます。

メインスレッドが止まらないようにすることは良質なゲーム体験に繋がります。

 

ではどうしても重い処理を行いたい場合はどうすればいいのでしょうか。

その場合は処理をメインスレッドでやらなければいいのです。

スレッドはあくまで一つの処理の実行単位であり、ハードウェアが許す限りはメインスレッドとは別のスレッドを利用することが可能です。

ParallelTest02

このように処理の実行を呼び出すだけで、処理の完了を待たずに次の処理を始めることを 非同期処理 と言います。

ではこれを実際にUE4で行うにはどうすればよいのかをご説明します。

 

重い処理をメインスレッドで行った場合

ひとまず重くなりそうな処理を作ってみます。

 

 

特に意味もない処理ですが、これは重そうです。

これを以下の様にして普通にメインスレッドから実行するとどうなるでしょうか。

 

 

結果はコチラ。

ParallelTest05

 

こちらは「stat UnitGraph」コマンドで処理負荷を可視化したものですが、重い処理の実行タイミングでグラフに大きな変化が見られます。

今回は重い処理の実行タイミングで 53.7ms もの時間がかかってしまったようです。

60fps を目指すと1フレームに 16.6ms しか使えないので大きくオーバーしています。

もちろんこの処理を実行したタイミングでは画面が急に止まったように見えてしまいます。

これでは問題があるので、この重い処理を別スレッドで実行してみます。

 

重い処理をメインスレッド以外で行った場合

UE4には自前でスレッドを用意しなくても、必要になった時に手軽に別スレッドで処理を実行できる FAsyncTask という仕組みがあります。

FAsyncTask に実行したい処理(タスク)を指定しつつ実行リクエストを呼び出すことで、指定されたタスクが別スレッドで実行されます。

今回は FAsyncTask と同等の機能を持ちつつ、タスクの完了時に自動で削除される FAutoDeleteAsyncTask を使って重い処理を実行してみます。

 

 

非同期実行タスクを定義したので、これをメインスレッドから呼び出してみます。

 

 

結果はコチラ。

ParallelTest06

 

同じ重い処理を実行したにも関わらず、全く処理落ちが発生していません。

このように重くなって処理落ちを発生させてしまう可能性がある処理に関しては、別スレッドで行った方が良いです。

 

注意事項

これだけ聞くととても使いやすいように思えますが、複数のスレッドを利用するプログラム(マルチスレッドプログラム)は注意深く利用する必要があります。

例えば複数のスレッドから同時に同じリソース(とあるクラスのメンバ変数など)にアクセスした場合、競合が発生して意図しない動作となってしまう可能性があります。

そのため特に何も考えずに処理を組んでしまうとクラッシュの原因になったりもします。(例えば別スレッドでアクターのTransform情報を書き換えようものなら高頻度でクラッシュします)

このあたりは排他処理を組むことでできることが増えるのですが、その辺りはまた別の機会でご紹介しようと思います。