Simulationsimplementierung benutzerdefinierter Coroutine-Anweisungen und Coroutinen in Unity

In diesem Artikel wird die Simulationsimplementierung benutzerdefinierter Coroutine-Anweisungen und Coroutinen in Unity beschrieben

Im letzten Artikel haben wir kurz die grundlegenden Konzepte und die Verwendung von Coroutinen in Unity und Lua erläutert und einige Vergleiche zwischen den beiden angestellt.

In diesem Artikel werden wir die Implementierung von Coroutinen durch Unity weiter untersuchen und erraten und simulieren, wie Unity Coroutinen durch Anpassen von Coroutinen implementiert.

Benutzerdefinierte Coroutine-Anweisungen in Unity

Anweisungen wie WaitForSeconds und WaitForEndOfFrame, die von Unity standardmäßig bereitgestellt werden, erben von YieldInstruction .

Bietet außerdem einige flexible Möglichkeiten zum Definieren benutzerdefinierter Anweisungen.

Das offizielle Dokument bietet zwei Möglichkeiten zum Implementieren benutzerdefinierter Befehle, die im Folgenden einzeln vorgestellt werden.

Durch Erben der CustomYieldInstruction-Klasse

Unity stellt die CustomYieldInstruction- Klasse bereit, die wir erben können, um unsere eigenen Coroutine-Anweisungen zu implementieren, z. B. Wartezeiten, z. B. das Erfüllen bestimmter Bedingungen usw.

Der Kernpunkt der benutzerdefinierten Anweisung besteht darin, die keepWaiting- Eigenschaft der CustomYieldInstruction- Klasse neu zu schreiben, die verwendet wird, um Unity die Möglichkeit zu geben, festzustellen, ob die Anweisung beendet ist.

Unity fragt in jedem Frame zwischen Update und LateUpdate nach der Anweisung für diese Eigenschaft.

Hier ist Beispiel 1:

class WaitWhile : CustomYieldInstruction
{
    Func<bool> m_Predicate;

    public override bool keepWaiting { get { return m_Predicate(); } }

    public WaitWhile(Func<bool> predicate) { m_Predicate = predicate; }
}

public class ExampleScript : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(waitForSomething());
    }

	// 打印start之后, 直到鼠标右键按下才会继续执行
    public IEnumerator waitForSomething()
    {
        Debug.Log("start");
        yield return new WaitWhile() {()=> return Input.GetMouseButtonDown(1);}
        Debug.Log("Right mouse button pressed");
    }
}

Hier ist Beispiel 2:

public class WaitForMouseDown : CustomYieldInstruction
{
    public override bool keepWaiting
    {
        get
        {
            return !Input.GetMouseButtonDown(1);
        }
    }

    public WaitForMouseDown()
    {
        Debug.Log("Waiting for Mouse right button down");
    }
}

public class ExampleScript : MonoBehaviour
{
    void Update()
    {
        if (Input.GetMouseButtonUp(0))
        {
            Debug.Log("Left mouse button up");
            StartCoroutine(waitForMouseDown());
        }
    }

    // 打印Update之后, 直到鼠标右键按下才会继续执行
    public IEnumerator waitForMouseDown()
    {
        Debug.Log("Update");
        yield return new WaitForMouseDown();
        Debug.Log("Right mouse button pressed");
    }
}

Implementieren Sie benutzerdefinierte Anweisungen, indem Sie die IEnumerator-Schnittstelle implementieren und einen Iterator erstellen

Unity implementiert Coroutinen über Iteratoren und iterierbare Objekte, sodass wir auch Iteratoren erstellen können, um den gewünschten Effekt zu erzielen.

Hier ist ein Beispiel:

class WaitWhile : IEnumerator
{
    Func<bool> m_Predicate;

    public object Current { get { return null; } }

    public bool MoveNext() { return m_Predicate(); }

    public void Reset() {}

    public WaitWhile(Func<bool> predicate) { m_Predicate = predicate; }
}

public class ExampleScript : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(waitForSomething());
    }

	// 打印start之后, 直到鼠标右键按下才会继续执行
    public IEnumerator waitForSomething()
    {
        Debug.Log("start");
        yield return new WaitWhile() {()=> return Input.GetMouseButtonDown(1);}
        Debug.Log("Right mouse button pressed");
    }
}

