simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

Unity:別スレッドで重い処理(AI処理とか、画像処理とか)

Unity でビジュアライズしている何かしらのアルゴリズム(AIとか画像処理とか)について、処理中にUIが固まることはユーザービリティの面から看過できません。

Unity 2017 になってから、C# 5.0 からの新機能 Task(async/await) が使えるようになりました。
これを使うだけです。

ただ使うには、PlayerSettingsをExperimental(実験的な設定)に変更する必要があります。私は次の記事を参考に変更しました。
qiita.com

具体的なコードとしては

await Task.Run 以降のラムダ式をメインスレッドとは別のスレッドにて非同期実行させることができます。
await キーワードは async キーワードを付けた関数の中でなければ、記述できません。

ということで、以下のようなコードで重い処理を別スレッドで書き、結果が出たらスレッドアンセーフでゲームオブジェクトへ適用することができるようになります。

using System.Collections;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class ThreadTaskTestBehaviour : MonoBehaviour
{
    void Start()
    {
        this.MoveAsync();
    }

    private async void MoveAsync()
    {
        var context = SynchronizationContext.Current;
        await Task.Run(() =>
        {
            for (int i = 0; i < 10000; i++)
            {
                if (9999 == i)
                {
                    Vector3 position = Vector3.zero;
                    Debug.Log("i = " + i);
                    context.Post((state) =>
                    {
                        position = this.transform.localPosition;
                        Debug.Log("position.x = " + position.x);
                        position.x -= i * 0.01f;
                        this.transform.localPosition = position;
                        Debug.Log("in post position.x = " + position.x);
                    }, null);
                    Debug.Log("out of post position.x = " + position.x);
                }
            }
        });
    }
}

Action を切り離して書くと以下のような感じです

using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class ThreadTaskTestBehaviour : MonoBehaviour
{
    private static SynchronizationContext _context;

    void Start()
    {
        _MoveAsync();
    }

    void Update()
    {
        Debug.Log("Update");
    }

    private async void _MoveAsync()
    {
        _context = SynchronizationContext.Current;
        await Task.Run(_Action);
    }

    private Action _Action = () =>
    {
        const int loopMax = 10000;
        for (int i = 0; i < loopMax; i++)
        {
            Debug.Log("Action i = " + i);
            if (loopMax == i + 1)
            {
                _context.Post((state) =>
                {
                    Debug.Log("in main thread");
                }, null);
            }
        }
        Debug.Log("finish");
    };
}

別に await 付けなくても、Task.Run 以降のラムダ式を別スレッドとして実行できます。
await するからには実行完了を待つわけで、ゆえに async 関数の中でないと宣言できないというわけでした。
併せて読みたいのは引数をスレッドごとに変えて渡す方法です。
simplestar-tech.hatenablog.com

また、次の私の記事でUniRx を勉強していて見つけたのですが
simplestar-tech.hatenablog.com

UniRx を導入すると次のコードで別スレッド処理を実行できました。

Observable.Start(() => {
// ここに別スレッドで行う処理
})
.ObserveOnMainThread()
.Subscribe(_ => { });

しかし、2018年中ごろには、Unityが並列処理を行う C# Jobs System を入れてくるらしいので、いずれはそれを使うことになるかもですね。

www.slideshare.net
しかし、単一の重い処理をバックグラウンドで実行する処理は、ひとまず今回の記事のやり方で対応できます。

以下、参考にした記事
Unityでのマルチスレッド処理はみなさんの関心事なんだなって思います。

techblog.kayac.com
tyheeeee.hateblo.jp
satoshi-maemoto.hatenablog.com
qiita.com
qiita.com