狠狠撸

狠狠撸Share a Scribd company logo
ロードオブナイツで使ってる
Unity非同期処理デザイン




          ++C++; 岩永信之 
              2012/10/12
イテレーター構文とUnityのコルーチン

コルーチン
ゲーム ループ
●   ゲームでは、どこか大元でループが回ってる
     while (isAlive)
     {
         // 固定?FPS?なら、所定の時間が来るまで?Sleep

         gameTime = … // ゲーム時間を進める

         foreach (var obj in gameObjects)
         {
             obj.Update (gameTime);
         }
     }
            1フレームに1回よばれる処理
重たい処理
●   フレームレートよりも時間がかかる処理をし
    ちゃダメ
    ダメな例
    string Load(string path)
    {
        // 30ミリ秒くらいかかるものとする
        var data = ファイルからバイナリロード(path);

    ????// これも30ミリ秒くらいかかるものとする
          return デシリアライズ(data);

    ????// 30 FPSだと、このメソッドは33ミリ秒以内に終えないと処理落ち
    }
                     2回に分けたい
そこで、イテレーター
●   イテレーターをコルーチンとして使う
ゲーム ループ中で、毎フレーム
 MoveNextを呼んでもらう
    IEnumerator Load(string path, Action<string> callback)
    {
          var data = ファイルからバイナリロード(path);        1フレーム目
    ????yield return null;

          callback(デシリアライズ(data));             2フレーム目
    ????yield return null;
    }
                         returnの代わりに
                        コールバック呼び出し
before/after (1)
●   メソッド宣言
    before
    string Load(string path)

    after
    IEnumerator Load(string path, Action<string> callback)

     ●   戻り値はIEnumerator固定
     ●   本来の戻り値はcallbackごしに返す
before/after (2)
●   return
    before
     return result;
    after
     callback(result);

     ●   returnステートメントの代わりにcallback呼び出し
