Le stockage de tous les objets du jeu dans une seule liste est-il une conception acceptable?

24

Pour chaque jeu que j'ai créé, je finis par placer tous mes objets de jeu (balles, voitures, joueurs) dans une seule liste de tableaux, que je boucle pour dessiner et mettre à jour. Le code de mise à jour de chaque entité est stocké dans sa classe.

Je me demandais: est-ce la bonne façon de procéder ou est-ce une manière plus rapide et plus efficace?

Derek
la source
4
Je remarque que vous avez dit "liste de tableaux", en supposant qu'il s'agit de .Net, vous devriez plutôt passer à une liste <T>. C'est presque identique, sauf qu'un List <T> est fort pour le type et à cause de cela, vous n'aurez pas à taper cast chaque fois que vous accédez à un élément. Voici le doc: msdn.microsoft.com/en-us/library/6sh2ey19.aspx
John McDonald

Réponses:

26

Il n'y a pas qu'une seule bonne façon. Ce que vous décrivez fonctionne et est probablement assez rapide pour que cela n'ait pas d'importance car vous semblez demander parce que vous êtes préoccupé par la conception et non parce que cela vous a causé des problèmes de performances. C'est donc une bonne façon, bien sûr.

Vous pouvez certainement le faire différemment, par exemple en conservant une liste homogène pour chaque type d'objet ou en vous assurant que tout dans votre liste hétérogène est regroupé de sorte que vous avez amélioré la cohérence du code en cache ou pour permettre des mises à jour parallèles. Il est difficile de dire si vous verriez une amélioration appréciable des performances en faisant cela sans connaître la portée et l'échelle de vos jeux.

Vous pouvez également prendre en compte les responsabilités de vos entités - il semble que les entités se mettent à jour et se rendent en ce moment - pour mieux suivre le principe de responsabilité unique. Cela peut augmenter la maintenabilité et la flexibilité de vos interfaces individuelles en les découplant les unes des autres. Mais encore une fois, il est difficile pour moi de dire si vous verriez un avantage du travail supplémentaire que cela impliquerait, sachant ce que je sais de vos jeux (ce qui n'est fondamentalement rien).

Je recommanderais de ne pas trop insister à ce sujet et gardez à l'esprit que cela pourrait être un domaine d'amélioration possible si vous remarquez des problèmes de performances ou de maintenabilité.

Josh
la source
16

Comme d'autres l'ont dit, si c'est assez rapide, alors c'est assez rapide. Si vous vous demandez s’il existe un meilleur moyen, la question à vous poser est

Est-ce que je me retrouve à parcourir tous les objets mais à n'opérer que sur un sous-ensemble d'entre eux?

Si vous le faites, cela indique que vous souhaiterez peut-être avoir plusieurs listes contenant uniquement des références pertinentes. Par exemple, si vous parcourez chaque objet de jeu pour les rendre mais que seul un sous-ensemble d'objets doit être rendu, il peut être utile de conserver une liste de rendu distincte uniquement pour les objets qui doivent être rendus.

Cela réduirait le nombre d'objets que vous parcourez et saute les vérifications inutiles car vous savez déjà que tous les objets de la liste doivent être traités.

Vous devez le profiler pour voir si vous obtenez un gain de performances important.

Michael
la source
Je suis d'accord avec cela, j'ai généralement des sous-ensembles de ma liste pour les objets qui doivent être mis à jour à chaque image, et j'ai envisagé de faire de même pour les objets qui rendent. Cependant, lorsque ces propriétés peuvent changer à la volée, la gestion de toutes les listes peut être complexe. Quand et l'objet passe du rendu au non-rendu, ou à la mise à jour au non-mise à jour, et maintenant vous les ajoutez ou les supprimez de plusieurs listes à la fois.
Nic Foster
8

Cela dépend presque entièrement de vos besoins de jeu spécifiques. Il peut parfaitement fonctionner pour un jeu simple, mais échouer sur un système plus complexe. Si cela fonctionne pour votre jeu, ne vous inquiétez pas ou n'essayez pas de le sur-concevoir jusqu'à ce que le besoin s'en fasse sentir.

Quant à savoir pourquoi l'approche simple peut échouer dans certaines situations, il est difficile de résumer cela dans un seul article, car les possibilités sont infinies et dépendent entièrement des jeux eux-mêmes. D'autres réponses ont mentionné, par exemple, que vous souhaitiez peut-être regrouper des objets partageant des responsabilités dans une liste distincte.

C'est l'un des changements les plus courants que vous pourriez apporter en fonction de la conception de votre jeu. Je prendrai la chance de décrire quelques autres exemples (plus complexes), mais n'oubliez pas qu'il existe encore de nombreuses autres raisons et solutions possibles.

Pour commencer, je soulignerai que la mise à jour et le rendu de vos objets de jeu peuvent avoir des exigences différentes dans certains jeux, et doivent donc être traités séparément. Quelques problèmes possibles avec la mise à jour et le rendu des objets de jeu:

Mise à jour des objets de jeu

Voici un extrait de Game Engine Architecture que je recommanderais de lire:

En présence de dépendances inter-objets, la technique de mise à jour progressive décrite ci-dessus doit être un peu ajustée. En effet, les dépendances inter-objets peuvent conduire à des règles contradictoires régissant l'ordre de mise à jour.

En d'autres termes, dans certains jeux, il est possible que les objets dépendent les uns des autres et nécessitent un ordre de mise à jour spécifique. Dans ce scénario, vous devrez peut-être concevoir une structure plus complexe qu'une liste pour stocker les objets et leurs interdépendances.

entrez la description de l'image ici

Rendre les objets du jeu

Dans certains jeux, les scènes et les objets créent une hiérarchie de nœuds parents-enfants et doivent être rendus dans le bon ordre et avec des transformations par rapport à leurs parents. Dans ces cas, vous pourriez avoir besoin d'une structure plus complexe comme un graphique de scène (une structure arborescente) au lieu d'une seule liste:

entrez la description de l'image ici

D'autres raisons peuvent être, par exemple, l'utilisation d'une structure de données de partitionnement spatial pour organiser vos objets d'une manière qui améliore l'efficacité de la suppression des troncs de vue.

David Gouveia
la source
4

La réponse est "oui, ça va." Lorsque j'ai travaillé sur de grandes simulations de fer où les performances n'étaient pas négociables, j'ai été surpris de tout voir dans un tableau global de gros gras (BIG et FAT). Plus tard, j'ai travaillé sur un système OO et j'ai pu comparer les deux. Bien qu'elle soit super moche, la version "un seul tableau pour les gouverner tous" dans un vieux C simple à thread unique compilé dans le débogage était plusieurs fois plus rapide que le "joli" système OO multithread compilé -O2 en C ++. Je pense que cela était en partie dû à l'excellente localité du cache (à la fois dans l'espace et dans le temps) dans la version big-fat-array.

