vendredi 7 août 2015

Unity3D - Coroutines et retour de valeurs

Sur Unity3D, un mécanisme simple permet à du code procédural de lancer des traitements asynchrones et d'attendre la fin de son exécution: les coroutines.
Ce mécanisme n'a rien de magique. Il se repose sur les énumérateurs, qui permettent à du code de renvoyer une liste de valeurs, une par une. Cependant, les coroutines ne permettent pas de retourner de valeurs que l'on puisse exploiter.
Il existe toutefois une manière simple de le faire.
For an english version of this article, follow this link: http://antoine-agthe.blogspot.com/2015/08/unity3d-returning-value-from-coroutine.html

Les énumérateurs

En C#, l'avantage des énumérateurs est que l'on peut mettre l'exécution d'une méthode "en pause" grâce à l'instruction yield, et ce n'est que lorsqu'on souhaite récupérer la valeur suivante que le code poursuit son exécution, à l'instruction sur laquelle il s'était arrêté.
IEnumerator Start ()
{
    Debug.Log ( "Let's wait 10 frames" );
    yield return WaitForFrames ( 10 );
    Debug.Log ( "Finished" );
}

YieldInstruction WaitForFrames ( int count )
{
    var routine = DoAsyncRoutine ( count );
    return StartCoroutine ( routine );
}

IEnumerator DoAsyncRoutine ( int count )
{
    var i = 0;
    while ( i < count )
    {
        yield return null;
    }

    yield break; // This instruction is useless in this context.
                 // It just helps me illustrate.
}
Ce mécanisme est d'ailleurs très utile quand on souhaite donner l'accès à une liste de valeurs, sans pour autant autoriser de modifications dans cette liste. Pensez néanmoins à fournir à votre interface un accesseur sur le nombre d'éléments disponibles.
IEnumerator Start ()
{
    Debug.Log ( "Let's wait 10 frames" );
    yield return WaitForFrames ( 10 );
    Debug.Log ( "Finished" );
}

YieldInstruction WaitForFrames ( int count )
{
    var routine = DoAsyncRoutine ( count );
    return StartCoroutine ( routine );
}

IEnumerator DoAsyncRoutine ( int count )
{
    var i = 0;
    while ( i < count )
    {
        yield return null;
    }

    yield break; // This instruction is useless in this context.
                 // It just helps me illustrate.
}
This mecanism is very useful when you want to get access to a list of values, but you want to prevent this list to be modified. Just keep in mind that you have to create an accessor to get the number of values of your list.
class ReadOnlyListExample
{
    List<object> mValues;

    public ReadOnlyListExample ()
    {
        // mValues initialization with n values.
    }

    // Retrieves the number of available values.
    public int Count
    {
        get { return mValues.Count; }
    }

    // Retrieves the values, one by one.
    public IEnumerable<object> Values
    {
        get
        {
            for ( var i = 0; i < mValues.Count; i++ )
            {
                yield return mValues[i];
            }
        }
    }
}
    

Les coroutines

Maintenant que ce point sur les énumérateurs est fini, passons au cas spécifique des coroutines.
IEnumerator Start ()
{
    Debug.Log ( "Let's wait 10 frames" );
    yield return WaitForFrames ( 10 );
    Debug.Log ( "Finished" );
}

YieldInstruction WaitForFrames ( int count )
{
    var routine = DoAsyncRoutine ( count );
    return StartCoroutine ( routine );
}

IEnumerator DoAsyncRoutine ( int count )
{
    var i = 0;

    while ( i < count )
    {
        yield return null;
    }

    yield break; // This instruction is useless in this context.
                 // It just helps me illustrate.
}
    
Dans cet exemple, on patiente pendant n images (10, en l'occurence) avant d'afficher un message de fin. En interne, Unity gère ses coroutines avec des listes. À l'appel de StartCoroutine(), le système crée un objet YieldInstruction qui référence l'énumérateur fourni en paramètre. Cet objet YieldInstruction est ajouté à une liste d'exécution. Cette liste est traitée avant chaque la mise à jour des composants de Unity (Update()). Le système demande alors à chaque YieldInstruction de récupérer la valeur suivante de leur énumérateur. La suite dépend de la valeur renvoyée par l'énumérateur. La plupart du temps, renvoyer une valeur ne sert à rien, car la valeur est perdue. Il est donc préférable de renvoyer null. (voir yield return null dans la méthode DoAsyncRoutine()). Mais certaines valeurs ont un impact sur le comportement du système. Dans l'exemple, Start() retourne un IEnumerator, ce qui a pour effet de forcer le système à créer une coroutine avec Start(). Appelons cette coroutine StartInstruction. Dans l'énumérateur de StartInstruction, on exécute yield return WaitForFrames (n). WaitForFrames(n) retourne une YieldInstruction, que l'on appellera WaitInstruction. Cela a pour effet de stopper momentanément StartInstruction, le temps que WaitInstruction se termine. Le principe est simple. Quand le système récupère WaitInstruction suite ù l'appel de la valeur suivante de l'énumérateur de StartInstruction, il remarque que cette valeur est une YieldInstruction. Il va donc sortir StartInstruction des coroutines à traiter, et y placer WaitInstruction. Quand WaitInstruction n'a plus de valeur à renvoyer, ou qu'une instruction explicite yield break a été exécutée, il va sortir WaitInstruction de la liste, et y remettre StartInstruction. Ce n'est pas forcément la stratégie adoptée par les développeurs de Unity3D, mais cette version est facile à comprendre. Le résultat est que tant que WaitInstruction renvoie des valeurs, StartInstruction n'est pas traitée. On a donc trois solutions de retour dans l'énumérateur d'une coroutine:
  • un objet YieldInstruction, qui notifiera au système de faire un traitement particulier
  • une valeur quelconque, ou NULL, qui sera juste ignorée par le système.
  • yield break, qui stoppe l'énumérateur, et par conséquent la coroutine associée.
Il y a plusieurs types d'objets YieldInstruction disponibles, comme WaitForSeconds, WaitForFixedUpdate, … qui ont juste pour effet de placer la coroutine dans une liste différente, traitée à un moment particulier de la boucle d'exécution.

Retourner une valeur d'une coroutine

Mais alors, si toute valeur de retour est ignorée, comment récupérer le résultat d'un traitement asynchrone sous Unity3D. La solution la plus facile à mettre en œuvre est celle de la closure. L'avantage d'une coroutine est qu'elle permet d'écrire du code asynchrone de manière procédurale. L'idée n'est donc pas d'injecter dans la routine une lambda qui exécute du code complexe, mais bien de sortir le résultat d'un traitement, de la méthode qui l'a générée, à travers la lambda. De cette manière, on conserve l'esprit du code procédural.
// My asynchronous process returning un boolean.
// Note that my process takes a lambda as parameter, which takes
// a boolean as parameter.
IEnumerator ProcessRoutine ( Action<bool> resultCB )
{
    // Makes the process asynchronous
    yield return null;

    // I create a result
    var result = true;

    if (resultCB != null)
    {
        // I give my result to my callback
        resultCB( result );
    }
}

YieldInstruction Process ( Action<bool> resultCB )
{
    // I give my callback to the process
    return StartCoroutine ( ProcessRoutine ( resultCB ) );
}

IEnumerator Start ()
{
    // I create a target variable which will keep the result of
    // my asynchronous process.
    bool result;

    // I give a lambda to my process.
    // This lambda just updates the local "result" variable
    // with the result of the process.
    yield return Process ( r => result = r );

    // Here I can use my result
    if ( result )
    {
        // …
    }
}
    

Aucun commentaire:

Enregistrer un commentaire