before/after (3)
●    呼び出し側
    before
    var x = Load("path"); …(後続の処理)

    after
    StartCoroutine(Load("path", x => { …後続の処理 });

     ●      戻り値を直接受け取れない
            ●   形式上の戻り値(IEnumerator)はコルーチン起動のた
                めに使う
     ●      匿名関数を使って後続の処理をつなぐ
.NET Framework 4のTaskクラスを参考に、Unityコルーチンをラッピング

TASKクラス
問題
●   複数のコルーチンを扱いにくい
     StartCoroutine(A);
     StartCoroutine(B);


        ●   A, B両方が完了するのを待ちたいときはどうする?
        ●   Aの完了後にBを開始したいときはどうする?
             ● 特に、Aの(本来の)戻り値をBで使いたいときは?
             ● エラーの伝播を考えるとさらに面倒
Taskクラス
●   こういうクラスを用意
        public class Task<T> : IEnumerator
        {
            IEnumerator Routine;
            public T Result { get; }
            public Exception Error { get; }
            public void OnComplete(… callback);
            public Task<T> ContinueWith<U>(… continuation);
        }


    ●   継続処理の登録
    ●   (本来の)戻り値やエラーの伝播
実例

●   ロードオブナイツのマップ
    ●   64万要素程度の配列をJSONで受け取ってた
         ● 通信もコルーチン
            ●   通信エラーが発生する可能性あり
        ●   デコードもそこそこ高負荷なのでコルーチン化したい
            ●   (行単位でデコード、1フレームに1行ずつとか)
            ●   通信の結果を使う
            ●   デコード エラーが発生する可能性あり
        ●   ローカル ストレージにキャッシュしたい
            ●   IOエラーが発生する可能性あり

※ 最新バージョンではデータを小分けで受信するように改善され、デコード
処理はコルーチンではなくなっている
Task利用例(戻り値)
●   (本来の)戻り値の伝播
     IEnumerator A(Action<int> callback);
     IEnumerator B(int x, Action<string> callback);
     IEnumerator C(string s);

                A, B, C の順で実行したい
                A の戻り値(int)を B で、B の戻り値(string)を C で使いたい
                同期処理なら C(B(A()); だけで書けるもの

     var t = new Task<int>(A)
         .ContinueWith<string>(B)
         .ContinueWith(C);

     StartCoroutine(t);
同期処理と比べて
●   同期処理との対比
     IEnumerator A(Action<int> callback);
     IEnumerator B(int x, Action<string> callback);
     IEnumerator C(string s);

                                          int A();
                                          string B(int x);
                                          void C(string s);

     var t = new Task<int>(A)             var x = A();
         .ContinueWith<string>(B)         var s = B(x);
         .ContinueWith(C);                C(s);

     StartCoroutine(t);                      あるいは
                                            C(B(A()));
Task利用例(例外処理)
●   コルーチン内で発生した例外も受け取れる
    var t = new Task<int>(A)
        .ContinueWith<string>(B)
        .ContinueWith(C)
        .OnComplete(t =>             ●   A, B, C のどこかで例外が発
        {                                生した場合、そこでコルーチン
            if (t.Error != null) …       の実行は中断。
        });                          ●   発生した例外はErrorプロパ
                                         ティにセットされる
    StartCoroutine(t);
以上

More Related Content

Async design with Unity3D

  • 3. ゲーム ループ ● ゲームでは、どこか大元でループが回ってる while (isAlive) { // 固定?FPS?なら、所定の時間が来るまで?Sleep gameTime = … // ゲーム時間を進める foreach (var obj in gameObjects) { obj.Update (gameTime); } } 1フレームに1回よばれる処理
  • 4. 重たい処理 ● フレームレートよりも時間がかかる処理をし ちゃダメ ダメな例 string Load(string path) { // 30ミリ秒くらいかかるものとする var data = ファイルからバイナリロード(path); ????// これも30ミリ秒くらいかかるものとする return デシリアライズ(data); ????// 30 FPSだと、このメソッドは33ミリ秒以内に終えないと処理落ち } 2回に分けたい
  • 5. そこで、イテレーター ● イテレーターをコルーチンとして使う ゲーム ループ中で、毎フレーム MoveNextを呼んでもらう IEnumerator Load(string path, Action<string> callback) { var data = ファイルからバイナリロード(path); 1フレーム目 ????yield return null; callback(デシリアライズ(data)); 2フレーム目 ????yield return null; } returnの代わりに コールバック呼び出し
  • 6. before/after (1) ● メソッド宣言 before string Load(string path) after IEnumerator Load(string path, Action<string> callback) ● 戻り値はIEnumerator固定 ● 本来の戻り値はcallbackごしに返す
  • 7. before/after (2) ● return before return result; after callback(result); ● returnステートメントの代わりにcallback呼び出し
  • 8. before/after (3) ● 呼び出し側 before var x = Load("path"); …(後続の処理) after StartCoroutine(Load("path", x => { …後続の処理 }); ● 戻り値を直接受け取れない ● 形式上の戻り値(IEnumerator)はコルーチン起動のた めに使う ● 匿名関数を使って後続の処理をつなぐ
  • 10. 問題 ● 複数のコルーチンを扱いにくい StartCoroutine(A); StartCoroutine(B); ● A, B両方が完了するのを待ちたいときはどうする? ● Aの完了後にBを開始したいときはどうする? ● 特に、Aの(本来の)戻り値をBで使いたいときは? ● エラーの伝播を考えるとさらに面倒
  • 11. Taskクラス ● こういうクラスを用意 public class Task<T> : IEnumerator { IEnumerator Routine; public T Result { get; } public Exception Error { get; } public void OnComplete(… callback); public Task<T> ContinueWith<U>(… continuation); } ● 継続処理の登録 ● (本来の)戻り値やエラーの伝播
  • 12. 実例 ● ロードオブナイツのマップ ● 64万要素程度の配列をJSONで受け取ってた ● 通信もコルーチン ● 通信エラーが発生する可能性あり ● デコードもそこそこ高負荷なのでコルーチン化したい ● (行単位でデコード、1フレームに1行ずつとか) ● 通信の結果を使う ● デコード エラーが発生する可能性あり ● ローカル ストレージにキャッシュしたい ● IOエラーが発生する可能性あり ※ 最新バージョンではデータを小分けで受信するように改善され、デコード 処理はコルーチンではなくなっている
  • 13. Task利用例(戻り値) ● (本来の)戻り値の伝播 IEnumerator A(Action<int> callback); IEnumerator B(int x, Action<string> callback); IEnumerator C(string s); A, B, C の順で実行したい A の戻り値(int)を B で、B の戻り値(string)を C で使いたい 同期処理なら C(B(A()); だけで書けるもの var t = new Task<int>(A) .ContinueWith<string>(B) .ContinueWith(C); StartCoroutine(t);
  • 14. 同期処理と比べて ● 同期処理との対比 IEnumerator A(Action<int> callback); IEnumerator B(int x, Action<string> callback); IEnumerator C(string s); int A(); string B(int x); void C(string s); var t = new Task<int>(A) var x = A(); .ContinueWith<string>(B) var s = B(x); .ContinueWith(C); C(s); StartCoroutine(t); あるいは C(B(A()));
  • 15. Task利用例(例外処理) ● コルーチン内で発生した例外も受け取れる var t = new Task<int>(A) .ContinueWith<string>(B) .ContinueWith(C) .OnComplete(t => ● A, B, C のどこかで例外が発 { 生した場合、そこでコルーチン if (t.Error != null) … の実行は中断。 }); ● 発生した例外はErrorプロパ ティにセットされる StartCoroutine(t);