Comment un jeu peut-il gérer tous les personnages à la fois?

31

Cette question est juste de savoir comment un jeu peut gérer autant de personnages à la fois. Je suis nouveau dans le jeu, je vous demande pardon à l'avance.

Exemple

Je crée un jeu de tower defense dans lequel il y a 15 emplacements de tours où les tours sont construites et chaque tour éjecte le projectile à une certaine vitesse; disons que chaque seconde, 2 projectiles sont créés par chacune des tours et qu'il y a des ennemis marchant sur le champ de bataille, disons 70 (chacun avec 10 types d'attributs comme HP, mana, etc., qui changeront en se déplaçant autour du champ de bataille).

Sommaire

Tour Count = 15
Projectiles Créé par chaque tour par seconde = 2
Nombre total de Projectiles Créé par seconde = 30
unités dans Battlefield Count = 70

Maintenant, le jeu gère-t-il ces 30 projectiles et 70 unités en les manipulant sur 100 threads différents (ce qui est trop pour un PC) ou 1 thread qui les déplace tous, réduit leur valeur, etc. (ce qui sera un peu lent , Je pense)?

Je n'ai aucune idée à ce sujet, alors quelqu'un peut-il me guider sur la façon dont cela fonctionnera?

Développeur Nation
la source
Les commentaires ne sont pas pour une discussion prolongée; cette conversation a été déplacée pour discuter .
MichaelHouse
Ajout aux autres réponses ... un exemple de certains jeux massifs. Skyrim avait la plupart de sa mise à jour de la logique de jeu sur un seul thread. La façon dont il gère si bien cela est que les PNJ distants (PNJ qui sont à des kilomètres) sont approximés selon leur calendrier. La plupart des MMO mettent à jour la logique du jeu sur un seul thread, MAIS chaque partie d'une carte existe sur un thread différent ou un rack de serveur.
moonshineTheleocat

Réponses:

77

Maintenant, comment le jeu gère-t-il ces 30 projectiles et 70 unités en les manipulant sur 100 threads différents

Non, ne fais jamais ça. Ne créez jamais un nouveau thread par ressource, cela ne s'adapte pas au réseau, ni à la mise à jour des entités. (Quelqu'un se souvient-il des moments où vous aviez un thread pour lire par socket en Java?)

1 fil qui les déplace tous réduit leur valeur, etc.?

Oui, pour commencer, c'est la voie à suivre. Les "gros moteurs" répartissent le travail entre les threads, mais cela n'est pas nécessaire pour démarrer un jeu simple comme un jeu de tower defense. Il y a probablement encore plus de travail à faire à chaque tick que vous ferez également dans ce fil. Oh ouais, et le rendu bien sûr.

(ce qui sera un peu lent je pense)

Eh bien ... Quelle est votre définition de lent ? Pour 100 entités, cela ne devrait pas prendre plus d'une demi-milliseconde, probablement même moins, selon la qualité de votre code et la langue avec laquelle vous travaillez. Et même si cela prend deux millisecondes complètes, c'est quand même assez bon pour atteindre les 60 tps (ticks par secondes, sans parler des frames dans ce cas).

tkausl
la source
34
Plus comme une demi-microseconde, sauf si vous faites quelque chose de bizarre. Mais plus important encore, la division du travail sur plusieurs threads va tout aggraver , pas mieux. Sans oublier que le multithreading est extrêmement difficile.
Luaan
13
+1 La plupart des moteurs de jeu modernes affichent des milliers, voire des dizaines de milliers de polygones en temps réel, ce qui est beaucoup plus intensif que le suivi du mouvement de seulement 100 objets en mémoire.
phyrfox
1
"Un jeu simple comme un jeu de tower defense." Hmm ... avez-vous déjà joué à Defense Grid: The Awakening , ou sa suite?
Mason Wheeler
4
"Ne créez jamais un nouveau thread par ressource, cela ne se transforme pas en réseau ..." ahem certaines architectures très évolutives font précisément cela!
NPSF3000
2
@BarafuAlbino: C'est une chose étrange à dire. Il existe de nombreuses raisons valables pour créer plus de threads que de cœurs disponibles. C'est un compromis complexité / performances / etc. comme toute autre décision de conception.
Dietrich Epp du
39

La règle numéro un du multithreading est la suivante: ne l'utilisez pas sauf si vous devez paralléliser sur plusieurs cœurs de processeur pour des performances ou une réactivité. Une exigence "x et y doivent se produire simultanément du point de vue des utilisateurs" n'est pas encore une raison suffisante pour utiliser le multithreading.

Pourquoi?

