Comment structurer / gérer au mieux des centaines de personnages «en jeu»

10

J'ai créé un jeu RTS simple, qui contient des centaines de personnages comme Crusader Kings 2, dans Unity. Pour les stocker, l'option la plus simple serait d'utiliser des objets scriptables, mais ce n'est pas une bonne solution car vous ne pouvez pas en créer de nouveaux au moment de l'exécution.

J'ai donc créé une classe C # appelée "Caractère" qui contient toutes les données. Tout fonctionne bien, mais au fur et à mesure que le jeu simule, il crée constamment de nouveaux personnages et tue certains personnages (comme les événements dans le jeu se produisent). Comme le jeu simule en continu, il crée des milliers de personnages. J'ai ajouté une simple vérification pour m'assurer qu'un personnage est "vivant" lors du traitement de sa fonction. Cela améliore donc les performances, mais je ne peux pas supprimer "Personnage" s'il est mort, car j'ai besoin de ses informations lors de la création de l'arbre généalogique.

Une liste est-elle le meilleur moyen de sauvegarder des données pour mon jeu? Ou cela posera des problèmes une fois qu'il y aura 10000s de personnage créé? Une solution possible consiste à créer une autre liste une fois que la liste atteint un certain montant et à y déplacer tous les caractères morts.

paul p
la source
9
Une chose que j'ai faite dans le passé quand j'ai besoin d'un sous-ensemble de données d'un personnage après sa disparition est de créer un objet "tombstone" pour ce personnage. La pierre tombale peut contenir les informations dont j'ai besoin pour chercher plus tard, mais elle peut être plus petite et itérée moins souvent car elle n'a pas besoin d'une simulation constante comme un personnage vivant.
DMGregory
2
Le jeu est-il comme CK2, ou est-ce seulement la partie d'avoir beaucoup de personnages? J'ai compris que tout le jeu ressemblait à CK2. Dans ce cas, beaucoup de réponses ici ne sont pas incorrectes et contiennent un bon savoir-faire, mais elles manquent le point de la question. Cela n'aide pas que vous appeliez CK2 un jeu de stratégie en temps réel alors qu'il s'agit en fait d'un grand jeu de stratégie . Cela peut sembler compliqué, mais c'est très pertinent pour les problèmes auxquels vous êtes confronté.
Raphael Schmitz
1
Par exemple, lorsque vous mentionnez "des milliers de personnages", les gens pensent à des milliers de modèles 3D ou de sprites sur l'écran en même temps - donc, dans Unity, des milliers de GameObjects. Dans CK2, le nombre maximum de personnages que j'ai vus en même temps était quand j'ai regardé ma cour et vu 10-15 personnes là-bas (je n'ai pas joué très loin cependant). Tout aussi bien, une armée de 3000 soldats n'est qu'une GameObject, affichant le nombre "3000".
Raphael Schmitz
1
@ R.Schmitz Oui, j'aurais dû indiquer clairement que chaque personnage n'a pas d'objet de jeu attaché. Chaque fois que nécessaire, comme déplacer le personnage d'un point à un autre. Une entité distincte est créée qui contient toutes les informations de ce personnage avec la logique Ai.
paul p

Réponses:

24

Vous devez considérer trois choses:

  1. Cela cause- t-il réellement un problème de performances? Des milliers, c'est bien peu en fait. Les ordinateurs modernes sont terriblement rapides et peuvent gérer beaucoup de choses. Surveillez le temps que prend le traitement des caractères et voyez si cela va réellement causer un problème avant de trop vous en préoccuper.

  2. Fidélité des personnages actuellement peu actifs. Une erreur fréquente des programmeurs de jeux débutants consiste à obséder pour la mise à jour précise des personnages hors écran de la même manière que ceux à l'écran. C'est une erreur, personne ne s'en soucie. Au lieu de cela, vous devez chercher à créer l' impression que les personnages hors écran agissent toujours. En réduisant la quantité de caractères de mise à jour reçus hors écran, vous pouvez réduire considérablement les temps de traitement.

  3. Envisagez la conception orientée données. Au lieu d'avoir 1000 objets de caractère et d'appeler la même fonction pour chacun, ayez un tableau de données pour les 1000 caractères et une boucle de fonction sur les 1000 caractères se mettant à jour à tour de rôle. Ce type d'optimisation peut considérablement améliorer les performances.

