Implémentation de la simulation d'instructions de coroutine personnalisées et de coroutines dans Unity

Cet article partage l'implémentation de simulation d'instructions de coroutine personnalisées et de coroutines dans Unity

Dans le dernier article, nous avons brièvement partagé les concepts de base et l'utilisation des coroutines dans Unity et Lua, et fait quelques comparaisons entre les deux.

Dans cet article, nous allons explorer plus en détail l'implémentation des coroutines par Unity, et deviner et simuler comment Unity implémente les coroutines en personnalisant les coroutines.

Instructions de coroutine personnalisées dans Unity

Les instructions telles que WaitForSeconds et WaitForEndOfFrame fournies par Unity héritent par défaut de YieldInstruction .

Fournit également des moyens flexibles de définir des directives personnalisées.

Le document officiel propose deux façons d'implémenter des commandes personnalisées, qui sont présentées une par une ci-dessous.

En héritant de la classe CustomYieldInstruction

Unity fournit la classe CustomYieldInstruction , dont nous pouvons hériter pour implémenter nos propres instructions de coroutine, telles que le temps d'attente, le respect de certaines conditions, etc.

Le point central de l'instruction personnalisée est de réécrire la propriété keepWaiting de la classe CustomYieldInstruction , qui est utilisée pour fournir à Unity la possibilité de déterminer si l'instruction est terminée.

Unity demandera la directive pour cette propriété entre Update et LateUpdate à chaque frame .

Voici l'exemple 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");
    }
}

Voici l'exemple 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");
    }
}

Implémenter des instructions personnalisées en implémentant l'interface IEnumerator et en construisant un itérateur

Unity implémente des coroutines via des itérateurs et des objets itérables, nous pouvons donc également construire des itérateurs pour obtenir l'effet souhaité.

Voici un exemple:

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");
    }
}

On peut voir que cette méthode est très similaire à l'exemple 1, sauf que keepWaiting est remplacé par MoveNext, et l'implémentation par défaut de l'interface est fournie.

L'utilisation décrite ci-dessus est très simple, et il n'y a rien de plus à dire.

Dans les pages suivantes, nous allons essayer d'apprendre de Unity pour simuler et implémenter des coroutines par nous-mêmes.

Simuler l'implémentation de coroutines Unity

Notre simulation implique probablement plusieurs classes :

  • MonoBehavior, le script dans lequel nous écrivons habituellement du code, le point de départ de la coroutine.

  • YieldInstruction, une classe d'instructions, est une classe itérable et implémente l'interface IEnumerable.

  • WaitForFrames, classe d'instruction, héritée de YieldInstruction, permettant d'attendre un certain nombre de trames

  • Coroutine, une classe coroutine, hérite de YieldInstruction et fournit une implémentation spécifique.

  • CustomYieldInstruction, une classe d'instructions personnalisées, implémente l'interface IEnumerator, fournissant un comportement personnalisé

  • WaitWhile, une classe d'instruction personnalisée, héritée de CustomYieldInstruction, utilisée pour l'attente conditionnelle

coroutine

Tout d'abord, regardons la classe coroutine.

La classe coroutine peut être considérée comme le cœur de toute l'implémentation (nonsense_ ) .Elle joue un lien entre le précédent et le suivant à différents points.

La classe coroutine hérite de YieldInstruction et est elle-même un itérateur. Chaque itération fera avancer le code une fois, qui peut être exécuté jusqu'au rendement suivant, ou exécuté jusqu'à la fin du code, ou exécuté une fois.

Propriétés des coroutines

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

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

m_Routine maintient un itérateur, qui est la valeur de retour de la méthode coroutine que nous avons écrite dans le script Mono.

Chaque MoveNext de m_Routine réexécutera la méthode coroutine jusqu'à ce qu'il rencontre yield ou la fin de la méthode.

Après que m_Routine ait exécuté MoveNext, son Current pointera vers un nouvel itérateur (s'il existe), correspondant au code yield return new xxx, ce xxx est le nouvel itérateur.

Nous devons itérer l'itérateur pointé par Current pour continuer l'exécution de la méthode coroutine.

Ce courant est au cœur de notre capacité à implémenter des coroutines.

Nous résumons ce Current en une instruction yield , c'est-à-dire qu'il peut s'agir d'une YieldInstruction, d'une Coroutine, d'une CustomYieldInstruction ou même d'un IEnumerator. Tant qu'il implémente l'interface IEnumerator ou un comportement similaire, il peut être utilisé.

Ainsi, le deuxième attribut de Coroutine, nous le définissons comme l'instruction courante .

Méthode coroutine

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;
}