Es ist ersichtlich, dass diese Methode Beispiel 1 sehr ähnlich ist, mit der Ausnahme, dass keepWaiting durch MoveNext ersetzt wird und die Standardimplementierung der Schnittstelle bereitgestellt wird.

Die oben beschriebene Verwendung ist sehr einfach und es gibt nichts mehr zu sagen.

Auf den folgenden Seiten werden wir versuchen, von Unity zu lernen, Coroutinen selbst zu simulieren und zu implementieren.

Simulieren Sie die Implementierung von Unity-Coroutinen

Unsere Simulation umfasst wahrscheinlich mehrere Klassen:

  • MonoBehavior, das Skript, in dem wir normalerweise Code schreiben, ist der Ausgangspunkt der Coroutine.

  • YieldInstruction, eine Anweisungsklasse, ist eine iterierbare Klasse und implementiert die IEnumerable-Schnittstelle.

  • WaitForFrames, eine von YieldInstruction geerbte Anweisungsklasse, die zum Warten auf eine bestimmte Anzahl von Frames verwendet wird

  • Coroutine, eine Coroutine-Klasse, erbt von YieldInstruction und stellt eine spezifische Implementierung bereit.

  • CustomYieldInstruction, eine benutzerdefinierte Anweisungsklasse, implementiert die IEnumerator-Schnittstelle und bietet benutzerdefiniertes Verhalten

  • WaitWhile, eine benutzerdefinierte Anweisungsklasse, geerbt von CustomYieldInstruction, die für bedingtes Warten verwendet wird

Coroutine

Schauen wir uns zunächst die Coroutine-Klasse an.

Man kann sagen, dass die Coroutine-Klasse der Kern der gesamten Implementierung ist (nonsense_ ) . Sie stellt an verschiedenen Stellen eine Verbindung zwischen dem Vorhergehenden und dem Folgenden her.

Die Coroutine-Klasse erbt von YieldInstruction und ist selbst ein Iterator. Bei jeder Iteration wird der Code einmal vorangetrieben, was zur nächsten Ausbeute oder bis zum Ende des Codes oder einmal ausgeführt werden kann.

Eigenschaften von Coroutinen

// 路径, 这个就是我们写的协程方法, 即public IEnumerator Wait(), 每执行一次MoveNext就走到下一个yield或者结束
protected IEnumerator m_Routine;

// 是当前指令, 即yield return new CustomYieldInstruction
protected IEnumerator m_CurInstruction;

m_Routine verwaltet einen Iterator, der der Rückgabewert der Coroutine-Methode ist, die wir im Mono-Skript geschrieben haben.

Bei jedem MoveNext von m_Routine wird die Coroutine-Methode erneut ausgeführt, bis sie auf yield oder das Ende der Methode stößt.

Nachdem m_Routine MoveNext ausgeführt hat, zeigt sein Current auf einen neuen Iterator (falls vorhanden), der dem Code entspricht yield return new xxx. Dieser xxx ist der neue Iterator.

Wir müssen den Iterator, auf den Current zeigt, iterieren, um die Ausführung der Coroutine-Methode fortzusetzen.

Dieser Strom ist der Kern unserer Fähigkeit, Coroutinen zu implementieren.

Wir abstrahieren diesen Strom in eine Yield-Anweisung , das heißt, es kann eine YieldInstruction, eine Coroutine, eine CustomYieldInstruction oder sogar ein IEnumerator sein. Solange es die IEnumerator-Schnittstelle oder ein ähnliches Verhalten implementiert, kann es verwendet werden.

Daher definieren wir das zweite Attribut von Coroutine als aktuelle Anweisung .

Coroutine-Methode

public Coroutine(IEnumerator routine)
{
    m_Routine = routine;
}

public override bool MoveNext()
{
    if (m_CurInstruction != null)
    {
        // 调用CustomYieldInstruction结束
        if (!m_CurInstruction.MoveNext())
        {
            m_CurInstruction = null;
        }

        return true;
    }

    // 调用yield, 获取一个CustomYieldInstruction, 即yield return new CustomYieldInstruction
    // 如果返回值是false, 整个停止
    if (!m_Routine.MoveNext())
        return false;

    var instruction = m_Routine.Current as IEnumerator;

    // null, 暂停一帧
    if (instruction == null)
        return true;

    // 调用CustomYieldInstruction的下一步
    if (instruction.MoveNext())
    {
        m_CurInstruction = instruction;
    }

    return true;
}

