Synchronisation entre le fil logique de jeu et le fil de rendu


16

Comment séparer la logique du jeu et le rendu? Je sais qu'il semble y avoir déjà des questions sur ce point, mais les réponses ne me satisfont pas.

D'après ce que je comprends jusqu'à présent, le point de les séparer en différents threads est que la logique du jeu puisse commencer à s'exécuter pour le prochain tick immédiatement au lieu d'attendre le prochain vsync où le rendu revient finalement de l'appel du swapbuffer.

Mais spécifiquement quelles structures de données sont utilisées pour empêcher les conditions de concurrence entre le thread logique de jeu et le thread de rendu. Vraisemblablement, le thread de rendu a besoin d'accéder à diverses variables pour savoir quoi dessiner, mais la logique du jeu pourrait mettre à jour ces mêmes variables.

Existe-t-il une technique standard de facto pour gérer ce problème? Peut-être comme copier les données nécessaires au thread de rendu après chaque exécution de la logique du jeu. Quelle que soit la solution, le surcoût de la synchronisation ou quoi que ce soit sera-t-il inférieur à la simple exécution de tout un seul thread?


1
Je déteste simplement spammer un lien, mais je pense que c'est une très bonne lecture et que cela devrait répondre à toutes vos questions: altdevblogaday.com/2011/07/03/threading-and-your-game-loop
Roy T.


1
Ces liens donnent le résultat final typique que l'on aimerait, mais ne précisent pas comment le faire. Souhaitez-vous copier le graphique de scène entier à chaque image ou autre chose? Les discussions sont trop élevées et trop vagues.
user782220

Je pensais que les liens étaient assez explicites sur la quantité d'état copiée dans chaque cas. par exemple. (à partir du 1er lien) "Un lot contient toutes les informations nécessaires pour dessiner un cadre, mais ne contient aucun autre état de jeu." ou (à partir du 2e lien) "Les données doivent toujours être partagées cependant, mais maintenant, au lieu que chaque système accède à un emplacement de données commun pour dire, obtenir des données de position ou d'orientation, chaque système a sa propre copie" (Voir en particulier 3.2.2 - État Manager)
DMGregory

Celui qui a écrit cet article Intel ne semble pas savoir que le threading de haut niveau est une très mauvaise idée. Personne ne fait quelque chose d'aussi stupide. Du coup, toute l'application doit communiquer par des canaux spécialisés et il y a des verrous et / ou d'énormes échanges d'état coordonnés partout. Sans oublier que personne ne sait quand les données envoyées seront traitées, il est donc extrêmement difficile de raisonner sur ce que fait le code. Il est beaucoup plus facile de copier les données de scène pertinentes (immuables en tant que pointeurs comptés par référence, modifiables - par valeur) à un moment donné et de laisser le sous-système les trier comme il le souhaite.
snake5

Réponses:


1

J'ai travaillé sur la même chose. La préoccupation supplémentaire est qu'OpenGL (et à ma connaissance, OpenAL), et un certain nombre d'autres interfaces matérielles, sont en fait des machines à états qui ne s'entendent pas bien avec l'appel de plusieurs threads. Je ne pense pas que leur comportement soit même défini, et pour LWJGL (peut-être aussi JOGL), il lève souvent une exception.

J'ai fini par créer une séquence de threads qui implémentait une interface spécifique et les charger dans la pile d'un objet de contrôle. Lorsque cet objet recevait un signal pour arrêter le jeu, il parcourait chaque thread, appelait une méthode ceaseOperations () implémentée et attendait leur fermeture avant de se fermer. Les données universelles pouvant être pertinentes pour le rendu du son, des graphiques ou de toute autre donnée sont conservées dans une séquence d'objets volatils ou universellement disponibles pour tous les threads mais jamais conservées dans la mémoire des threads. Il y a une légère pénalité de performance, mais utilisée correctement, elle m'a permis d'assigner de manière flexible l'audio à un thread, les graphiques à un autre, la physique à un autre, et ainsi de suite sans les lier à la "boucle de jeu" traditionnelle (et redoutée).

