Comment ne pas geler le fil principal dans Unity?


33

J'ai un algorithme de génération de niveau qui est lourd en calcul. En tant que tel, l'appel entraîne toujours le blocage de l'écran du jeu. Comment puis-je placer la fonction sur un deuxième thread alors que le jeu continue à afficher un écran de chargement pour indiquer que le jeu n'est pas gelé?


1
Vous pouvez utiliser le système de threads pour exécuter d'autres travaux en arrière-plan ... mais ils ne peuvent rien appeler dans l'API Unity, car ces éléments ne sont pas threadsafe. Juste pour commenter parce que je n'ai pas fait cela et je ne peux pas vous donner rapidement un exemple de code en toute confiance.
Almo

Utilisation Listd’Actions pour stocker les fonctions que vous souhaitez appeler dans le fil principal. verrouiller et copier la Actionliste dans la Updatefonction à la liste temporaire, effacer la liste d'origine, puis exécuter le Actioncode dans celui Listdu thread principal. Voir UnityThread de mon autre article pour savoir comment faire cela. Par exemple, pour appeler une fonction sur le fil principal, UnityThread.executeInUpdate(() => { transform.Rotate(new Vector3(0f, 90f, 0f)); });
Programmeur

Réponses:


48

Mise à jour: en 2018, Unity déploie un système de travail C # comme moyen de décharger du travail et d'utiliser plusieurs cœurs de processeur.

La réponse ci-dessous est antérieure à ce système. Cela fonctionnera toujours, mais il existe peut-être de meilleures options dans Unity moderne, en fonction de vos besoins. En particulier, le système de travail semble résoudre certaines des limitations auxquelles les threads créés manuellement peuvent accéder en toute sécurité, décrites ci-dessous. Les développeurs expérimentant avec le rapport de prévisualisation signalent par exemple des radios et des constructions de maillages en parallèle .

J'inviterais les utilisateurs expérimentés dans ce système d'emploi à ajouter leurs propres réponses reflétant l'état actuel du moteur.