Si vous voulez des performances, un grand tableau C global gros sera la limite supérieure (par expérience).

anon
la source
2

Fondamentalement, ce que vous voulez faire est de créer des listes selon vos besoins. Si vous redessinez des objets de terrain en une seule fois, une liste de ceux-ci pourrait être utile. Mais pour que cela en vaille la peine, il vous en faudrait des dizaines de milliers et des milliers de choses qui ne sont pas du terrain. Sinon, parcourir votre liste de tout et utiliser un "si" pour retirer les objets du terrain est assez rapide.

J'ai toujours eu besoin d'une liste principale, quel que soit le nombre d'autres listes que j'ai eues. Habituellement, j'en fais une carte, un tableau à clés ou un dictionnaire, afin de pouvoir accéder de manière aléatoire à des éléments individuels. Mais une simple liste est beaucoup plus simple; si vous n'accédez pas au hasard aux objets du jeu, vous n'avez pas besoin de la carte. Et la recherche séquentielle occasionnelle de quelques milliers d'entrées pour trouver la bonne ne prendra pas longtemps. Je dirais donc, quoi que vous fassiez d'autre, prévoyez de vous en tenir à la liste principale. Envisagez d'utiliser une carte si elle devient grande. (Bien que si vous pouvez utiliser des index au lieu de clés, vous pouvez vous en tenir aux tableaux et avoir quelque chose de bien meilleur qu'une carte. J'ai toujours eu de gros écarts entre les numéros d'index, donc j'ai dû y renoncer.)

Un mot sur les tableaux: ils sont incroyablement rapides. Mais quand ils récupèrent environ 100 000 éléments, ils commencent à s'adapter aux Garbage Collectors. Si vous pouvez allouer l'espace une fois et ne pas le toucher par la suite, c'est bien. Mais si vous étendez continuellement le tableau, vous allouez et libérez continuellement de gros morceaux de mémoire et vous êtes susceptible de faire bloquer votre jeu pendant quelques secondes à la fois et même d'échouer avec une erreur de mémoire.