Die erste ist die Konstruktionsmethode, die einen Iterator akzeptiert, der der Rückgabewert der von uns geschriebenen Coroutine-Methode ist.

Als nächstes folgt die Beschreibung des Kernalgorithmus:

  1. Die Coroutine iteriert einmal (MoveNext). Wenn eine aktuelle Anweisung vorhanden ist, springen Sie zum vierten Schritt, andernfalls springen Sie zum nächsten Schritt
  2. Der Pfad wird einmal iteriert (MoveNext). Wenn er false zurückgibt, bedeutet dies, dass die Ausführung der Coroutine-Methode abgeschlossen ist und die gesamte Coroutine-Operation beendet ist. Andernfalls springen Sie zum nächsten Schritt
  3. Holen Sie sich die aktuelle Anweisung über Current. Wenn sie null ist, unterbrechen Sie die Ausführung der Coroutine, warten Sie auf die nächste Pfaditeration, andernfalls springen Sie zum nächsten Schritt
  4. Die Anweisung iteriert einmal (MoveNext). Wenn sie false zurückgibt, bedeutet dies, dass die Anweisung ausgeführt wird. Anschließend wird der Betrieb der Coroutine angehalten und auf die nächste Pfaditeration gewartet

Die allgemeine Bedeutung besteht darin, Anweisungen gemäß der Iteration des Pfads zu erhalten und den Pfad nach Abschluss der Anweisung zu iterieren, bis die Pfaditeration abgeschlossen ist.

MonoBehavior, der Start, Stopp und Aufruf der Coroutine

Lassen Sie uns als Nächstes über den Start- und Aufrufprozess der Coroutine sprechen.

Im Mono-Skript wird eine Liste von Coroutinen verwaltet: List<Coroutine> m_DelayCallLst.

Die Coroutinen in der Liste werden zwischen jedem Update und LateUpdate iteriert.

Verwenden Sie StartCoroutine/StopCoroutine, um die Coroutine zu starten oder zu stoppen.

Hier ist der ungefähre Code:

public class MonoBehavior
{
	List<Coroutine> m_DelayCallLst = new List<Coroutine>();

	public MonoBehavior()
	{
		Start();
	}

	protected virtual void Start() { }
	protected virtual void Update() { }
	private void LateUpdate() { }
	private void DoDelayCall()
	{
		for (int i = m_DelayCallLst.Count - 1; i >= 0; i--)
		{
			var call = m_DelayCallLst[i];
			if (!call.MoveNext())
			{
				m_DelayCallLst.Remove(call);
			}
		}
	}

	public void MainLoop()
	{
		Update();
		DoDelayCall();
		LateUpdate();
	}

	public Coroutine StartCoroutine(IEnumerator routine)
	{
		var coroutine = new Coroutine(routine);
		m_DelayCallLst.Add(coroutine);

		return coroutine;
	}
    
    public void StopCoroutine(Coroutine coroutine)
	{
		m_DelayCallLst.Remove(coroutine);
	}
}

Rufen Sie die Update-Methode des Mono-Skripts für jeden Frame in der Hauptschleife auf. Hier legen wir jeden Frame auf 100 ms fest.

void Main()
{
	var testMono = new TestMono();

	int i = 0;
	while (i < 20)
	{
		testMono.MainLoop();
		Thread.Sleep(100);
		i++;
	}
}

Befehlsbezogene Klasse

Ertragsanweisung

Die Basisklasse der Anweisungsklasse, die häufig verwendeten WaitForFrames, WaitForFixedUpdate, WaitForSeconds, WaitForSecondsRealtime usw. werden von dieser Klasse geerbt.

Die Anweisungsklasse definiert das Verhalten der Anweisung. Die wichtigsten sind Iteration (MoveNext) und Current.

Standardmäßig teilt die Anweisungsklasse der externen Anweisung erst dann mit, dass sie über MoveNext abgeschlossen werden soll, wenn die von ihr selbst festgelegten Bedingungen erfüllt sind.