Jack Aidley
la source
3
Entité / Composant / Système fonctionne bien pour cela. Construisez chaque "système" pour ce dont vous avez besoin, faites-lui conserver les milliers ou les dizaines de milliers de caractères (leurs composants) et fournissez "l'ID de caractère" au système. Cela vous permet de garder les différents modèles de données séparés et plus petits, et vous pouvez également supprimer les caractères morts des systèmes qui n'en ont pas besoin. (Vous pouvez également décharger complètement un système, si vous ne l'utilisez pas pour le moment.)
Der Kommissar
1
En réduisant la quantité de caractères de mise à jour reçus hors écran, vous pouvez augmenter considérablement les temps de traitement. Tu ne veux pas dire diminuer?
Tejas Kale
1
@TejasKale: Oui, corrigé.
Jack Aidley
2
Un millier de personnages, ce n'est pas beaucoup, mais lorsque chacun vérifie constamment s'il peut castrer les autres, cela commence à avoir un impact majeur sur les performances globales ...
curiousdannii
1
Il vaut toujours mieux vérifier, mais c'est généralement une hypothèse de travail sûre que les Romains vont vouloir se castrer les uns les autres;)
curiousdannii
11

Dans cette situation, je suggère d'utiliser Composition :

Le principe selon lequel les classes doivent atteindre un comportement polymorphe et réutiliser le code par leur composition (en contenant des instances d'autres classes qui implémentent la fonctionnalité souhaitée)


Dans ce cas, il semble que votre Characterclasse est devenue semblable à Dieu et contient tous les détails sur le fonctionnement d'un personnage à toutes les étapes de son cycle de vie.

Par exemple, vous notez que les caractères morts sont toujours requis - car ils sont utilisés dans les arbres généalogiques. Cependant, il est peu probable que toutes les informations et fonctionnalités de vos personnages vivants soient encore nécessaires juste pour les afficher dans un arbre généalogique. Ils peuvent par exemple avoir simplement besoin de noms, de date de naissance et d'une icône de portrait.


La solution consiste à diviser les parties distinctes de vos Charactersous-classes, dont le Characterpossède une instance. Par exemple:

  • CharacterInfo peut être une simple structure de données avec le nom, la date de naissance, la date de décès et la faction,

  • Equipmentpeut avoir tous les objets de votre personnage ou leurs actifs actuels. Il peut également avoir la logique qui gère ces fonctions.

  • CharacterAIou CharacterControllerpeut avoir toutes les informations nécessaires sur l'objectif actuel du personnage, leurs fonctions utilitaires, etc. Et il peut également avoir la logique de mise à jour réelle qui coordonne la prise de décision / interaction entre ses différentes parties.

Une fois que vous avez divisé le personnage, vous n'avez plus besoin de vérifier un indicateur Alive / Dead dans la boucle de mise à jour.

Au lieu de cela, vous souhaitez simplement faire un AliveCharacterObjectqui a CharacterController, CharacterEquipmentet des CharacterInfoscripts associés. Pour "tuer" le personnage, vous supprimez simplement les parties qui ne sont plus pertinentes (comme les CharacterController) - cela ne gaspillera plus de mémoire ni de temps de traitement.

Notez, comment CharacterInfoest probablement la seule donnée réellement nécessaire pour l'arbre généalogique. En décomposant vos classes en fonctionnalités plus petites - vous pouvez plus facilement conserver ce petit objet de données après la mort, sans avoir besoin de conserver l'intégralité du personnage piloté par l'IA.


Il convient de mentionner que ce paradigme est celui qu'Unity a été conçu pour utiliser - et c'est pourquoi il gère les choses avec de nombreux scripts distincts. La construction de grands objets divins est rarement le meilleur moyen de gérer vos données dans Unity.

Bilkokuya
la source
8

Lorsque vous avez une grande quantité de données à gérer et que chaque point de données n'est pas représenté par un objet de jeu réel, il n'est généralement pas une mauvaise idée de renoncer aux classes spécifiques à Unity et de simplement utiliser de vieux objets C #. De cette façon, vous minimisez les frais généraux. Vous semblez donc être sur la bonne voie ici.

Le stockage de tous les caractères, vivants ou morts, dans une liste ( ou tableau ) peut être utile car l'index dans cette liste peut servir d'ID de caractère canonique. L'accès à une position de liste par index est une opération très rapide. Mais il pourrait être utile de conserver une liste séparée des ID de tous les personnages vivants, car vous devrez probablement les répéter beaucoup plus souvent que vous n'aurez besoin des caractères morts.

Au fur et à mesure que votre implémentation de vos mécanismes de jeu progresse, vous voudrez peut-être également examiner les autres types de recherches que vous effectuez le plus. Comme "tous les personnages vivants dans un endroit spécifique" ou "tous les ancêtres vivants ou morts d'un personnage spécifique". Il pourrait être avantageux de créer des structures de données plus secondaires optimisées pour ce type de requêtes. N'oubliez pas que chacun d'eux doit être tenu à jour. Cela nécessite une programmation supplémentaire et sera une source de bugs supplémentaires. Ne le faites donc que si vous vous attendez à une augmentation notable des performances.

CKII « pruneaux » caractères de sa base de données quand il les considère comme sans importance pour économiser les ressources. Si votre pile de personnages morts consomme trop de ressources dans un jeu de longue durée, alors vous voudrez peut-être faire quelque chose de similaire (je ne veux pas appeler cette "collecte des ordures". Peut-être "incrémateur respectueux"?).

Si vous avez réellement un objet de jeu pour chaque personnage du jeu, alors le nouveau système Unity ECS et Jobs pourrait vous être utile. Il est optimisé pour gérer un grand nombre d'objets de jeu très similaires de manière performante. Mais cela force votre architecture logicielle dans des modèles très rigides.

Soit dit en passant, j'aime vraiment CKII et la façon dont il simule un monde avec des milliers de personnages uniques contrôlés par l'IA, donc j'ai hâte de jouer votre point de vue sur le genre.

Philipp
la source
Bonjour, merci pour la réponse. Tous les calculs sont effectués par un seul gestionnaire GameObject. J'affecte des objets de jeu à des acteurs individuels uniquement lorsque cela est nécessaire (comme pour montrer les mouvements de Army of Character d'une position à une autre).
paul p
1
Like "all living characters in a specific location" or "all living or dead ancestors of a specific character". It might be beneficial to create some more secondary data-structures optimized for these kinds of queries.D'après mon expérience avec le modding CK2, cela est proche de la façon dont CK2 gère les données. CK2 semble utiliser des index qui sont essentiellement des index de base de données de base, ce qui accélère la recherche de caractères pour une situation spécifique. Au lieu d'avoir une liste de personnages, il semble avoir une base de données interne de personnages, avec tous les inconvénients et avantages que cela comporte.
Morfildur
1

