MonoBehaviourコルーチンの導出とLuaでのその使用

ユニティコルーチン非同期操作の微分使用とLuaでのその使用

1.一般的に使用されるコルーチンの例

多くの場合、私たちはモノビヘイビアのstartcoroutineを使用してコルーチンを開始します。これは、Unityで使用する最も一般的な直感的な理解です。このコルーチンで、ファイルのダウンロード、ファイルのロードなど、いくつかの非同期操作を実行します。これらの操作が完了したら、コールバックを実行します。例えば:

public static void Download(System.Action finishCB)
{
    
    
      string url = "https: xxxx";
      StartCoroutine(DownloadFile(url));
}
 
private static IEnumerator DownloadFile(string url)
{
    
    
     UnityWebRequest request = UnityWebRequest.Get(url);
     request.timeout = 10;
     yield return request.SendWebRequest();
     if(request.error != null)     
     {
    
    
                Debug.LogErrorFormat("加载出错: {0}, url is: {1}", request.error, url);
                request.Dispose();
                yield break;
      }
      
      if(request.isDone)
      {
    
    
            string path = "xxxxx";
            File.WriteAllBytes(path, request.downloadHandler.data);
            request.Dispose();
            yiled break;
      }
}

この例では、いくつかのキーワードが使用されています。IEnumerator/ yield return xxx / yield break / StartCoroutine次に、これらのキーワードから始めて、このようなダウンロード操作の特定の実装を理解します。

1.キーワードIEnumerator

このキーワードはUnityに固有のものではなく、Unityはc#に由来するため、理解するためにc#の例を見つける方が適切です。まず、IEnumeratorの定義を見てください。

public interface IEnumerator
{
    
    
     bool MoveNext();
     void Reset();
     Object Current{
    
    get;}
}

イテレータには、Current / MoveNext / Resetの3つの基本操作があることが定義から理解できます。ここでは、その操作プロセスについて簡単に説明します。一般的なコレクションでは、foreachなどの列挙操作を使用する場合、最初に、列挙子はコレクションの最初の要素の前に設定され、リセット操作は列挙子をこの位置に戻すことです。

イテレータが反復を実行すると、最初にMoveNextが実行されます。trueが返された場合は、次の位置にオブジェクトがあることを意味し、この時点でCurrentを次のオブジェクトに設定します。この時点で、Currentは次のオブジェクト。もちろん、c#がこのIEnumratorを実行用のオブジェクトの例にコンパイルする方法については、以下で説明します。

2.キーワードの利回り

C#のyieldキーワードの背後には2つの基本的な式があります。

Yield return
yiled break
Yield breakは、コルーチンからジャンプする操作です。これは通常、エラーが報告された場所やコルーチンを終了する必要がある場所で使用されます。

歩留まりリターンは、より頻繁に使用される式です。具体的な表現は、次の一般的な例です。

WWW:各フレームの終わりに呼び出される一般的なWeb操作は、isDone / isErrorをチェックし、trueの場合、MoveNext
WaitForSecondsを呼び出します:間隔が経過しているかどうかをチェックし、trueを返し、MoveNextを呼び出し
ますnull:直接MoveNextを呼び出します
WaitForEndOfFrame:レンダリング後に呼び出します、MoveNextを呼び出す

2.コルーチンへのc#呼び出しのコンパイル結果

ここでは、上記の例をコンパイルして生成する代わりに、前の記事の例を借用するだけです。

class Test
{
    
    
     static IEnumerator GetCounter()
     {
    
    
           for(int count = 0; count < 10; count++)
           {
    
    
                yiled return count;
           }
      }
}

コンパイラによって生成されたC ++の結果:

internal class Test 
{
    
     
    // GetCounter获得结果就是返回一个实例对象
    private static IEnumerator GetCounter() 
    {
    
     
        return new <GetCounter>d__0(0); 
    } 
   
    // Nested type automatically created by the compiler to implement the iterator 
    [CompilerGenerated] 
    private sealed class <GetCounter>d__0 : IEnumerator<object>, IEnumerator, IDisposable 
    {
    
     
        // Fields: there'll always be a "state" and "current", but the "count" 
        // comes from the local variable in our iterator block. 
        private int <>1__state; 
        private object <>2__current; 
        public int <count>5__1; 
       
