Aller au contenu

VI — Programmation de séquence (coroutine)

Nous avons maîtrisé les événements automatisés et simples avec la Cube Factory. Maintenant, créons des flux plus complexes et plongeons plus profondément dans les coroutines !

Réutiliser le projet Unity précédent

Nous pouvons encore gagner du temps en continuant à utiliser le projet précédent. Assurez-vous simplement de supprimer ou de désactiver l'objet CubeFactory et vous devriez être prêt à partir.

Archiver et copier des projets Unity

Une autre façon de continuer à travailler sans sacrifier les projets déjà terminés est de faire une copie d'un projet Unity entier dans votre explorateur de fichiers, par exemple, en allant dans le hub Unity, en faisant un clic droit sur votre projet pour "Afficher dans Finder/Explorer" et en copiant son dossier entier. Vous pouvez renommer le dossier copié comme vous le souhaitez, puis revenir au Hub et cliquer sur Ouvrir au lieu de Nouveau Projet, puis le pointer vers votre dossier nouvellement copié.

Configurer la scène

Nous créons à nouveau quelques nouveaux objets.

Créez en tant qu'enfants de l'objet du plan de sol deux nouveaux objets 3D Plane et configurez-les comme ceci :

  • "StandingMark"
    • Position: (-2.333, .001, -2.333)
    • Scale: (.1, 1, .1)
  • "OriginMark"
    • Position (0, .001, 0)
    • Echelle (.1, 1, .1)

Créez un matériel avec une couleur bleue nommé BlueBox et un avec une couleur rouge nommé RedBox._Assignez le Material _BlueBox à StandingMark, et le Material RedBox à OriginMark.

En dehors de l'objet du sol, dans les espaces vides de la hiérarchie de notre scène, créez :

  • Un objet 3D Sphère et nommez-le "Zeppelin" :
    • Position (-1, 1.5, -1)
    • Rotation (0, 90, 0)
    • Echelle (.5, .2, .2)
  • Un nouveau Cube :
    • Position (0, 1.5, 1)
    • Scale (.2, .1, .2)

Attachez au nouveau Sphere (Zeppelin) et au nouveau cube notre script existant IsCollidingChecker en tant que composant : Cliquez sur Ajouter un composant et commencez à taper son nom.

Donnez aux composants Is Colliding Checker que vous venez d'attacher au Cube et au Zeppelin deux nouvelles couleurs de votre choix. Elles peuvent être différentes pour pimenter un peu les choses !

Mettre les objets en mouvement

Vous devriez maintenant avoir les deux derniers objets simplement flottant dans l'air. Écrivons de nouveaux scripts pour les faire bouger.

Créez un script appelé Moving et attachez-le à l'objet Zeppelin :

Moving.cs
using UnityEngine;

public class Moving : MonoBehaviour
{
    [Tooltip("Units per second")] public float speed;
    [Tooltip("Start position in 3D space")] public Vector3 startPoint;
    [Tooltip("End position in 3D space")] public Vector3 endPoint;

    public float interpolator = 1f;
    public bool isDone => interpolator > .999f;

    void Update()
    {
        print(isDone);
        if (isDone) return;

        interpolator += speed * Time.deltaTime;
        transform.position = Vector3.Lerp(startPoint, endPoint, interpolator);
    }
}

Il prend speed et deux variables Vector3 pour sa vitesse de déplacement (via interpolation) et pour les points de départ et d'arrivée de son voyage.

isDone ?

L'expression isDone => interpolator > .999f est un raccourci pratique : l'opérateur => assigne à isDone le résultat de l'évaluation de interpolator > .999f, similaire à ce qu'un énoncé if ferait. Vous pouvez en lire plus à ce sujet dans la documentation C#.

Ce que cela fait effectivement, c'est de vérifier constamment si le interpolator est supérieur à .999f, et de définir isDone à true si c'est le cas, et false si ce n'est pas le cas.