Donc, en règle générale, tous les appels OpenGL passent par le thread graphique, tous les OpenAL par le thread audio, toutes les entrées par le thread d'entrée, et tout ce dont le thread de contrôle d'organisation doit se soucier est la gestion des threads. L'état du jeu est détenu dans la classe GameState, qu'ils peuvent tous consulter comme ils le souhaitent. Si jamais je décide que, disons, JOAL est devenu daté et que je souhaite utiliser la nouvelle édition de JavaSound à la place, je viens d'implémenter un thread différent pour Audio.

J'espère que vous voyez ce que je dis, j'ai déjà quelques milliers de lignes sur ce projet. Si vous souhaitez que j'essaye de rassembler un échantillon, je verrai ce que je peux faire.


Le problème auquel vous serez éventuellement confronté est que cette configuration ne s'adapte pas particulièrement bien sur une machine multicœur. Oui, certains aspects d'un jeu sont généralement mieux servis dans leur propre thread, comme l'audio, mais une grande partie du reste de la boucle de jeu peut en fait être gérée en série conjointement avec des tâches de pool de threads. Si votre pool de threads prend en charge les masques d'affinité, vous pouvez facilement mettre en file d'attente, par exemple, des tâches de rendu à exécuter sur le même thread et demander à votre planificateur de threads de gérer les files d'attente de travail des threads et d'effectuer le vol selon les besoins, ce qui vous permet de prendre en charge plusieurs threads et plusieurs cœurs.
Naros

1