Le multithreading est difficile. Vous n'avez aucun contrôle sur le moment où chaque thread est exécuté, ce qui peut entraîner toutes sortes de problèmes impossibles à reproduire («conditions de concurrence»). Il existe des méthodes pour éviter cela (verrous de synchronisation, sections critiques), mais celles-ci s'accompagnent de leur propre ensemble de problèmes ("blocages").

Habituellement, les jeux qui traitent un aussi petit nombre d'objets que quelques centaines (oui, ce n'est pas beaucoup dans le développement de jeux) les traitent généralement en série chaque tick logique en utilisant une forboucle commune .

Même les processeurs de smartphone relativement plus faibles peuvent exécuter des milliards d'instructions par seconde. Cela signifie que même lorsque la logique de mise à jour de vos objets est complexe et prend environ 1000 instructions par objet et tick, et que vous visez 100 généreuses ticks par seconde, vous avez une capacité CPU suffisante pour des dizaines de milliers d'objets. Oui, il s'agit d'un calcul au dos de l'enveloppe extrêmement simplifié, mais il vous donne une idée.

En outre, la sagesse courante dans le développement de jeux est que les logiques de jeu sont très rarement le goulot d'étranglement d'un jeu. La partie critique des performances est presque toujours le graphisme. Oui, même pour les jeux 2D.