interpolator étant déjà 1 au début définira isDone à true, ce qui annulera l'exécution de Update(). Il devra être défini à une valeur plus petite pour commencer à fonctionner.

Cette fonction affiche également l'état de isDone dans la Console à chaque mise à jour en utilisant la commande print() : vous pouvez le voir lorsque le programme est en cours d'exécution, car il remplira rapidement la console avec des impressions et continuera à défiler vers le bas.

Impression dans la console

Il peut être judicieux d'afficher l'état des variables dans la console pour avoir une compréhension claire de ce qui se passe. Le faire comme ci-dessus (impression à chaque Update()) est une façon, mais généralement, cela n'est utilisé qu'à des événements spécifiques, comme lorsque une variable est modifiée. Utiliser print() — ou l'équivalent Unity Debug.Log() — est un puissant outil pour comprendre et déboguer votre code, alors n'hésitez pas à l'essayer sur d'autres variables et à d'autres endroits dans votre code par vous-même !

Définissez speed dans l'inspecteur à 0.25, et donnez (-1, 1.5, -1) comme point de départ et (-1, 1.5, 1) comme point d'arrivée pour sa trajectoire.

Maintenant, créez un script appelé Rotating pour le nouvel objet cube :

Rotating.cs
using UnityEngine;

public class Rotating : MonoBehaviour
{
    [Tooltip("Units per second")] public float speed;
    [Tooltip("Axis to rotate around")] public Vector3 axis;

    public bool isRotating;

    void Update()
    {
        if (!isRotating) return;

        transform.Rotate(axis, speed * Time.deltaTime);
    }
}

Très similaire à notre premier script de rotation, il ne diffère que par le fait d'avoir un booléen public appelé isRotating qui est vérifié avant d'effectuer la rotation — il agit essentiellement comme un interrupteur marche/arrêt.

Définissez la vitesse à 180 et l'axe de rotation à (1, 0, 0) dans l'inspecteur.

Si vous exécutez le jeu maintenant, seul l'objet cube devrait tourner, et ce uniquement si vous définissez son paramètre isRotating sur true dans son composant Rotating dans l'inspecteur. Essayez-le, et jouez avec les paramètres dans l'inspecteur pour voir leurs effets :

Script d'un protocole

Avec les deux objets en mouvement et les deux zones sur lesquelles marcher, nous avons préparé le terrain pour introduire des flux plus complexes, tels que ceux qui pourraient être nécessaires pour de véritables expériences (ou même des jeux).

Variables et affectations

Créez un nouveau script pour l'objet Main Camera et nommez-leProtocol:

Protocol.cs
using System.Collections;
using UnityEngine;

public class Protocol : MonoBehaviour
{
    public Moving movingComp;
    public Rotating rotatingComp;

    public IsCollidingChecker zeppelinColliderChecker;
    public IsCollidingChecker cubeColliderChecker;

    public Transform headCamera;
    public Transform standingMark;

    public float positionTolerance = 0.15f;

    private bool isRunning = true;
}

À ce stade, rien dans cette première partie de la déclaration ne devrait être inconnu : Protocol.cs contient un certain nombre de variables publiques, quatre composants d'objets, deux transformations, un nombre à virgule flottante et un booléen.

Enregistrez le script tel qu'il est jusqu'à présent, et assurez-vous qu'il est attaché à notre "Main Camera". Retournez dans l'éditeur Unity pour assigner les variables non remplies dans l'inspecteur.

Pouvez-vous découvrir par vous-même comment remplir correctement les champs dans l'inspecteur pour le composant Protocol du "Main Camera" ?Les noms et types attendus devraient rendre cela facile.

Continuez seulement lorsque vous êtes sûr de l'avoir configuré correctement. N'hésitez pas à demander si vous rencontrez des difficultés ici.

Fonctions d'interaction

