Aller au contenu

V — Usine à cubes

Forme d'une usine de cubes

Quel meilleur exemple d'automatisation qu'une usine ? Créons une usine de cubes.

Créez un nouvel objet GameObject vide dans la hiérarchie ("Créer vide") et nommez-le CubeFactory. Placez-le à (-1, 0, -0.25).

Maintenant, faites un clic droit sur cet objet pour créer des objets enfant à l'intérieur : six nouveaux cubes. Leurs noms, positions et échelles doivent être comme ci-dessous.

Pour gagner du temps...

Vous pouvez entrer rapidement ces chiffres en appuyant sur la touche Tab après avoir entré chacun d'eux, ce qui fera avancer le focus au champ suivant. Vous pouvez également gagner du temps en omettant les zéros avant les virgules : l'éditeur les remplira automatiquement.

  1. "CubeRight"
    • Position (0, 0.9, 0)
    • Échelle (0.5, 1.8, 0.25)
  2. "CubeLeft"
    • Position (0, 0.9, 0.5)
    • Échelle (0.5, 1.8, 0.25)
  3. "CubeBack"
    • Position (-0.2, 0.9, 0.25)
    • Échelle (0.1, 1.8, 0.25)
  4. "CubeSlope"
    • Position (0, 0.125, 0.25)
    • Rotation (0, 0, 55)
    • Échelle (0.1, 0.6, 0.25)
  5. "CubeFront"
    • Position (0.2, 1.175, 0.25)
    • Échelle (0.1, 1.25, 0.25)
  6. "CubeInteract"
    • Position (0.21, 1, 0.25)
    • Échelle (0.1, 0.2, 0.2)

Vous devriez vous retrouver avec un cuboïde creux qui a une ouverture inclinée à son extrémité inférieure — un peu comme une cheminée. "CubeInteract" devrait dépasser juste un peu :

Ajouter du texte avec TextMeshPro