Si rapide et plus efficace? Sûr. Une carte pour un accès aléatoire. Pour les très grandes listes, une liste liée battra un tableau si et quand vous n'avez pas besoin d'un accès aléatoire. Plusieurs cartes / listes pour organiser les objets de différentes manières, selon vos besoins. Vous pouvez multi-threader la mise à jour.

Mais c'est beaucoup de travail. Ce tableau unique est rapide et simple. Vous ne pouvez ajouter d'autres listes et éléments qu'en cas de besoin, et vous n'en aurez probablement pas besoin. Sachez simplement ce qui peut mal tourner si votre jeu devient gros. Vous voudrez peut-être avoir une version de test avec 10 fois plus d'objets que dans la version réelle pour vous donner une idée du moment où vous vous dirigez vers des problèmes. (Ne faites cela que si votre jeu est commence à être grande.) (Et ne pas essayer le multi-threading sans lui donner beaucoup de pensée. Tout le reste est facile de démarrer avec et vous pouvez faire autant ou aussi peu que vous aimez et reculer à tout moment. Avec le multi-thread, vous paierez un prix élevé pour entrer, les frais de séjour sont élevés et il est difficile de sortir.)

En d'autres termes, continuez à faire ce que vous faites. Votre plus grande limite est probablement votre propre temps, et vous en faites le meilleur usage possible.

RalphChapin
la source
Je ne pense vraiment pas qu'une liste liée surpasse un tableau de n'importe quelle taille, sauf si vous ajoutez ou supprimez souvent des éléments du milieu.
Zan Lynx
@ZanLynx: Et même alors. Je pense que les nouvelles optimisations d'exécution aident beaucoup les tableaux - pas qu'ils en aient besoin. Mais si vous allouez et libérez de gros morceaux de mémoire de manière répétée et rapide, comme cela peut arriver avec de grands tableaux, vous pouvez attacher le ramasse-miettes en nœuds. (La quantité totale de mémoire est également un facteur ici. Le problème est fonction de la taille des blocs, de la fréquence de libération et d'allocation et de la mémoire disponible.) L'échec se produit soudainement, le programme se bloquant ou se bloquant. Il y a généralement beaucoup de mémoire libre; le GC ne peut tout simplement pas l'utiliser. Les listes liées évitent ce problème.
RalphChapin
D'un autre côté, les listes chaînées ont une probabilité significativement plus élevée de mettre en cache les erreurs lors de l'itération en raison du fait que les nœuds ne sont pas contigus en mémoire. Ce n'est pas si simple et sec. Vous pouvez atténuer les problèmes de réallocation en ne faisant pas cela (l'allocation initiale si possible, car c'est souvent le cas) et vous pouvez atténuer les problèmes de cohérence du cache dans une liste chaînée en regroupant les nœuds (bien que vous ayez toujours le coût de suivi de la référence , c'est relativement banal).
Josh
Oui, mais même dans un tableau, ne stockez-vous pas seulement des pointeurs vers des objets? (À moins qu'il ne s'agisse d'un tableau de courte durée pour un système de particules ou quelque chose du genre.) Si c'est le cas, il y aura encore beaucoup de ratés de cache.
Todd Lehman
1

J'ai utilisé une méthode quelque peu similaire pour les jeux simples. Tout stocker dans un tableau est très utile lorsque les conditions suivantes sont remplies:

  1. Vous avez un petit nombre ou un maximum connu d'objets de jeu
  2. Vous privilégiez la simplicité à la performance (c'est-à-dire: des structures de données plus optimisées telles que les arbres ne valent pas la peine)

Dans tous les cas, j'ai implémenté cette solution comme un tableau de taille fixe qui contient une gameObject_ptrstructure. Cette structure contenait un pointeur sur l'objet de jeu lui-même et une valeur booléenne représentant si l'objet était "vivant" ou non.

L'objectif du booléen est d'accélérer les opérations de création et de suppression. Au lieu de détruire les objets du jeu lorsqu'ils sont retirés de la simulation, je définirais simplement le drapeau "vivant" sur false.

Lorsque vous effectuez des calculs sur des objets, le code passe en boucle dans le tableau, effectuant des calculs sur des objets marqués comme "vivants", et seuls les objets marqués "vivants" sont rendus.

Une autre optimisation simple consiste à organiser le tableau par type d'objet. Toutes les puces, par exemple, pourraient vivre dans les n dernières cellules du tableau. Ensuite, en stockant un int avec la valeur de l'index ou un pointeur vers l'élément de tableau, des types spécifiques pourraient être référencés à un coût réduit.

Il va sans dire que cette solution n'est pas bonne pour les jeux avec de grandes quantités de données, car le tableau sera trop grand. Il n'est pas non plus adapté aux jeux avec des contraintes de mémoire qui interdisent aux objets inutilisés de prendre de la mémoire. L'essentiel est que si vous avez une limite supérieure connue sur le nombre d'objets de jeu que vous aurez à un moment donné, il n'y a rien de mal à les stocker dans un tableau statique.

Ceci est mon premier message posté! J'espère que cela répond à votre question!

blz
la source
premier poste! Yay!
Tili
0

C'est acceptable, quoique inhabituel. Des termes comme «plus rapide» et «plus efficace» sont relatifs; En règle générale, vous organisez les objets de jeu en catégories distinctes (donc une liste pour les balles, une liste pour les ennemis, etc.) pour rendre la programmation plus organisée (et donc plus efficace), mais ce n'est pas comme si l'ordinateur parcourait tous les objets plus rapidement .

jhocking
la source
2
C'est vrai pour la plupart des jeux XNA, mais l'organisation de vos objets peut en fait accélérer leur itération: les objets du même type sont plus susceptibles d'atteindre le cache d'instructions et de données de votre CPU. Les erreurs de cache sont coûteuses, en particulier sur les consoles. Sur la Xbox 360 par exemple, un échec de cache L2 vous coûtera environ 600 cycles. Cela laisse beaucoup de performances sur la table. C'est un endroit où le code managé a un avantage sur le code natif: les objets alloués les uns aux autres dans le temps seront également proches en mémoire, et la situation s'améliore avec le garbage collection (compactage).
Programmeur de jeux chroniques du
0

Vous dites tableau unique et objets.

Donc (sans votre code à regarder), je suppose qu'ils sont tous liés à 'uberGameObject'.

Alors peut-être deux problèmes.

1) Vous pouvez avoir un objet «dieu» - fait des tonnes de choses, pas vraiment segmenté par ce qu'il fait ou est.

2) Vous parcourez cette liste et le casting pour taper? ou déballer chacun. Cela a une grosse pénalité de performance.

