僕がUnityでコードを書く時はUnityっぽい書き方に寄せたいな、と思っています。非同期処理を書こうとすると毎回うまくできないのでまとめてみます。
memo
ややこしい処理をすれば実現できるのですが、できるだけややこしい処理をせずに実現してみます。UniRxも存在しない世界です。
はじめに非同期処理と並行処理
yieldやasyncやスレッドを使ってると、いろいろな非同期が発生します。いろいろ端折ってますが以下のような感じです。
yield
IEnumerable Start() { yield return Run(); } IEnumerator Run(string url) { for (int i = 0; i < 1000000; i++) { // なにか処理 ... if (i % 1000 == 999) { yield return null; } } }
メインスレッドのまま処理を数フレームに分けて処理落ちを軽減する非同期。
async(Task.Runなし)
async void Start() { await Run(); } async Task Run() { for (int i = 0; i < 1000000; i++) { // なにか処理 ... if (i % 1000 == 999) { await Task.Yield(); } } }
メインスレッドのまま処理を数フレームに分けて処理落ちを軽減する非同期。awaitの先は実装次第で非同期だったり並行だったり別スレッドだったりします。
async(Task.Runあり)
async void Start() { await Task.Run(() => Run()); } void Run() { for (int i = 0; i < 1000000; i++) { // なにか処理 ... } }
別スレッドなのでメインスレッドの処理落ちを考えずぶん回す非同期。
目指したい書き方
Resources.LoadAsync
っぽい書き方です。
public class AsyncSample : MonoBehaviour { [SerializeField] RawImage m_Photo; IEnumerator Start() { var request = Resources.LoadAsync<Texture2D>("cat"); yield return request; m_Photo.texture = request.asset as Texture2D; } }
今回はフルHDの画像をCPUで反転する処理を非同期にしたい、という内容で進めます。
void Run(Texture2D tex) { var result = new Texture2D(tex.width, tex.height, tex.format, false); var cols = tex.GetPixels(); for (int y = 0; y < tex.height; y++) { for (int x = 0; x < tex.width; x++) { var idx = x + y * tex.width; var col = cols[idx]; cols[idx] = new Color(1f - col.r, 1f - col.g, 1f - col.b, col.a); } } result.SetPixels(cols); result.Apply(); }
目指す書き方
public class AsyncSample : MonoBehaviour { [SerializeField] RawImage m_Photo; IEnumerator Start() { var request = TextureUtility.InvertAsync((Texture2D)m_Photo.texture); yield return request; m_Photo.texture = request.result; } }
Resources.LoadAsyncの中身
var request = Resources.LoadAsync<Texture2D>("cat");
この時点で非同期処理が動いています。request
はAsyncOperationを継承したオブジェクトで、結果の保持と処理がおわったかどうかを保持しています。呼び出した時点で非同期処理が動き出すのがポイントです。
yield return request;
ここは処理がおわるのを待っているだけでStartCoroutine
しているわけではなく、この記述がなくても裏では非同期処理が進行しています。
AsyncOperationを実装する
public abstract class MyAsyncOperation<T> : CustomYieldInstruction { protected T m_Result; public T result => m_Result; public override bool keepWaiting => m_Result == null; // publicになってしまうのは気持ちわるい public void Complete(T resule) { m_Result = resule; } }
CustomYieldInstruction
を使ってそれっぽいものを作りました。
public class TextureConvertRequest : MyAsyncOperation<Texture2D> { }
結果として画像を加工した結果を返すMyAsyncOperation
です。
Coroutineを使ったパターン
async / await
が使えなかった頃は別スレッドに逃すかCoroutineを使っていました。
public static class TextureUtility { public static TextureConvertRequest InvertAsync(Texture2D tex, MonoBehaviour mono) { var req = new TextureConvertRequest(); mono.StartCoroutine(Run(tex, req)); return req; } static IEnumerator Run(Texture2D tex, TextureConvertRequest req) { var result = new Texture2D(tex.width, tex.height); var cols = tex.GetPixels(); for (int x = 0; x < tex.width; x++) { for (int y = 0; y < tex.height; y++) { var idx = x + y * tex.width; var col = cols[idx]; cols[idx] = new Color(1f - col.r, 1f - col.g, 1f - col.b, col.a); } // 一列ごとに処理を戻す yield return null; } result.SetPixels(cols); result.Apply(); req.Complete(result); } }
この場合は「目指す書き方」は不可能です。誰かがStartCoroutineを呼ばなければいけないので、MonoBehaviourを渡したりグローバルなコルーチンを回すだけの存在を作ったりしていました。
note
MoveNext()
とかを駆使すればできるかも。
asyncを使ったパターン
public static class TextureUtility { public static TextureConvertRequest InvertAsync(Texture2D tex) { var req = new TextureConvertRequest(); // なげっぱなし var _ = Run(tex, req); return req; } static async Task Run(Texture2D tex, TextureConvertRequest req) { var result = new Texture2D(tex.width, tex.height); var cols = tex.GetPixels(); for (int x = 0; x < tex.width; x++) { for (int y = 0; y < tex.height; y++) { var idx = x + y * tex.width; var col = cols[idx]; cols[idx] = new Color(1f - col.r, 1f - col.g, 1f - col.b, col.a); } // 一列ごとに処理を戻す await Task.Yield(); } result.SetPixels(cols); result.Apply(); req.Complete(result); } }
asyncが使えるようになったのでStartCoroutineなしに非同期処理を回せます。「目指す書き方」が出来ました。
スレッドを使ったパターン
public static class TextureUtility { public static TextureConvertRequest InvertAsync(Texture2D tex) { var req = new TextureConvertRequest(); // なげっぱなし var _ = Run(tex, req); return req; } static async Task Run(Texture2D tex, TextureConvertRequest req) { var result = new Texture2D(tex.width, tex.height); // メインスレッド以外ではUnityのAPIが使えないのでここで取得する var cols = tex.GetPixels(); var width = tex.width; var height = tex.height; try { await Task.Run(() => _Run(cols, width, height)); } catch (System.Exception e) { Debug.LogException(e); throw e; } result.SetPixels(cols); result.Apply(); req.Complete(result); } static void _Run(Color[] cols, int width, int height) { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { var idx = x + y * width; var col = cols[idx]; cols[idx] = new Color(1f - col.r, 1f - col.g, 1f - col.b, col.a); } } } }
メインスレッドの処理をほとんど止めることなく実現できました。
おまけ
以下のクラスを実装しておくとコルーチン
もasync / await
も同居できると思います。UnityWebRequest
をawaiしたい場面や、プラグインがasync
を使っていてyield return
したい時に使えると思います。
AsyncOperationをawaitする
拡張メソッドでGetAwaiter
を定義することでawaitできるようになります。
public static class AsyncOperationExtension { public static TaskAwaiter<bool> GetAwaiter(this AsyncOperation ope) { var source = new TaskCompletionSource<bool>(); ope.completed += (_) => { source.TrySetResult(true); }; return source.Task.GetAwaiter(); } }
public class AsyncSample : MonoBehaviour { [SerializeField] RawImage m_Photo; private async void Start() { var request = Resources.LoadAsync<Texture2D>("cat"); await request; m_Photo.texture = request.asset as Texture2D; } }
Taskをyield returnする
class WaitForTaskCompletion : CustomYieldInstruction { Task m_Task; public override bool keepWaiting => !m_Task.IsCompleted; public WaitForTaskCompletion(Task task) { m_Task = task; } }
これはfirebaseのunityプラグインでも使われていました。
public class AsyncSample : MonoBehaviour { [SerializeField] RawImage m_Photo; private IEnumerator Start() { var task = TextureUtility.InvertAsyncByTask(); yield return new WaitForTaskCompletion(task); m_Photo.texture = task.Result; } }