Un autre "Objet 3D" très utile que nous pouvons ajouter à l'usine est Texte - TextMeshPro. Créez-en un dans l'objet GameObject CubeFactory, et donnez-lui les paramètres suivants (il peut vous demander d'importer certaines ressources la première fois que vous le faites — laissez-le faire) :

  • Dans le composant Rect Transform :
    • Position = (0.251, 1.25, 0.25)
    • Largeur = 70, Hauteur = 20
    • Rotation = (0, -90, 0)
    • Échelle = (0.01, 0.01, 1)
  • Icônes d'alignement centré et milieu dans son composant TextMeshPro - Text
  • Le texte suivant à la place de "Texte d'exemple" :

    Usine de Cubes

    Touchez le bouton avec le contrôleur et appuyez sur la gâchette pour créer un nouveau cube

Vous devriez maintenant avoir une étiquette utile pour l'usine de cubes :

Factory interaction

Pour l'instant, c'est juste une structure vide, ne faisant rien. Changeons cela avec un peu d'action.

Créez un nouveau script pour le cube que nous avons appelé CubeInteract, called IsCollidingChecker

IsCollidingChecker.cs
using UnityEngine;

public class IsCollidingChecker : MonoBehaviour
{
    public bool isColliding;

    public Color[] colors;
    private Material _material;

    void Start()
    {
        _material = GetComponent<MeshRenderer>().material;
        _material.color = colors[0];
    }

    //Detect if the Cursor starts to pass over the GameObject
    public void OnControllerEnter()
    {
        isColliding = true;
        _material.color = colors[1];
    }

    //Detect when Cursor leaves the GameObject
    public void OnControllerExit()
    {
        isColliding = false;
        _material.color = colors[0];
    }
}

La principale différence est qu'il expose maintenant sa variable booléenne isColliding à d'autres objets. Cette variable est définie sur True chaque fois qu'elle est en collision, et False lorsqu'elle ne l'est pas, tout en changeant de couleur en même temps.
Enregistrez le script et assignez deux couleurs de votre choix à ce composant dans l'éditeur.

Les fonctions OnControllerEnter et OnControllerExit ne sont pas appelée automatiquement à ce stade. Pour que l'interaction est lieu nous devons ajouter au bouton (CubeInteract) le composant "XR Simple Interactable".

Dans ce composant, vous trouverez des évènements d'interaction, nous sommes intéressés par les évènements Hover qui correspondent à la manette touchant de près le cube (évènements déclenchés lorsque la manette touche le cube, puis quand elle en sort). Assigné le CubeInteract après avoir cliqué sur le petit symbole "+", puis dans la liste à droite choisissez le bon script et fonction à appeler.

Par défaut vous pouvez créer une interaction à distance entre une manette et un objet. Si vous souhaitez n'autoriser que les interactions proche (touché) désactivez l'option "Enable Far Casting" dans les composants "Near-Far Interactor" qui sont enfants des manettes.

En exécutant le jeu, vous pouvez maintenant changer la couleur de l'objet CubeInteract en le frappant avec votre contrôleur, et vous verrez également sa valeur Is Colliding changer en conséquence :

https://umtice.univ-lemans.fr/draftfile.php/303177/user/draft/568471533/3_1_Pushing%20That%20Factory%20Button.mp4

Factory scripting

L'usine ne fonctionne toujours pas comme annoncé au-dessus de ce joli bouton. Changeons cela et créons quelques cubes.

Créez un nouveau script pour l'objet CubeFactory lui-même, appelé… CubeFactory.

Extraits

Cela sera un script plus long, donc à partir de maintenant, nous ne montrerons que des parties de son code à la fois — d'ici là, vous devriez être familiarisé avec les parties qui appartiennent où. Ne vous inquiétez pas de l'ordre des variables et des fonctions : tant qu'elles sont dans la portée correcte (crochets), peu importe lesquelles apparaissent en premier dans votre script.

Nous allons utiliser encore plus de bibliothèques maintenant qu'auparavant, alors assurez-vous d'inclure toutes ces déclarations using en haut.

CubeFactory.cs
using System.Diagnostics;
using UnityEngine;

Dans la définition de la classe CubeFactory, déclarez les variables suivantes :

CubeFactory.cs
public IsCollidingChecker isCollidingChecker;
private Stopwatch stopwatch;
public int cooldown = 500;

Les deux dernières nous aideront avec le timing : nous voulons seulement laisser l'usine produire des cubes à des intervalles minimum, donc nous créons un Stopwatch pour mesurer le temps écoulé, et une variable cooldown qui nous permet de dire combien de temps elle doit se reposer avant de produire un autre cube.

CubeFactory.cs
void Start()
{
    stopwatch = new Stopwatch();
    stopwatch.Start();
}

Au début du jeu, nous initialisons la variable stopwatch en utilisant la commande new Stopwatch() et start() immédiatement.

Cube creation

Créons maintenant la fonction qui générpublic IsCollidingCheckerera réellement de nouveaux cubes qui interagissent avec la physique, et avec des couleurs aléatoires en prime !

CubeFactory.cs
public Material mat;

public GameObject CreateCube()
{
    GameObject cubeGo = GameObject.CreatePrimitive(PrimitiveType.Cube);
    Transform cubeTrans = cubeGo.transform;

    cubeTrans.position = new Vector3(-1f, 0.9f, 0f);
    cubeTrans.localScale = new Vector3(.15f,.15f,.15f);

    MeshRenderer mr = cubeGo.GetComponent<MeshRenderer>();
    mr.material = Instantiate(mat);
    mr.material.color = Random.ColorHSV();

    Rigidbody cubeRB = cubeGo.AddComponent<Rigidbody>();
    // Add randomness otherwise cubes will all show the exact same trajectory
    cubeRB.velocity = new Vector3(Random.value * .1f,-1,Random.value * .1f) * 5f;

    return cubeGo;
}

Créez un nouveau matériel dans votre projet (Material), laissez ses paramètres par défaut. Assignez ce ce matériel à ce script dans l'éditeur.

Lorsque la fonction CreateCube est appelée, elle crée un nouvel objet GameObject que nous appelons cubeGo en invoquant la fonction CreatePrimitive de la classe GameObject, avec PrimitiveType.Cube comme entrée.

Nous prenons le transform de ce nouvel objet et stockons une référence dans un nouvel objet Transform appelé cubeTrans, afin que nous puissions travailler directement dessus dans les deux lignes suivantes, où nous lui donnons une nouvelle position et une nouvelle échelle.

Nous assignons au matériau par défaut de cubeGo une nouvelle couleur, dans ce cas une Random combinaison de Hue, Saturation, et Value (luminosité).

Enfin, nous ajoutons un composant Rigidbody à celui-ci, auquel nous assignons également une vitesse aléatoire (dans des limites), pour pimenter un peu leurs trajectoires.

Étant donné que cette fonction est déclarée non pas comme void, elle est censée retourner quelque chose, dans ce cas un GameObject (voir la première ligne du code ci-dessus — la déclaration de la fonction). Nous voulons évidemment return l'objet cubeGo que nous venons de créer, ce que fait la dernière ligne.

Requesting new cubes

Nous pouvons maintenant aller à la boucle Update() et appeler notre fonction de production de cubes :

CubeFactory.cs
void Update()
{
    if (stopwatch.Elapsed.Milliseconds >= cooldown) {
        CreateCube();
        stopwatch.Restart();
    }
}

Pour garder notre production de cubes sous contrôle, nous devons d'abord vérifier que notre chronomètre a mesuré suffisamment de millisecondes (plus que notre période de refroidissement définie). Ce n'est qu'alors qu'un cube sera créé, et le chronomètre redémarré.

Enregistrez le code et voyez-le s'exécuter : la production de l'usine est en plein essor !

Problème avec UnityEngine.InputSystem ?

Il se peut que votre éditeur de code et Unity se plaignent d'un UnityEngine.InputSystem manquant. Cela peut être facilement corrigé en allant dans le package manager et en recherchant InputSystem dans le Unity Registry. Installez-le, ce qui incitera également Unity à configurer certains paramètres et à redémarrer l'éditeur, alors assurez-vous de sauvegarder vos modifications avant de le faire.

Controlling factory output

Pour éviter une situation de type Tribbles, nous devons avoir un certain contrôle sur la production de cubes de l'usine.

Nous pouvons ajouter quelques conditions supplémentaires à notre instruction if dans la boucle Update() pour ne permettre qu'au personnel autorisé de lancer la production :

CubeFactory.cs
private bool isButtonPressed;
public void toggleButtonState(bool state)
{
    isButtonPressed = state;
}

void Update()
{
    if (stopwatch.Elapsed.Milliseconds >= cooldown &&
            isButtonPressed) {
        CreateCube();
        stopwatch.Restart();
    }
}

L'opérateur And && peut être utilisé pour enchaîner des instructions logiques. Vous pouvez mettre tout ce qui se trouve dans les parenthèses de l'instruction if() sur une seule ligne, mais de cette manière, nous préservons la lisibilité d'une ligne autrement lourde et longue.

Maintenant, l'instruction if() exécutera son bloc si le chronomètre a mesuré plus de cooldown, ET qu'une collision est détectée par l'objet CubeInspect, ET que le bouton sur le côté ([5], grab) est pressé. Si l'une de ces conditions n'est pas vraie, aucun cube ne sera produit. Cela devrait garder le sol de l'usine en sécurité.

Configurez les évènements comme suit dans CubeInteract pour que toggleButtonState soit appelé quand le bouton est pressé :

Enregistrez le code, retournez à l'éditeur et assignez le composant Is Colliding Checker de l'objet CubeInteract au composant de script de l'usine de cubes.

Manual override

Il est souvent bon d'inclure un contournement manuel des conditions comme nous l'avons fait ci-dessus pour la production de cubes. Cela peut-être pratique pour débugger en dehors de la VR. Vous pouvez étendre l'instruction if() avec une clause supplémentaire, cette fois un opérateur OU || pour que cela ressemble à ceci :

if (stopwatch.Elapsed.Milliseconds >= cooldown
    && isButtonPressed
    || Keyboard.current.spaceKey.wasReleasedThisFrame)

Maintenant, la seule condition nécessaire pour déclencher la production du prochain cube est que l'utilisateur appuie sur la touche espace de l'ordinateur qui exécute Unity (tout en étant focalisé sur la vue du jeu), contournant toutes les autres règles. Les cubes seront maintenant produits aussi vite que vous pouvez appuyer sur la barre d'espace — ne la cassez pas !

Cube removal

Même avec une production contrôlée, les cubes s'accumulent toujours avec le temps. Nettoyons-les.

Destroyer script

Créez un nouveau script dans le dossier des scripts sans l'attacher à aucun GameObject et nommez-le DestroyOnTouch. Nous voulons qu'il entre en action chaque fois que notre souris est au-dessus d'un cube fraîchement produit pour les faire disparaître dans l'oubli.

DestroyOnTouch.cs
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Interactables;

using System.Collections;

public class DestroyOnTouch : MonoBehaviour
{
    private void Start()
    {
        XRSimpleInteractable interact = gameObject.AddComponent<XRSimpleInteractable>();

        interact.hoverEntered.AddListener(DestroyCube);
    }

    public void DestroyCube(HoverEnterEventArgs eventArgs)
    {
        Destroy(gameObject);
    }
}

La fonction DestroyCube() appellera simplement la commande Destroy() sur gameObject lui-même, qui fait référence au GameObject dans lequel ce script fonctionne. Comme c'est ainsi, le code reste dans notre dossier Scripts et ne fait rien, alors attachons-le à tous les cubes fraîchement produits par notre CubeFactory.

La fonction Start() contient maintenant du code pour ajouter procéduralement le callback liant le "toucher un cube" à "appeler DestroyCube()". Nous ne pouvons pas créer ce lien dans l'éditeur dans ce cas cas nos cubes n'existent pas encore.

Attaching the destroyer

Pour attacher ce script (assurez-vous de l'enregistrer d'abord !) aux nouveaux cubes, nous devons simplement modifier la fonction CreateCube() dans CubeFactory :

CubeFactory.cs
public Material mat;

public GameObject CreateCube()
{
    GameObject cubeGo = GameObject.CreatePrimitive(PrimitiveType.Cube);
    Transform cubeTrans = cubeGo.transform;

    cubeTrans.position = new Vector3(-1f, 0.9f, 0f);
    cubeTrans.localScale = new Vector3(.15f,.15f,.15f);

    MeshRenderer mr = cubeGo.GetComponent<MeshRenderer>();
    mr.material = Instantiate(mat);
    mr.material.color = Random.ColorHSV();

    Rigidbody cubeRB = cubeGo.AddComponent<Rigidbody>();
    // Add randomness otherwise cubes will all show the exact same trajectory
    cubeRB.velocity = new Vector3(Random.value * .1f,-1,Random.value * .1f) * 5f;

    cubeGo.AddComponent<DestroyOnTouch>();

    return cubeGo;
}

La ligne ajoutée en bas montre comment le DestroyOnTouch (publiquement connu) est ajouté en tant que composant à chaque nouvel objet cubeGo, juste avant qu'il ne soit retourné.

Enregistrez ce code et essayez-le :

Making it pop

Transformons la corvée de nettoyage des cubes en une activité plus amusante en ajoutant un peu de pop.

Aussi immersive que soit la réalité virtuelle, vous pouvez encore l'intensifier en engageant des sens supplémentaires, comme notre ouïe. Unity est bien sûr capable de lecture audio, alors faisons en sorte qu'il joue un court échantillon chaque fois que nous détruisons un cube, comme un simple son de "pop".

Où trouver des échantillons sonores ?

Si vous n'êtes pas enclin à enregistrer ou synthétiser votre propre audio, le site populaire freesound.org est une excellente source gratuite de toutes sortes de matériel audible.

Pour vous faire gagner du temps pour l'instant, vous pouvez également télécharger ce clip .mp3 et l'utiliser : pop.mp3

Créez un nouveau dossier dans Assets appelé Resources, et stockez votre échantillon audio là-bas et renommez-le en pop.mp3, s'il n'est pas déjà appelé ainsi.

Retournez à notre fonction CreateCube() et ajoutez deux lignes :

CubeFactory.cs
public GameObject CreateCube()
{
    GameObject cubeGo = GameObject.CreatePrimitive(PrimitiveType.Cube);
    Transform cubeTrans = cubeGo.transform;

    cubeTrans.position = new Vector3(-1f, 0.9f, 0f);
    cubeTrans.localScale = new Vector3(.15f,.15f,.15f);

    cubeGo.GetComponent<MeshRenderer>().material.color = Random.ColorHSV();

    Rigidbody cubeRB = cubeGo.AddComponent<Rigidbody>();
    // Add randomness otherwise cubes will all show the exact same trajectory
    cubeRB.velocity = new Vector3(Random.value * .1f,-1,Random.value * .1f) * 5f;

    cubeGo.AddComponent<DestroyOnTouch>();

    AudioSource cubeAS = cubeGo.AddComponent<AudioSource>();
    cubeAS.clip = Resources.Load<AudioClip>("pop");

    return cubeGo;
}

Avec cela, nous ajoutons un autre composant aux nouveaux cubes, cette fois un AudioSource, et chargeons dans son composant clip notre fichier .mp3, en utilisant Resources.Load<AudioClip>("pop").

Cette dernière appel recherchera en fait dans le dossier Resources que nous avons créé ci-dessus tous les fichiers audio qui sont nommés "pop" avant leur extension de type de fichier, c'est pourquoi il était important de nommer notre clip "pop.mp3".

Retournez à notre script DestroyOnTouch pour le modifier. Nous ajoutons une nouvelle variable private appelée _audioSource, et la récupérons à partir du composant que l'usine ajoute maintenant aux nouveaux cubes en utilisant la fonction Start().

DestroyOnTouch.cs
public class DestroyOnTouch : MonoBehaviour
{
    private AudioSource _audioSource;

    private void Start()
    {
        XRSimpleInteractable interact = gameObject.AddComponent<XRSimpleInteractable>();
        interact.hoverEntered.AddListener(DestroyCube);

        _audioSource = GetComponent<AudioSource>(); // Last part of the tutorial (with pop sound added)
    }
}

Maintenant, changeons le reste de DestroyOnTouch pour réellement lire le son lors de la destruction :

DestroyOnTouch.cs
public void DestroyCube(HoverEnterEventArgs eventArgs)
{
    // Destroy(gameObject);
    StartCoroutine(PlayAudioThenDestroy()); // Last part of the tutorial (with pop sound added)
}

private IEnumerator PlayAudioThenDestroy()
{
    // Hide object
    Destroy(GetComponent<MeshRenderer>());
    // Delete collider component to prevent calling this coroutine twice
    Destroy(GetComponent<Rigidbody>());
    // Play pop sound
    _audioSource.Play();
    yield return new WaitUntil(() => !_audioSource.isPlaying);
    // Actually destroy the object now
    Destroy(gameObject);
}

Dans DestroyCube(), nous commentons notre ligne précédente qui détruit simplement l'objet (le rendant inerte) et ajoutons à la place un appel à StartCoroutine() sur le bloc IEnumerator que nous déclarons ci-dessous.

Coroutines

Les coroutines sont une fonctionnalité très puissante de Unity (et un concept informatique en général), qui permettent l'exécution différée de code sans bloquer tout le système — permettant ainsi l'exécution parallèle de plusieurs comportements.

Cela sera exploré plus en détail dans l'exercice suivant, mais n'hésitez pas à essayer de comprendre la fonction PlayAudioThenDestroy() ici à partir de son contexte et de ses commentaires.

Enregistrez le code, exécutez le jeu. Ça fonctionne !