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.
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'uneGameObject
, affichant le nombre "3000".Réponses:
Vous devez considérer trois choses:
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.
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.
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.
la source
Dans cette situation, je suggère d'utiliser Composition :
Dans ce cas, il semble que votre
Character
classe 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
Character
sous-classes, dont leCharacter
possè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,Equipment
peut avoir tous les objets de votre personnage ou leurs actifs actuels. Il peut également avoir la logique qui gère ces fonctions.CharacterAI
ouCharacterController
peut 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
AliveCharacterObject
qui aCharacterController
,CharacterEquipment
et desCharacterInfo
scripts associés. Pour "tuer" le personnage, vous supprimez simplement les parties qui ne sont plus pertinentes (comme lesCharacterController
) - cela ne gaspillera plus de mémoire ni de temps de traitement.Notez, comment
CharacterInfo
est 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.
la source
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.
la source
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.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,
la source
Clarification de la question
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.
Character
Classes sans fonctionnalité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
Character
classes 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éthodeGetFullName()
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 classeBirther
avec une méthodeCharacter CreateCharacter(Character father, Character mother)
s'avérera beaucoup plus propre que d'avoir cette fonctionnalité dans laCharacter
classe.Ne stockez pas de données dans le code
Non. Stockez-les au format JSON, à l'aide de JsonUtility d'Unity. Avec ces
Character
classes 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
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
ActingCharacter
etFamilyTreeEntry
. Un personnage mortFamilyTreeEntry
n'a pas besoin d'être mis à jour et a probablement besoin de beaucoup moins de données qu'un actifActingCharacter
.la source
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.
Voici donc où j'irais avec ceci:
D'abord et avant tout, créez un
ID
pour vos personnages. Unint
,Guid
comme 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
Pathfinder
système, vous en aurez probablement besoinVector2 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,struct
types, 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 exempleName
.)Mais, et je ne saurais trop insister sur ce point, maintenant un
Entity
est juste unID
, 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èmesSprite
etMovement
(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 leSprite
système peut contrôler, ou lePaperdoll
système peut contrôler.) Maintenant, notreNextPosition
peut être légèrement réécrit:,Vector2? NextPosition(int entity)
et il peut renvoyer unenull
position pour des entités qui ne l'intéressent pas. Nous appliquons également cela à notreNameSystem.GetName(int entity)
, il revientnull
pour 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.)
la source
GameObject
qui ne font pas grand-chose mais ont une liste deComponent
classes 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 leurGameObject
système est et a toujours été un ECS. OP utilise déjà un ECS.Comme vous le faites dans Unity, l'approche la plus simple est la suivante:
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.
la source