        [DebuggerHidden] 
        public <GetCounter>d__0(int <>1__state) 
        {
    
     
           //初始状态设置
            this.<>1__state = <>1__state; 
        } 
   
        // Almost all of the real work happens here 
        //类似于一个状态机,通过这个状态的切换,可以将整个迭代器执行过程中的堆栈等环境信息共享和保存
        private bool MoveNext() 
        {
    
     
            switch (this.<>1__state) 
            {
    
     
                case 0: 
                    this.<>1__state = -1; 
                    this.<count>5__1 = 0; 
                    while (this.<count>5__1 < 10)        //这里针对循环处理 
                    {
    
     
                        this.<>2__current = this.<count>5__1; 
                        this.<>1__state = 1; 
                        return true; 
                    Label_004B: 
                        this.<>1__state = -1; 
                        this.<count>5__1++; 
                    } 
                    break; 
   
                case 1: 
                    goto Label_004B; 
            } 
            return false; 
        } 
   
        [DebuggerHidden] 
        void IEnumerator.Reset() 
        {
    
     
            throw new NotSupportedException(); 
        } 
   
        void IDisposable.Dispose() 
        {
    
     
        } 
   
        object IEnumerator<object>.Current 
        {
    
     
            [DebuggerHidden] 
            get 
            {
    
     
                return this.<>2__current; 
            } 
        } 
   
        object IEnumerator.Current 
        {
    
     
            [DebuggerHidden] 
            get 
            {
    
     
                return this.<>2__current; 
            } 
        } 
    } 
}

コードはより直感的で、関連するコメントも少し書かれているので、コルーチンを実行するとき、本質はイテレーターのインスタンスを返すことです。その後、メインスレッドで、更新のたびにこのインスタンスが更新されます。 MoveNext操作を実行できる場合(ファイルのダウンロードが完了した場合など)、MoveNextを1回実行し、次のオブジェクトをCurrentに割り当てます(MoveNextはtrueを返す必要があり、falseの場合、反復が完了したことを意味します) 。

ここから、コルーチンは非同期ではなく、その本質は引き続きUnityのメインスレッドで実行され、更新のたびにMoveNextを実行するかどうかがトリガーされるという結論を得ることができます。

第三に、コルーチンの派生的な使用

IEnumeratorはこのように使用できるため、MoveNextとCurrentのみを使用して、実際に簡単なテストコルーチンの例を記述できます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;
 
public class QuotaCoroutine : MonoBehaviour
{
    
    
    // 每帧的额度时间,全局共享
    static float frameQuotaSec = 0.001f;
 
    static LinkedList<IEnumerator> s_tasks = new LinkedList<IEnumerator>();
 
    // Use this for initialization
    void Start()
    {
    
    
        StartQuotaCoroutine(Task(1, 100));
    }
 
    // Update is called once per frame
    void Update()
    {
    
    
        ScheduleTask();
    }
 
    void StartQuotaCoroutine(IEnumerator task)
    {
    
    
        s_tasks.AddLast(task);
    }
 
    static void ScheduleTask()
    {
    
    
        float timeStart = Time.realtimeSinceStartup;
        while (s_tasks.Count > 0)
        {
    
    
            var t = s_tasks.First.Value;
            bool taskFinish = false;
            while (Time.realtimeSinceStartup - timeStart < frameQuotaSec)
            {
    
    
                // 执行任务的一步, 后续没步骤就是任务完成
                Profiler.BeginSample(string.Format("QuotaTaskStep, f:{0}", Time.frameCount));
                taskFinish = !t.MoveNext();
                Profiler.EndSample();
 
                if (taskFinish)
                {
    
    
                    s_tasks.RemoveFirst();
                    break;
                }
            }
 
            // 任务没结束执行到这里就是没时间额度了
            if (!taskFinish)
                return;
        }
    }
 
    IEnumerator Task(int taskId, int stepCount)
    {
    
    
        int i = 0;
        while (i < stepCount)
        {
    
    
            Debug.LogFormat("{0}.{1}, frame:{2}", taskId, i, Time.frameCount);
            i++;
            yield return null;
        }
    }
}