Philipp
la source
1
"La règle numéro un du multithreading est: ne l'utilisez pas sauf si vous devez paralléliser sur plusieurs cœurs de processeur pour des performances ou une réactivité." Cela pourrait être vrai pour le développement de jeux (mais j'en doute même). Lorsque vous travaillez avec des systèmes en temps réel, la principale raison d'ajouter des threads est de respecter les délais et pour une simplicité logique.
Sam
6
@Sam Respecter les délais dans les systèmes en temps réel est un cas où vous avez besoin de multithreading pour la réactivité. Mais même là, la simplicité logique que vous atteignez apparemment à travers le filetage est souvent perfide, car elle crée une complexité cachée sous forme de blocages, de conditions de concurrence et de famine.
Philipp
Malheureusement, plusieurs fois, j'ai vu la logique du jeu s'embourber dans tout le jeu s'il y avait des problèmes de repérage.
Loren Pechtel
1
@ LorenPechtel J'ai vu ça aussi. Mais il était généralement possible de le résoudre en ne faisant pas de calculs de chemin inutiles (comme recalculer chaque chemin à chaque tick), en mettant en cache les chemins fréquemment demandés, en utilisant la recherche de chemin à plusieurs niveaux et en utilisant des algorithmes de recherche de chemin plus appropriés. C'est quelque chose où un programmeur qualifié peut généralement trouver beaucoup de potentiel d'optimisation.
Philipp
1
@LorenPechtel Par exemple, dans un jeu de tower defense, vous pouvez utiliser le fait qu'il n'y a généralement qu'une poignée de points de destination. Vous pouvez donc exécuter l'algorithme de Dijkstra pour chaque destination pour calculer une carte de direction qui guide toutes les unités. Même dans un environnement dynamique où vous devez recalculer ces cartes à chaque image, cela devrait toujours être abordable.
CodesInChaos
26

Les autres réponses ont géré le filetage et la puissance des ordinateurs modernes. Pour répondre à la plus grande question cependant, ce que vous essayez de faire ici est d'éviter les situations "n au carré".

Par exemple, si vous avez 1000 projectiles et 1000 ennemis, la solution naïve consiste à simplement les comparer les uns aux autres.

Cela signifie que vous vous retrouvez avec p * e = 1 000 * 1 000 = 1 000 000 de contrôles différents! C'est O (n ^ 2).

D'un autre côté, si vous organisez mieux vos données, vous pouvez éviter beaucoup de cela.

Par exemple, si vous listez sur chaque carré de la grille quels ennemis se trouvent dans cette case, vous pouvez parcourir vos 1000 projectiles et vérifier simplement le carré sur la grille. Maintenant, il vous suffit de vérifier chaque projectile par rapport au carré, c'est O (n). Au lieu d'un million de chèques par trame, vous n'en avez besoin que de mille.

Penser à organiser vos données et à les traiter efficacement grâce à cette organisation est la plus grande optimisation que vous puissiez faire.

Tim B
la source
1
Comme alternative au stockage de la grille entière en mémoire juste pour suivre quelques éléments, vous pouvez également utiliser des arbres b, un pour chaque axe, pour rechercher rapidement parmi les candidats possibles aux collisions, etc. Certains moteurs le font même pour vous "automatiquement "; vous spécifiez les régions touchées et demandez une liste des collisions, et la bibliothèque vous la donne. C'est l'une des nombreuses raisons pour lesquelles les développeurs devraient utiliser un moteur au lieu d'écrire à partir de zéro (si possible, bien sûr).
phyrfox
@phyrfox Certes, il existe un certain nombre de façons différentes de le faire - en fonction de votre cas d'utilisation, le meilleur variera considérablement.
Tim B
17

Ne créez pas de threads par ressource / objet mais par section de la logique de votre programme. Par exemple:

  1. Fil pour mettre à jour les unités et les projectiles - fil logique
  2. Fil pour le rendu de l'écran - fil GUI
  3. Thread for network (eg. Multijoueur) - thread IO

L'avantage de ceci est que votre interface graphique (par exemple des boutons) ne se bloque pas nécessairement si votre logique est lente. L'utilisateur peut toujours mettre en pause et enregistrer la partie. C'est aussi bon pour préparer votre jeu pour le multijoueur, maintenant que vous séparez le graphique de la logique.

Tomáš Zato - Réintégrer Monica
la source
1
Pour un débutant, je ne recommanderais pas d'utiliser des threads graphiques et logiques distincts, car à moins que vous ne copiez les données requises, le rendu de l'état du jeu nécessite un accès en lecture à l'état du jeu, vous ne pouvez donc pas modifier l'état du jeu tout en le dessinant.
CodesInChaos
1
Ne pas dessiner trop souvent (par exemple plus de 50 fois par seconde) est un peu important et cette question portait sur les performances. Le programme de division est la chose la plus simple à faire pour un réel avantage de performance. C'est vrai que cela nécessite des connaissances sur les threads, mais l'acquisition de ces connaissances en vaut la peine.
Tomáš Zato - Reinstate Monica
La division d'un programme en plusieurs threads est en quelque sorte la chose la plus difficile à faire pour un programmeur. La plupart vraiment bugs ennuyeux provenir de multi-threading et il est une quantité gigantesque de tracas et la plupart du temps juste pas la peine - Première règle: Vérifiez si vous avez un problème de performance, ALORS optimize. Et optimisez là où se trouve le goulot d'étranglement. Peut-être un seul thread externe pour un certain algorithme complexe. Mais même alors, vous devez penser à la façon dont votre logique de jeu avancera lorsque cet algorithme prendra 3 secondes pour terminer ...
Falco
@Falco Vous supervisez les avantages à long terme de ce modèle - à la fois pour le projet et pour l'expérience du programmeur. Votre affirmation selon laquelle il est le plus difficile de penser ne peut pas vraiment être abordée, ce n'est qu'une opinion. Pour moi, la conception d'interface graphique est beaucoup plus terrifiante. Tous les langages évolués (C ++, Java) ont des modèles de multithreading assez clairs. Et si vous n'êtes vraiment pas sûr, vous pouvez utiliser un modèle d'acteur qui ne souffre pas de bogues de multithreading débutant. Vous savez qu'il y a une raison pour laquelle la plupart des applications sont conçues comme je l'ai proposé, mais n'hésitez pas à en discuter davantage.
Tomáš Zato - Reinstate Monica
4

Même Space Invaders a géré des dizaines d'objets en interaction. Alors que le décodage d'une image de vidéo HD H264 implique des centaines de millions d'opérations arithmétiques. Vous disposez d'une grande puissance de traitement.

Cela dit, vous pouvez toujours ralentir si vous le gaspillez. Le problème n'est pas tant le nombre d'objets que le nombre de tests de collision effectués; l'approche simple consistant à comparer chaque objet les uns par rapport aux autres équivaut au nombre de calculs requis. Tester 1001 objets pour des collisions de cette façon nécessiterait un million de comparaisons. Souvent, ceci est résolu par exemple en ne vérifiant pas les projectiles pour une collision les uns avec les autres.

pjc50
la source
2
Je ne suis pas sûr que Space Invaders soit la meilleure comparaison à faire. La raison pour laquelle cela commence lentement et accélère lorsque vous tuez des ennemis n'est pas parce qu'il a été conçu de cette façon, mais parce que le matériel ne pouvait pas gérer autant d'ennemis à la fois. en.wikipedia.org/wiki/Space_Invaders#Hardware
Mike Kellogg
Qu'en est-il, chaque objet conserve une liste de tous les objets suffisamment proches pour qu'il puisse entrer en collision avec eux dans la seconde suivante, mis à jour une fois par seconde ou chaque fois qu'ils changent de direction?
Random832
Cela dépend de ce que vous modélisez. Les solutions de partitionnement d'espace sont une autre approche courante: partitionnez le monde en régions (par exemple BSP que vous devrez peut-être faire de toute façon à des fins de rendu, ou quadtree), alors vous ne pouvez entrer en collision qu'avec des objets dans la même région.
pjc50 du
3

Je vais être en désaccord avec certaines des autres réponses ici. Les threads logiques séparés sont non seulement une bonne idée, mais extrêmement bénéfiques pour la vitesse de traitement - si votre logique est facilement séparable .

Votre question est un bon exemple de logique qui est probablement séparable si vous pouvez ajouter un peu de logique supplémentaire par-dessus. Par exemple, vous pouvez exécuter plusieurs threads de détection de hit soit en verrouillant les threads dans des régions spécifiques de l'espace, soit en mutexant les objets impliqués.

Vous ne voulez probablement PAS un thread pour chaque collision possible, juste parce que cela risque de gâcher le planificateur; il existe également un coût associé à la création et à la destruction de threads. Mieux vaut faire un certain nombre de threads autour des cœurs du système (ou utiliser une métrique comme l'ancienne #cores * 2 + 4), puis les réutiliser lorsque leur processus se termine.

Cependant, toute logique n'est pas facilement séparable. Parfois, vos opérations peuvent atteindre simultanément toutes les données de jeu, ce qui rendrait le threading inutile (en fait, dangereux, car vous auriez besoin d'ajouter des vérifications pour éviter les problèmes de threading). De plus, si plusieurs étapes de la logique dépendent fortement les unes des autres se produisant dans des commandes spécifiques, vous devrez contrôler l'exécution des threads de manière à garantir que cela ne donne pas de résultats dépendants de la commande. Cependant, ce problème n'est pas éliminé en n'utilisant pas de threads, les threads l'exacerbent simplement.

La plupart des jeux ne le font pas simplement parce que c'est plus complexe que le développeur de jeux moyen ne veut / ne peut gérer ce qui n'est généralement pas le goulot d'étranglement en premier lieu. La grande majorité des jeux sont limités en GPU et non en CPU. Bien que l'amélioration de la vitesse du processeur puisse globalement aider, ce n'est généralement pas l'objectif.

Cela dit, les moteurs physiques utilisent souvent plusieurs threads, et je peux nommer plusieurs jeux qui, selon moi, auraient bénéficié de plusieurs threads logiques (les jeux Paradox RTS comme HOI3 et autres, par exemple).

Je suis d'accord avec d'autres articles que vous n'auriez probablement pas besoin d'employer des fils dans cet exemple spécifique, même si cela pourrait être bénéfique. Le thread doit être réservé aux cas où vous avez une charge CPU excessive qui ne peut pas être optimisée via d'autres méthodes. C'est une entreprise énorme qui affectera la structure fondamentale d'un moteur; ce n'est pas quelque chose que vous pouvez aborder après coup.


la source
2

Je pense que les autres réponses manquent une partie importante de la question en se concentrant trop sur le filetage de la question.

Un ordinateur ne gère pas du tout tous les objets d'un jeu à la fois. Il les gère en séquence.

Un jeu informatique progresse en pas de temps discrets. Selon le jeu et la vitesse du PC, ces étapes sont généralement de 30 ou 60 pas par seconde, ou autant / peu d'étapes que le PC peut calculer.

Dans une telle étape, un ordinateur calcule ce que chacun des objets de jeu fera pendant cette étape et les met à jour en conséquence, l'un après l'autre. Il pourrait même le faire en parallèle, en utilisant des threads pour être plus rapide, mais comme nous le verrons bientôt, la vitesse n'est pas du tout un problème.

Un processeur moyen doit être de 2 GHz ou plus, ce qui signifie 10 9 cycles d'horloge par seconde. Si l' on calcule 60 par seconde, pas de temps que les feuilles 10 9 /60 cycles d'horloge = 16666666 cycles d'horloge par pas de temps. Avec 70 unités, il nous reste encore environ 2 400 000 cycles d'horloge par unité. Si nous devions optimiser, nous pourrions mettre à jour chaque unité en aussi peu que 240 cycles, selon la complexité de la logique du jeu. Comme vous pouvez le voir, notre ordinateur est environ 10 000 fois plus rapide qu'il ne devrait l'être pour cette tâche.

Peter
la source
0

Avertissement: Mon type de jeu préféré de tous les temps est basé sur le texte et j'écris ceci en tant que programmeur de longue date d'un ancien MUD.

Je pense qu'une question importante que vous devez vous poser est la suivante: avez-vous même besoin de fils? Je comprends qu'un jeu graphique utilise probablement plus de MT, mais je pense que cela dépend aussi de la mécanique du jeu. (Il peut également être utile de considérer qu'avec les GPU, les CPU et toutes les autres ressources que nous avons aujourd'hui sont beaucoup plus puissantes, ce qui rend vos problèmes de ressources aussi problématiques que cela puisse vous paraître; en effet, 100 objets est pratiquement zéro). Cela dépend également de la façon dont vous définissez «tous les caractères à la fois». Voulez-vous dire exactement au même moment? Vous n'aurez pas cela comme Peter le souligne à juste titre, donc tout à coup n'est pas pertinent au sens littéral; cela n'apparaît que de cette façon.

En supposant que vous irez avec des threads: vous ne devriez certainement pas considérer 100 threads (et je ne vais même pas me demander si c'est trop pour votre processeur ou non; je ne parle que des complications et de la praticité de celui-ci).

Mais rappelez-vous ceci: le multi-threading n'est pas facile (comme le souligne Philipp) et a de nombreux problèmes. D'autres ont beaucoup plus d'expérience (par beaucoup) que moi avec MT mais je dirais qu'eux aussi suggéreraient la même chose (même s'ils seraient plus capables que moi - surtout sans pratique de ma part).

Certains soutiennent qu'ils ne sont pas d'accord sur le fait que les threads ne sont pas bénéfiques et certains soutiennent que chaque objet devrait avoir un thread. Mais (et encore une fois, c'est tout du texte, mais même si vous considérez plus d'un thread, vous n'avez pas besoin - et ne devriez pas - le considérer pour chaque objet), car Philipp souligne que les jeux ont tendance à parcourir les listes. Mais ce n'est pas seulement (comme il le suggère bien que je réalise qu'il ne répond qu'à vos paramètres de si peu d'objets) pour si peu d'objets. Dans le MUD, je suis un programmeur car nous avons les éléments suivants (et ce n'est pas toute l'activité qui se produit en temps réel, gardez cela à l'esprit également):

(Le nombre d'instances varie bien sûr - plus haut et plus bas)

Mobiles (PNJ, c'est-à-dire personnage non joueur): 2614; prototypes: 1360 Objets: 4457; prototypes: 2281 chambres: 7983; prototypes: 7983. Chaque pièce a généralement sa propre instance mais nous avons aussi des pièces dynamiques c'est-à-dire des pièces à l'intérieur d'une pièce; ou des pièces à l'intérieur d'un mobile, par exemple l'estomac d'un dragon; ou des pièces dans des objets, par exemple, vous entrez dans un objet magique). Gardez à l'esprit que ces pièces dynamiques existent par objet / pièce / mobile qui les a réellement définies. Oui, cela ressemble beaucoup à l'idée d'instances de World of Warcraft (je ne le joue pas mais un ami me l'a fait jouer quand j'avais une machine Windows, pendant un certain temps) sauf que nous l'avions bien avant que World of Warcraft n'existe.

Scripts: 868 (actuellement) (curieusement, notre commande de statistiques ne montre pas combien de prototypes nous avons donc je vais l'ajouter). Toutes ces activités ont lieu dans des zones / zones et nous en avons 103. Nous avons également des procédures spéciales qui se déclenchent à différents moments. Nous avons également d'autres événements. Ensuite, nous avons également des prises connectées. Les mobiles se déplacent, font différentes activités (outre le combat), interagissent avec les joueurs, etc. (Il en va de même pour les autres types d'entités).

Comment gérons-nous tout cela sans délai?

  • sockets: select (), files d'attente (entrée, sortie, événements, autres), tampons (entrée, sortie, autres), etc. Ceux-ci sont interrogés 10 fois par seconde.

  • personnages, objets, salles, combat, tout: le tout dans une boucle centrale sur différentes impulsions.

Nous avons également (mon implémentation basée sur une discussion entre le fondateur / autre programmeur et moi-même) un vaste suivi de liste liée et des tests de validité de pointeur et nous avons plus qu'assez de ressources gratuites si nous en avons réellement besoin. Tout cela (sauf que nous avons élargi le monde) existait il y a des années quand il y avait moins de RAM, de puissance CPU, d'espace sur le disque dur, etc. Et même alors, nous n'avons eu aucun problème. Dans les boucles décrites (les scripts provoquent cela comme les réinitialisations / repeuplements de zone comme le font d'autres choses), des monstres, des objets (objets) et d'autres choses sont créés, libérés, etc. Les connexions sont également acceptées, interrogées et tout ce que vous attendez.


la source