Auparavant, j'avais déjà utilisé le threading pour les tâches lourdes dans Unity (généralement le traitement des images et de la géométrie). Ce n'est pas très différent de l'utilisation de threads dans d'autres applications C #, avec deux mises en garde:

  1. Comme Unity utilise un sous-ensemble un peu plus ancien de .NET, il existe certaines fonctionnalités et bibliothèques de threads plus récentes que nous ne pouvons pas utiliser telles quelles, mais les bases sont toujours là.

  2. Comme Almo le note dans un commentaire ci-dessus, de nombreux types d'Unity ne sont pas threadsafe et jetteront des exceptions si vous essayez de les construire, de les utiliser ou même de les comparer à partir du thread principal. Points à garder à l'esprit:

    • Un cas courant consiste à vérifier si une référence GameObject ou Monobehaviour est nulle avant de tenter d'accéder à ses membres. myUnityObject == nullappelle un opérateur surchargé pour tout ce qui est issu de UnityEngine.Object, mais System.Object.ReferenceEquals()contourne-le dans une certaine mesure - rappelez-vous simplement qu'un GameObject Destroy () ed compare égal à null en utilisant la surcharge, mais n'est pas encore à null.

    • La lecture de paramètres à partir de types Unity est généralement sûre sur un autre thread (en ce sens qu'elle ne lève pas immédiatement une exception tant que vous veillez à vérifier les null comme ci-dessus), mais notez ici l'avertissement de Philipp selon lequel le thread principal pourrait être en train de modifier son état. pendant que tu le lis. Vous devrez faire preuve de discipline en ce qui concerne les personnes autorisées à modifier quoi et quand afin d'éviter la lecture de certains états incohérents, ce qui peut entraîner des bogues difficiles à localiser car ils dépendent de la minuterie inférieure à la milliseconde entre les threads. ne pas reproduire à volonté.

    • Les membres statiques Random et Time ne sont pas disponibles. Créez une instance de System.Random par thread si vous avez besoin d'aléatoire et System.Diagnostics.Stopwatch si vous avez besoin d'informations temporelles.

    • Les fonctions Mathf, Vector, Matrix, Quaternion et Color fonctionnent parfaitement sur tous les threads. Vous pouvez ainsi effectuer la plupart de vos calculs séparément.

    • La création de GameObjects, l'attachement de Monobehaviours, ou la création / mise à jour de textures, de maillages, de matériaux, etc. doivent tous se produire sur le fil principal. Dans le passé, lorsque j'avais besoin de travailler avec ceux-ci, j'avais configuré une file d'attente producteur-consommateur, où mon thread de travail préparait les données brutes (comme un grand tableau de vecteurs / couleurs à appliquer à un maillage ou une texture), et une mise à jour ou une coroutine sur le thread principal interroge les données et les applique.

Avec ces notes en retrait, voici un motif que j'utilise souvent pour le travail en mode fileté. Je ne garantis pas qu'il s'agit d'un style de pratique exemplaire, mais le travail est fait. (Les commentaires ou modifications à améliorer sont les bienvenus - je sais que le filetage est un sujet très profond dont je ne connais que les bases)

using UnityEngine;
using System.Threading; 

public class MyThreadedBehaviour : MonoBehaviour
{

    bool _threadRunning;
    Thread _thread;

    void Start()
    {
        // Begin our heavy work on a new thread.
        _thread = new Thread(ThreadedWork);
        _thread.Start();
    }


    void ThreadedWork()
    {
        _threadRunning = true;
        bool workDone = false;

        // This pattern lets us interrupt the work at a safe point if neeeded.
        while(_threadRunning && !workDone)
        {
            // Do Work...
        }
        _threadRunning = false;
    }

    void OnDisable()
    {
        // If the thread is still running, we should shut it down,
        // otherwise it can prevent the game from exiting correctly.
        if(_threadRunning)
        {
            // This forces the while loop in the ThreadedWork function to abort.
            _threadRunning = false;

            // This waits until the thread exits,
            // ensuring any cleanup we do after this is safe. 
            _thread.Join();
        }

        // Thread is guaranteed no longer running. Do other cleanup tasks.
    }
}

Si vous n'avez pas strictement besoin de répartir le travail sur plusieurs threads pour gagner du temps, et que vous cherchez simplement un moyen de le rendre non bloquant afin que le reste de votre jeu continue de tourner, une solution plus légère dans Unity est Coroutines . Ce sont des fonctions qui peuvent faire un peu de travail, puis céder le contrôle au moteur pour continuer ce qu’il fait, et le reprendre de manière transparente plus tard.

using UnityEngine;
using System.Collections;

public class MyYieldingBehaviour : MonoBehaviour
{ 

    void Start()
    {
        // Begin our heavy work in a coroutine.
        StartCoroutine(YieldingWork());
    }    

    IEnumerator YieldingWork()
    {
        bool workDone = false;

        while(!workDone)
        {
            // Let the engine run for a frame.
            yield return null;

            // Do Work...
        }
    }
}

Cela ne nécessite aucune considération de nettoyage particulière, car le moteur (pour autant que je sache) supprime les coroutines d'objets détruits pour vous.

Tout l’état local de la méthode est conservé lorsqu’il cède et reprend, de sorte que, dans de nombreux cas, il est comme s’il fonctionnait sans interruption sur un autre thread (mais vous avez toutes les commodités de l’exécution sur le thread principal). Vous devez simplement vous assurer que chaque itération est suffisamment courte pour ne pas ralentir votre thread principal de manière déraisonnable.

En vous assurant que les opérations importantes ne sont pas séparées par un rendement, vous pouvez obtenir la cohérence d'un comportement à un seul thread - en sachant qu'aucun autre script ou système du thread principal ne peut modifier les données sur lesquelles vous êtes en train de travailler.

La ligne de retour de rendement vous donne quelques options. Vous pouvez...

  • yield return null pour reprendre après la mise à jour de la prochaine image ()
  • yield return new WaitForFixedUpdate() à reprendre après la prochaine FixedUpdate ()
  • yield return new WaitForSeconds(delay) à reprendre après un certain temps de jeu
  • yield return new WaitForEndOfFrame() à reprendre une fois le rendu de l'interface graphique terminé
  • yield return myRequestmyRequestest une instance WWW , à reprendre une fois que le chargement des données demandées est terminé à partir du Web ou du disque.
  • yield return otherCoroutineotherCoroutineest une instance Coroutine , à reprendre une fois otherCoroutineterminée. Ceci est souvent utilisé dans la forme yield return StartCoroutine(OtherCoroutineMethod())pour enchaîner l'exécution à une nouvelle coroutine qui peut elle-même céder quand elle le souhaite.

    • Expérimentalement, ignorer le second StartCoroutineet simplement écrire permet d' yield return OtherCoroutineMethod()atteindre le même objectif si vous souhaitez enchaîner l'exécution dans le même contexte.

      L’emballage dans un objet StartCoroutinepeut toujours être utile si vous souhaitez exécuter la coroutine imbriquée en association avec un deuxième objet, tel queyield return otherObject.StartCoroutine(OtherObjectsCoroutineMethod())

... selon le moment où vous voulez que la coroutine prenne son prochain tour.

Ou yield break;bien arrêter la coroutine avant la fin, comme vous le feriez pour mettre au point return;une méthode conventionnelle.


Existe-t-il un moyen d’utiliser les coroutines pour ne céder que lorsque l’itération prend plus de 20 ms?
DarkDestry

@DarkDestry Vous pouvez utiliser une instance de chronomètre pour chronométrer le temps que vous passez dans votre boucle interne et vous connecter à une boucle externe dans laquelle vous vous rendez avant de réinitialiser le chronomètre et de reprendre la boucle interne.
DMGregory

1
Bonne réponse! Je veux juste ajouter qu’une raison de plus d’utiliser des coroutines est que si vous avez besoin de construire pour webgl, cela fonctionnera, ce qui ne fonctionnera pas si vous utilisez des threads. Assis avec cette migraine dans un projet énorme en ce moment: P
Mikael Högström

Bonne réponse et merci. Je me demandais simplement si je devais arrêter et redémarrer le thread plusieurs fois, j'essaie d'abandonner et de commencer, me
génère une

@flankechen Le démarrage et le démontage des threads sont des opérations relativement coûteuses. Nous préférons donc laisser le thread disponible mais en veille, en utilisant des éléments tels que des sémaphores ou des moniteurs pour signaler que nous avons du travail à faire. Vous souhaitez publier une nouvelle question décrivant votre cas d'utilisation en détail, et les gens peuvent suggérer des moyens efficaces pour y parvenir?
DMGregory

0

Vous pouvez mettre votre calcul lourd dans un autre thread, mais l'API de Unity n'est pas thread-safe, vous devez les exécuter dans le thread principal.

Eh bien, vous pouvez essayer ce package sur Asset Store pour vous aider à utiliser les threads plus facilement. http://u3d.as/wQg Vous pouvez simplement utiliser une seule ligne de code pour démarrer un thread et exécuter l'API Unity en toute sécurité.



0

@DMGregory l'a très bien expliqué.

Le filetage et les coroutines peuvent être utilisés. Ancien pour décharger le thread principal et ensuite pour rendre le contrôle au thread principal. Pousser des charges lourdes sur du fil séparé semble plus raisonnable. Par conséquent, les files d’attente peuvent être ce que vous recherchez.

Il existe un très bon exemple de script JobQueue sur Unity Wiki.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.