アイデアについて話しましょう。最初に、IEnueratorインスタンスを作成してリンクリストに詰め込み、その後の各フレーム更新で、このインスタンスを取り出し、MoveNextを1回実行し、すべての実行が完了した後にこのインスタンスを削除します。表示せずにStartCoroutineを呼び出すと、同様にMoveNextの実行をトリガーできます。

結果を見てください:
ここに写真の説明を挿入

実行可能。OK、ここに統一に関するコルーチンを記述し、次にxluaでのコルーチンの実現について見ていきます。

4、Luaのコルーチン

Luaのコルーチンとユニティコルーチンの違いは、プリエンプティブ実行ではないことです。つまり、MoveNextのような操作はアクティブに実行されませんが、前の例と同じように、アクティブに実行を刺激する必要があります。同じように、操作にチェックマークを付けます。あなた自身。

Luaのコルーチンの3つの主要なAPI:

coroutine.create()/ wrap:コルーチンをビルドし、ラップビルドの結果は関数であり、createはスレッドタイプのオブジェクトです

coroutine.resume():MoveNextと同様の操作を実行します

coroutine.yield():コルーチンを一時停止します

比較的単純で、テストする例を書くことができます。

local func = function(a, b)
    for i= 1, 5 do
        print(i, a, b)
    end
end
 
local func1 = function(a, b)
    for i = 1, 5 do
        print(i, a, b)
        coroutine.yield()
    end
end
 
 
co =  coroutine.create(func)
coroutine.resume(co, 1, 2)
--此时会输出 112/ 212/ 312/412/512
 
co1 = coroutine.create(func1)
coroutine.resume(co1, 1, 2)
--此时会输出 112 然后挂起
coroutine.resume(co1, 3, 4)
--此时将上次挂起的协程恢复执行一次,输出: 2, 1, 2 所以新传入的参数34是无效的

xluaによってオープンソース化されたutilでコルーチンを使用する方法と、それをluaのコルーチンと組み合わせる方法を見てみましょう。c#側もこのインスタンスを取得して追加できるように、lua側でコルーチンを構築します。ユニティ側のメインスレッド更新をトリガーします。

次のAPIを見てください。

local util = require 'xlua.util'

local gameobject = CS.UnityEngine.GameObject('Coroutine_Runner')
CS.UnityEngine.Object.DontDestroyOnLoad(gameobject)
local cs_coroutine_runner = gameobject:AddComponent(typeof(CS.Coroutine_Runner))

return {
    
    
    start = function(...)
        return cs_coroutine_runner:StartCoroutine(util.cs_generator(...))
    end;

    stop = function(coroutine)
        cs_coroutine_runner:StopCoroutine(coroutine)
    end
}

start操作の本質は、関数を1つのレイヤーにラップし、util.csgeneratorを呼び出し、utilでのcs_generatorの実装を詳しく調べることです。

local move_end = {
    
    }

local generator_mt = {
    
    
    __index = {
    
    
        MoveNext = function(self)
            self.Current = self.co()
            if self.Current == move_end then
                self.Current = nil
                return false
            else
                return true
            end
        end;
        Reset = function(self)
            self.co = coroutine.wrap(self.w_func)
        end
    }
}

local function cs_generator(func, ...)
    local params = {
    
    ...}
    local generator = setmetatable({
    
    
        w_func = function()
            func(unpack(params))
            return move_end
        end
    }, generator_mt)
    generator:Reset()
    return generator
end

コードは短いですが、アイデアは非常に明確です。まず、キーが関数に対応するテーブルを作成し、次に_indexメソッドを変更して、MoveNext関数の実装との実装を含むメタテーブルを削除します。リセット関数ですが、ここでリセットはIEnumeratorとは異なります。ここでは、coroutine.wrapを呼び出してコルーチンを生成します。このように、c#側がこのジェネレーターのhandleIDを取得した後、MoveNextは後続のフレーム更新ごとに実行されます。すべてが実行されると、この時点でmove_endが返され、コルーチンが実行されたことを示します。c#にfalseを返します。コルーチンのhandleIDをクリアする側。

おすすめ

転載: blog.csdn.net/qq_43505432/article/details/109744685