Vous n'avez pas besoin de simuler / mettre à jour des milliers de personnages lorsque seuls quelques-uns sont à proximité du joueur. Il vous suffit de mettre à jour ce que le joueur peut réellement voir au moment actuel, de sorte que les personnages les plus éloignés du joueur doivent être suspendus jusqu'à ce que le joueur soit plus proche d'eux.

Si cela ne fonctionne pas parce que vos mécanismes de jeu nécessitent des personnages distants pour montrer le temps qui passe, vous pouvez les mettre à jour en une "grosse" mise à jour lorsque le joueur se rapproche. Si vos mécanismes de jeu exigent que chaque personnage réponde réellement aux événements du jeu au fur et à mesure qu'ils se produisent, peu importe où le personnage se trouve par rapport au joueur ou à l'événement, alors cela pourrait fonctionner pour réduire la fréquence à laquelle les personnages plus éloignés du joueur sont mis à jour (c'est-à-dire qu'ils sont toujours mis à jour en synchronisation avec le reste du jeu, mais pas aussi souvent, il y aura donc un léger délai avant que les personnages distants répondent à un événement, mais il est peu probable que cela cause un problème ou soit même remarqué par le joueur ). Vous pouvez également utiliser une approche hybride,

Micheal Johnson
la source
c'est un RTS. Nous devons supposer qu'à un moment donné, un nombre considérable d'unités est réellement à l'écran.
Tom
Dans un RTS, le monde doit continuer pendant que le joueur ne regarde pas. Une grosse mise à jour prendrait autant de temps, mais serait en grande rafale lorsque vous bougez la caméra.
PStag
1

