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:
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à.
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 == null
appelle 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 myRequest
où myRequest
est une instance WWW , à reprendre une fois que le chargement des données demandées est terminé à partir du Web ou du disque.
yield return otherCoroutine
où otherCoroutine
est une instance Coroutine , à reprendre une fois otherCoroutine
terminé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 StartCoroutine
et 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 StartCoroutine
peut 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.