Habituellement, la logique qui traite des passes de rendu graphique (et leur calendrier, et quand elles vont s'exécuter, etc.) est gérée par un thread séparé. Cependant, ce thread est déjà implémenté (opérationnel) par la plate-forme que vous utilisez pour développer votre boucle de jeu (et votre jeu).

Donc, pour obtenir une boucle de jeu où la logique de jeu se met à jour indépendamment du calendrier de rafraîchissement graphique, vous n'avez pas besoin de créer de threads supplémentaires, il vous suffit de puiser dans le thread déjà existant pour lesdites mises à jour graphiques.

Cela dépend de la plateforme que vous utilisez. Par exemple:

  • si vous le faites dans la plupart des plates-formes liées à Open GL ( GLUT pour C / C ++ , JOLG pour Java , Action liée à OpenGL ES d'Android ), elles vous donneront généralement une méthode / fonction qui est périodiquement appelée par le thread de rendu, et que vous peut s'intégrer dans votre boucle de jeu (sans faire dépendre les itérations du gameloop du moment où cette méthode est appelée). Pour GLUT utilisant C, vous faites quelque chose comme ceci:

    glutDisplayFunc (myFunctionForGraphicsDrawing);

    glutIdleFunc (myFunctionForUpdatingState);

  • en JavaScript, vous pouvez utiliser Web Workers car il n'y a pas de multi-threading (que vous pouvez atteindre par programmation) , vous pouvez également utiliser le mécanisme "requestAnimationFrame" pour être averti lorsqu'un nouveau rendu graphique sera planifié, et effectuez les mises à jour de votre état de jeu en conséquence .

Fondamentalement, ce que vous voulez, c'est une boucle de jeu à étapes mixtes: vous avez un code qui met à jour l'état du jeu, et qui est appelé dans le thread principal de votre jeu, et vous souhaitez également exploiter périodiquement (ou être rappelé par) le déjà fil de rendu graphique existant pour savoir quand il est temps de rafraîchir les graphiques.


0

En Java, il y a le mot-clé "synchronized", qui verrouille les variables que vous lui passez pour les rendre threadsafe. En C ++, vous pouvez réaliser la même chose en utilisant Mutex. Par exemple:

Java:

synchronized(a){
    //code using a
}

C ++:

mutex a_mutex;

void f(){
    a_mutex.lock();
    //code using a
    a_mutex.unlock();
}

Le verrouillage des variables garantit qu'elles ne changent pas lors de l'exécution du code qui le suit, afin que les variables ne soient pas modifiées par votre thread de mise à jour pendant que vous les rendez (en fait, elles changent, mais du point de vue de votre thread de rendu, elles ne le font pas '' t). Vous devez cependant faire attention au mot-clé synchronisé en Java, car il s'assure uniquement que le pointeur vers la variable / Object ne change pas. Les attributs peuvent toujours changer sans changer le pointeur. Pour envisager cela, vous pouvez copier l'objet vous-même ou appeler synchronisé sur tous les attributs de l'objet que vous ne souhaitez pas modifier.


1
Les mutex ne sont pas nécessairement la réponse ici parce que l'OP aurait non seulement besoin de découpler la logique et le rendu du jeu, mais ils veulent également éviter tout blocage de la capacité d'un thread à avancer dans son traitement, quel que soit l'endroit où l'autre thread se trouve actuellement dans son traitement. boucle.
Naros

0

Ce que j'ai généralement vu pour gérer la communication logique / fil de rendu est de tripler la mémoire tampon de vos données. De cette façon, le thread de rendu a dit le compartiment 0 à partir duquel il lit. Le thread logique utilise le compartiment 1 comme source d'entrée pour la trame suivante et écrit les données de trame dans le compartiment 2.

Aux points de synchronisation, les indices de la signification de chacun des trois compartiments sont échangés de sorte que les données de la trame suivante soient transmises au thread de rendu et que le thread logique puisse continuer.

Mais il n'y a pas nécessairement de raison de diviser le rendu et la logique en leurs threads respectifs. Vous pouvez en fait conserver la boucle de jeu en série et découpler votre fréquence d'images de rendu de l'étape logique en utilisant l'interpolation. Pour tirer parti des processeurs multicœurs utilisant ce type de configuration, vous disposez d'un pool de threads qui fonctionne sur des groupes de tâches. Ces tâches peuvent être simplement des choses telles que plutôt que d'itérer une liste d'objets de 0 à 100, vous itérez la liste en 5 compartiments de 20 sur 5 threads augmentant efficacement vos performances mais sans compliquer la boucle principale.


0

Ceci est un ancien message mais il apparaît toujours, alors je voulais ajouter mes 2 cents ici.

Liste des premières données qui doivent être stockées dans le thread d'interface utilisateur / d'affichage vs thread logique. Dans le fil d'interface utilisateur, vous pouvez inclure un maillage 3D, des textures, des informations sur la lumière et une copie des données de position / rotation / direction.

Dans le thread de logique de jeu, vous pourriez avoir besoin de la taille de l'objet de jeu en 3D, des primitives de délimitation (sphère, cube), des données de maillage 3D simplifiées (pour les collisions détaillées par exemple), tous les attributs affectant le mouvement / le comportement, comme la vitesse de l'objet, le taux de rotation, etc., et également des données de position / rotation / direction.

Si vous comparez deux listes, vous pouvez voir que seule une copie des données de position / rotation / direction doit être passée de la logique au thread d'interface utilisateur. Vous pourriez également avoir besoin d'une sorte d'ID de corrélation pour déterminer à quel objet de jeu ces données appartiennent.

La façon dont vous le faites dépend de la langue avec laquelle vous travaillez. Dans Scala, vous pouvez utiliser la mémoire transactionnelle logicielle, en Java / C ++ une sorte de verrouillage / synchronisation. J'aime les données immuables, j'ai donc tendance à retourner un nouvel objet immuable pour chaque mise à jour. C'est un peu de gaspillage de mémoire, mais avec les ordinateurs modernes, ce n'est pas si grave. Néanmoins, si vous souhaitez verrouiller des structures de données partagées, vous pouvez le faire. Consultez la classe Exchanger en Java, l'utilisation de deux tampons ou plus peut accélérer les choses.

Avant de partager des données entre des threads, déterminez la quantité de données que vous devez réellement transmettre. Si vous avez un octree partitionnant votre espace 3D et que vous pouvez voir 5 objets de jeu sur 10 objets au total, même si votre logique doit mettre à jour les 10, vous devez redessiner uniquement les 5 que vous voyez. Pour plus de lecture, consultez ce blog: http://gameprogrammingpatterns.com/game-loop.html Il ne s'agit pas de synchronisation, mais il montre comment la logique du jeu est séparée de l'affichage et quels défis vous devez surmonter (FPS). J'espère que cela t'aides,

marque

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.