Clarification de la question

J'ai créé un jeu RTS simple, qui contient des centaines de personnages comme Crusader Kings 2 in a Unity.

Dans cette réponse, je suppose que l'ensemble du jeu est censé être comme CK2, au lieu de seulement la partie ayant beaucoup de personnages. Tout ce que vous voyez à l'écran dans CK2 est facile à faire et ne mettra pas en danger vos performances ni ne sera compliqué à implémenter dans Unity. Les données derrière, c'est là que ça devient complexe.

CharacterClasses sans fonctionnalité

J'ai donc créé une classe C # appelée "Caractère" qui contient toutes les données.

Bien, car un personnage de votre jeu n'est que des données. Ce que vous voyez à l'écran n'est qu'une représentation de ces données. Ces Characterclasses sont le cœur même du jeu et en tant que telles risquent de devenir des " objets divins ". Je conseillerais donc des mesures extrêmes contre cela: supprimez toutes les fonctionnalités de ces classes. Une méthode GetFullName()qui combine le prénom et le nom, OK, mais pas de code qui "fait quelque chose". Mettez ce code dans des classes dédiées qui effectuent une action; Par exemple, une classe Birtheravec une méthode Character CreateCharacter(Character father, Character mother)s'avérera beaucoup plus propre que d'avoir cette fonctionnalité dans la Characterclasse.

Ne stockez pas de données dans le code

Pour les stocker, l'option la plus simple serait d'utiliser des objets scriptables

Non. Stockez-les au format JSON, à l'aide de JsonUtility d'Unity. Avec ces Characterclasses sans fonctionnalité , cela devrait être trivial à faire. Cela fonctionnera pour la configuration initiale du jeu ainsi que pour le stocker dans des sauvegardes. Cependant, c'est toujours une chose ennuyeuse à faire, donc je viens de donner l'option la plus simple dans votre situation. Vous pouvez également utiliser XML ou YAML ou tout autre format, tant qu'il peut toujours être lu par les humains lorsqu'il est stocké dans un fichier texte. CK2 fait de même, en fait la plupart des jeux le font. C'est également une excellente configuration pour permettre aux gens de modifier votre jeu, mais c'est une pensée pour beaucoup plus tard.

Pensez abstrait

J'ai ajouté une simple vérification pour m'assurer que le personnage est "Vivant" lors du traitement [...] mais je ne peux pas supprimer "Personnage" s'il est mort car j'ai besoin de ses informations lors de la création de l'arbre généalogique.