envisager de diviser différents types (puces, méchants, etc.) en leurs propres listes fortement typées.

List<BadGuyObj> BadGuys = new List<BadGuyObj>();
List<BulletsObj> Bullets = new List<BulletObj>();

 //Game 
OnUpdate(gameticks gt)
{  foreach (BulletObj thisbullet in Bullets) {thisbullet.Update();}  // much quicker.
Dr9
la source
Il n'est pas nécessaire d'effectuer un transtypage de type s'il existe une sorte de classe de base ou d'interface commune contenant toutes les méthodes qui seront appelées dans la boucle de mise à jour (par exemple update()).
bummzack
0

Je peux proposer un compromis entre la liste unique générale et les listes séparées pour chaque type d'objet.

Gardez la référence à un objet dans plusieurs listes. Ces listes sont définies par des comportements de base. Par exemple, l' MarineSoldierobjet sera référencé dans PhysicalObjList, GraphicalObjList, UserControlObjList, HumanObjList. Ainsi, lorsque vous traitez la physique, vous parcourez PhysicalObjList, lorsque le processus endommage le poison - HumanObjList, si le soldat meurt - enlevez-le, HumanObjListmais continuez PhysicalObjList. Pour que ce mécanisme fonctionne, vous devez organiser l’insertion / suppression automatique des objets lors de leur création / suppression.

Avantages de l'approche:

  • organisation typée des objets (non AllObjectsListavec casting lors de la mise à jour)
  • les listes sont basées sur des logiques, pas sur des classes d'objets
  • aucun problème avec les objets avec des capacités combinées ( MegaAirHumanUnderwaterObjectserait inséré dans les listes correspondantes au lieu de créer une nouvelle liste pour son type)

PS: Quant à la mise à jour automatique, vous rencontrerez un problème lorsque plusieurs objets interagissent et chacun d'entre eux dépend des autres. Pour résoudre ce problème, nous devons affecter l'objet à l'extérieur, et non à l'intérieur de la mise à jour automatique.

brigadir
la source