La première est la méthode de construction, qui accepte un itérateur, qui est la valeur de retour de la méthode coroutine que nous avons écrite.

Vient ensuite la description de l'algorithme de base :

  1. La coroutine itère une fois (MoveNext), s'il y a une instruction en cours, puis saute à la quatrième étape, sinon saute à l'étape suivante
  2. Le chemin est itéré une fois (MoveNext), s'il renvoie false, cela signifie que l'exécution de la méthode coroutine est terminée et que toute l'opération coroutine est terminée, sinon passez à l'étape suivante
  3. Obtenez l'instruction actuelle via Current, si elle est nulle, suspendez l'exécution de la coroutine, attendez la prochaine itération du chemin, sinon passez à l'étape suivante
  4. L'instruction itère une fois (MoveNext), si elle retourne false, cela signifie que l'instruction est exécutée, alors suspendez l'opération de la coroutine et attendez la prochaine itération du chemin

Le sens général est d'obtenir des instructions en fonction de l'itération du chemin et d'itérer le chemin une fois l'instruction terminée jusqu'à ce que l'itération du chemin soit terminée.

MonoBehavior, le démarrage, l'arrêt et l'appel de la coroutine

Parlons ensuite du processus de démarrage et d'appel de la coroutine.

Une liste de coroutines est maintenue dans le script Mono : List<Coroutine> m_DelayCallLst.

Les coroutines de la liste sont itérées entre chaque mise à jour et LateUpdate.

Utilisez StartCoroutine/StopCoroutine pour démarrer ou arrêter la coroutine.

Voici le code approximatif :

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);
	}
}

Appelez la méthode Update du script Mono à chaque image de la boucle principale, ici nous définissons chaque image sur 100 ms.

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

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

Classe liée à la commande

RendementInstruction

La classe de base de la classe d'instructions, les WaitForFrames, WaitForFixedUpdate, WaitForSeconds, WaitForSecondsRealtime, etc. couramment utilisés sont héritées de cette classe.

La classe d'instruction définit le comportement de l'instruction, les plus importantes sont l'itération (MoveNext) et Current.

Par défaut, la classe d'instruction indique à l'instruction externe de se terminer via MoveNext uniquement après que les conditions définies par elle-même sont remplies.

La mise en œuvre est la suivante :

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

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

WaitForFrames

La classe d'instruction qui continue à s'exécuter après avoir attendu un nombre spécifié de trames, hérite de YieldInstruction et est implémentée comme suit :

// 等待多少帧
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

C'est aussi une classe d'instructions, mais au lieu d'hériter de YieldInstruction, elle implémente l'interface d'itérateur, en faisant abstraction de la requête de MoveNext en un attribut keepWaiting .

La sous-classe décide de terminer l'instruction en définissant cet attribut. L'implémentation est la suivante :

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; } }
}

Patiente pendant que

Selon le jugement commandé, la classe d'instructions qui continue de s'exécuter lorsque la condition spécifiée est atteinte, hérite de CustomYieldInstruction et est implémentée comme suit :

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");
}

Exemple d'utilisation

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++;
	}
}

Le code est relativement simple, je n'entrerai donc pas dans les détails ici.

Résumer

L'implémentation des coroutines utilise principalement les caractéristiques des itérateurs et des classes itérables.

L'algorithme de base consiste à envelopper un "chemin" dans une classe coroutine. Lorsque des "instructions" sont rencontrées pendant le processus d'itération du chemin, les instructions sont itérées jusqu'à ce que le chemin entier soit terminé.

Le processus ci-dessus est une simulation de l'auteur se référant à la définition de la classe Unity et à sa propre exploration, et ne représente pas l'implémentation réelle de Unity.

Voici le code de simulation complet .

J'espère pouvoir inspirer et aider tout le monde.

Je suppose que tu aimes

Origine blog.csdn.net/woodengm/article/details/119322100#comments_27913500
conseillé
Classement