Die Implementierung ist wie folgt:

public class YieldInstruction : IEnumerator
{
	public virtual bool MoveNext()
	{
		return false;
	}

	// 实现接口, 无用
	public void Reset() { }
	public object Current { get { return null; } }
}

WaitForFrames

Die Anweisungsklasse, die nach dem Warten auf eine bestimmte Anzahl von Frames weiter ausgeführt wird, erbt von YieldInstruction und ist wie folgt implementiert:

// 等待多少帧
public class WaitForFrames : YieldInstruction
{
	private float m_Frames;
	
	public WaitForFrames(float seconds)
	{
		m_Frames = seconds;
	}

	public override bool MoveNext()
	{
		m_Frames--;
		return m_Frames > 0;
	}
}

CustomYieldInstruction

Es handelt sich ebenfalls um eine Anweisungsklasse, die jedoch nicht von YieldInstruction erbt, sondern die Iteratorschnittstelle implementiert und die Abfrage von MoveNext in ein keepWaiting- Attribut abstrahiert.

Die Unterklasse entscheidet, ob die Anweisung beendet werden soll, indem sie dieses Attribut setzt. Die Implementierung ist wie folgt:

public class CustomYieldInstruction : IEnumerator
{
	public CustomYieldInstruction() { }

	protected virtual bool keepWaiting { get; }

	public bool MoveNext()
	{
		return keepWaiting;
	}

	// 实现接口, 无用
	public void Reset() { }
	public object Current { get { return null; } }
}

WaitWhile

Nach dem in Auftrag gegebenen Urteil erbt die Anweisungsklasse, die bei Erreichen der angegebenen Bedingung weiter ausgeführt wird, von CustomYieldInstruction und wird wie folgt implementiert:

public class WaitWhile : CustomYieldInstruction
{
	Func<bool> m_Predicate;
	public WaitWhile(Func<bool> func)
	{
		m_Predicate = func;
	}

	protected override bool keepWaiting
	{
		get
		{
			return m_Predicate();
		}
	}
}

//---------------------------------------
// 使用示例
public IEnumerator Wait()
{
    Console.WriteLine("End");
    yield return new WaitWhile(() => { return m_i < 4; });
    Console.WriteLine("End");
}

Anwendungsbeispiel

public class TestMono : MonoBehavior
{
	private int m_i = 1;

	protected override void Start()
	{
		StartCoroutine(Wait());
	}

	protected override void Update()
	{
		Console.WriteLine($"------------------------------ Tick ...... {m_i}");
		m_i++;
	}

	public IEnumerator Wait()
	{
		yield return new WaitForFrames(5);
		Console.WriteLine("Begin at 6");
		yield return new WaitWhile(() => { return m_i < 4; });
		Console.WriteLine("Wait4");
		yield return null;
		Console.WriteLine("Wait5");
		yield return null;
		Console.WriteLine("Wait6");
		yield return null;

		yield return new WaitWhile(() => { return m_i < 10; });

		Console.WriteLine("End at 10");
	}
}

void Main()
{
	var testMono = new TestMono();

	int i = 0;
	while (i < 20)
	{
		testMono.MainLoop();
		Thread.Sleep(100);
		i++;
	}
}

Der Code ist relativ einfach, daher werde ich hier nicht auf Details eingehen.

Zusammenfassen

Die Implementierung von Coroutinen nutzt hauptsächlich die Eigenschaften von Iteratoren und iterierbaren Klassen.

Der Kernalgorithmus besteht darin, einen „Pfad“ in eine Coroutine-Klasse einzuschließen. Wenn während des Pfaditerationsprozesses auf „Anweisungen“ gestoßen wird, werden die Anweisungen iteriert, bis der gesamte Pfad abgeschlossen ist.

Der obige Prozess ist eine Simulation des Autors, der sich auf die Definition der Unity-Klasse und seine eigene Erkundung bezieht, und stellt nicht die tatsächliche Implementierung von Unity dar.

Hier ist der vollständige Simulationscode .

Ich hoffe, allen inspirieren und helfen zu können.

Supongo que te gusta

Origin blog.csdn.net/woodengm/article/details/119322100#comments_27913500
Recomendado
Clasificación