Celui-ci est plus facile à dire qu'à faire, car il entre souvent en collision avec la façon de penser naturelle. Vous pensez de façon "naturelle", à un "personnage". Cependant, en ce qui concerne votre jeu, il semble qu'il y ait au moins 2 types différents de données qui "est un personnage": je l'appellerai ActingCharacteret FamilyTreeEntry. Un personnage mort FamilyTreeEntryn'a pas besoin d'être mis à jour et a probablement besoin de beaucoup moins de données qu'un actif ActingCharacter.

Raphael Schmitz
la source
0

Je vais parler d'un peu d'expérience, passant d'une conception OO rigide à une conception ECS (Entity-Component-System).

Il y a quelque temps, j'étais comme vous , j'avais un tas de différents types de choses qui avaient des propriétés similaires et j'ai construit divers objets et essayé d'utiliser l'héritage pour le résoudre. Une personne très intelligente m'a dit de ne pas faire cela et d'utiliser à la place Entity-Component-System.

Maintenant, ECS est un grand concept, et il est difficile de bien faire les choses. Il y a beaucoup de travail à faire pour construire correctement des entités, des composants et des systèmes. Avant de pouvoir le faire, cependant, nous devons définir les termes.

  1. Entité : c'est la chose , le joueur, l'animal, le PNJ, peu importe . C'est une chose qui a besoin de composants attachés.
  2. Composant : il s'agit de l' attribut ou de la propriété , tel qu'un "Nom" ou "Âge", ou "Parents", dans votre cas.
  3. Système : c'est la logique derrière un composant, ou un comportement . En règle générale, vous créez un système par composant, mais ce n'est pas toujours possible. De plus, les systèmes doivent parfois influencer d' autres systèmes.

Voici donc où j'irais avec ceci:

D'abord et avant tout, créez un IDpour vos personnages. Un int, Guidcomme vous voulez. Ceci est "l'entité".

Deuxièmement, commencez à réfléchir aux différents comportements que vous avez. Des choses comme le "Family Tree" - c'est un comportement. Au lieu de modéliser cela comme des attributs sur l'entité, construisez un système qui contient toutes ces informations . Le système peut alors décider quoi en faire.

De même, nous voulons construire un système pour "Le personnage est-il vivant ou mort?" C'est l'un des systèmes les plus importants de votre conception, car il influence tous les autres. Certains systèmes peuvent supprimer les caractères "morts" (comme le système "sprite"), d'autres systèmes peuvent réorganiser en interne les choses pour mieux prendre en charge le nouveau statut.

Vous allez construire un système "Sprite" ou "Dessin" ou "Rendu", par exemple. Ce système aura la responsabilité de déterminer avec quel sprite le personnage doit être affiché et comment l'afficher. Ensuite, lorsqu'un personnage meurt, retirez-les.

De plus, un système "AI" qui peut dire à un personnage quoi faire, où aller, etc. Cela devrait interagir avec de nombreux autres systèmes et prendre des décisions en fonction d'eux. Encore une fois, les personnages morts peuvent probablement être supprimés de ce système, car ils ne font plus vraiment rien.

Votre système "Name" et votre système "Family Tree" devraient probablement garder le personnage (vivant ou mort) en mémoire. Ce système doit rappeler ces informations, quel que soit l'état du personnage. (Jim est toujours Jim, même après que nous l'avons enterré.)

Cela vous donne également l'avantage de changer lorsqu'un système réagit plus efficacement: le système a sa propre minuterie. Certains systèmes doivent se déclencher rapidement, d'autres non. C'est là que nous commençons à comprendre ce qui fait qu'un jeu fonctionne efficacement. Nous n'avons pas besoin de recalculer la météo toutes les millisecondes, nous pouvons probablement le faire toutes les 5 environ.

Cela vous donne également un effet de levier plus créatif: vous pouvez créer un système "Pathfinder" qui peut gérer le calcul d'un chemin de A à B, et peut mettre à jour si nécessaire, permettant au système Movement de dire "où dois-je allez ensuite? " Nous pouvons désormais séparer pleinement ces préoccupations et les raisonner plus efficacement. Le mouvement n'a pas besoin de trouver le chemin, il a juste besoin de vous y conduire.