Donnons-nous la capacité d'interagir avec Protocol.cs dans le monde VR. Ajoutez ces fonctions :

Protocol.cs
private void Update()
{
    if (button.ReadWasCompletedThisFrame() && isRunning)
    {
        isRunning = false;
    }
}

private bool IsStandingOnTarget(Vector2 targetPos)
{
    Vector3 pos3D = headCamera.position;
    Vector2 standingPos = new Vector2(pos3D.x, pos3D.z);

    return Vector2.Distance(standingPos, targetPos) < positionTolerance;
}

Utilisez la méthode décrite dans la section IV à la sous-section "Les moyens d'interaction" > "En programmant" pour que la variable button fasse référence à l'état d'un bouton d'une des manettes.

La boucleUpdate() devrait être claire : si le bouton est pressé ETisRunning est déjà vrai, ALORS mettez la variable isRunning à faux.

IsStandingOnTarget(Vector2 targetPos) prend une position cible donnée (un vecteur 2D) et mesure sa distance à une projection 2D de la headCamera — si elle est inférieure à la positionTolerance, elle retourne vrai, sinon faux.

Séquence scriptée

Mettons maintenant ces variables et fonctions au travail.

Nous allons utiliser l'appel Start() pour cela, mais d'abord, changez-le d'une fonction de type private void basique, nous allons la transformer enIEnumerator — de cette façon, elle agit comme une coroutine et nous pouvons arrêter et continuer son exécution en utilisantWaitWhile(), WaitUntil(), et WaitForSecondsRealtime() commandes.

Remplacez la fonction Start() actuelle par ceci :

Protocol.cs
private IEnumerator Start()
{
    while (isRunning)
    {
        // Wait until user has moved onto square on the floor
        Vector3 standingMarkPos = standingMark.position;
        yield return new WaitWhile(() => !IsStandingOnTarget(new Vector2(standingMarkPos.x, standingMarkPos.z)));
        print("Stepped on the first square");
        standingMark.gameObject.SetActive(false); // Hide

        // Wait until user has touched the zeppelin
        yield return new WaitUntil(() => zeppelinColliderChecker.isColliding);
        print("Touched the Zeppelin");

        movingComp.interpolator = 0; // This triggers the start of Zeppelin's animation
        // Wait for moving animation to end
        yield return new WaitUntil(() => movingComp.isDone);
        print("Zeppelin's animation is done");

        // Move to center of the room
        yield return new WaitWhile(() => !IsStandingOnTarget(Vector2.zero));
        print("Stepped on the center square");

        // Wait until user has touched the cube
        yield return new WaitUntil(() => cubeColliderChecker.isColliding);
        rotatingComp.isRotating = true; // Start rotating cube
        print("Touched the cube");

        // Wait one second while it rotates
        yield return new WaitForSecondsRealtime(1f);
        rotatingComp.isRotating = false; // Stop rotating cube
        print("One second has passed");

        // RESET everything
        standingMark.gameObject.SetActive(true); // Show
        movingComp.transform.position = movingComp.startPoint;
        rotatingComp.transform.rotation = Quaternion.identity;
        print("Everything's reset!");
    }
}

Vous devriez être en mesure de lire ce script et de comprendre ce qu'il fait. Les différentes lignes yield return new interrompent l'exécution de la fonction (arrêtant l'avancement vers les lignes suivantes) jusqu'à ce que leurs clauses Wait soient remplies, comme expliqué dans les commentaires pour chaque ligne.

Enregistrez le script, retournez dans Unity, et assurez-vous que le cube n'est pas déjà configuré pour tourner.

Exécutez le jeu et essayez d'avancer à travers les différentes étapes comme vous pouvez le voir dans le protocole !

Défi : plus de déclenchement

Pour l'instant, le déclencheur de la manette ne fait pas grand-chose d'autre que d'interrompre la séquence. Pouvez-vous penser à un moyen de l'utiliser de manière plus créative ici ?