Vous voudrez exposer certaines parties d'un système à l'extérieur. Dans votre Pathfindersystème, vous en aurez probablement besoin Vector2 NextPosition(int entity). De cette façon, vous pouvez conserver ces éléments dans des tableaux ou des listes étroitement contrôlés. Vous pouvez utiliser plus petits, structtypes, qui peuvent vous aider à garder les composants dans les plus petits, des blocs de mémoire contigus, qui peuvent faire des mises à jour du système beaucoup plus rapide. (Surtout si les influences externes sur un système sont minimes, il n'a plus qu'à se soucier de son état interne, par exemple Name.)

Mais, et je ne saurais trop insister sur ce point, maintenant un Entityest juste un ID, y compris les tuiles, les objets, etc. Si une entité n'appartient pas à un système, alors le système ne le suivra pas. Cela signifie que nous pouvons créer nos objets "Tree", les stocker dans les systèmes Spriteet Movement(les arbres ne bougeront pas, mais ils ont un composant "Position"), et les garder hors des autres systèmes. Nous n'avons plus besoin d'une liste spéciale pour les arbres, car le rendu d'un arbre n'est pas différent d'un personnage, à part le paperdolling. (Ce que le Spritesystème peut contrôler, ou le Paperdollsystème peut contrôler.) Maintenant, notre NextPositionpeut être légèrement réécrit:, Vector2? NextPosition(int entity)et il peut renvoyer une nullposition pour des entités qui ne l'intéressent pas. Nous appliquons également cela à notre NameSystem.GetName(int entity), il revient nullpour les arbres et les rochers.


Je vais conclure ceci, mais l'idée ici est de vous donner quelques informations sur ECS, et comment vous pouvez vraiment l' utiliser pour vous donner une meilleure conception de votre jeu. Vous pouvez augmenter les performances, découpler les éléments non liés et conserver les choses de manière plus organisée. (Cela se marie également bien avec les langages / configurations fonctionnels, comme F # et LINQ, que je recommande fortement de vérifier F # si vous ne l'avez pas déjà fait, il se marie très bien avec C # lorsque vous les utilisez conjointement.)

Der Kommissar
la source
Bonjour, Merci pour cette réponse si détaillée. J'utilise uniquement One GameObject Manager qui contient la référence à tous les autres personnages du jeu.
paul p
Le développement dans Unity s'articule autour d'entités appelées GameObjectqui ne font pas grand-chose mais ont une liste de Componentclasses qui font le travail réel. Il y a une décennie, il y a eu un changement de paradigme concernant le rond-point ECS , car il est plus propre de placer le code actif dans des classes système distinctes. Unity a également récemment mis en place un tel système, mais leur GameObjectsystème est et a toujours été un ECS. OP utilise déjà un ECS.
Raphael Schmitz
-1

Comme vous le faites dans Unity, l'approche la plus simple est la suivante:

  • créer un objet de jeu par personnage ou type d'unité
  • enregistrez-les en tant que préfabriqués
  • vous pouvez ensuite instancier un préfabriqué chaque fois que nécessaire
  • lorsqu'un personnage est tué, détruisez l'objet de jeu et il ne prendra plus de CPU ni de mémoire

Dans votre code, vous pouvez conserver des références aux objets dans quelque chose comme une liste pour vous éviter d'utiliser tout le temps Find () et ses variations. Vous échangez des cycles CPU contre de la mémoire, mais une liste de pointeurs est assez petite, donc même avec quelques milliers d'objets, cela ne devrait pas poser beaucoup de problème.

Au fur et à mesure que vous progressez dans votre jeu, vous constaterez que le fait d'avoir des objets de jeu individuels vous offre des tonnes d'avantages, y compris la navigation et l'IA.

À M
la source