Une carte avec 20 millions de tuiles fait que le jeu manque de mémoire, comment l'éviter?

11

Lors du chargement d'énormes cartes supplémentaires, la méthode de chargement libère une exception de mémoire lorsqu'une nouvelle instance de tuile de carte est créée. J'aimerais que toute la carte soit traitée au moins sur l'application serveur (et sur le client si possible). Comment dois-je résoudre ce problème?

UPD: la question ici est de savoir comment arrêter le plantage du jeu lorsqu'il y a encore de la mémoire disponible pour l'utiliser. En ce qui concerne la division de la carte en morceaux, c'est une bonne approche, mais pas ce que je veux dans mon cas.

UPD2: Au début, je suis allé attribuer une texture à chaque nouvelle instance de classe de tuiles et c'est ce qui a pris tellement de mémoire (également le temps de chargement). Maintenant, cela prend environ quatre fois moins d'espace. Merci à tout le monde, maintenant je peux exécuter d'énormes cartes sans penser à les décomposer en morceaux pour l'instant.

UPD3: Après avoir réécrit le code pour voir si les tableaux de propriétés de tuiles fonctionnent plus rapidement ou consomment moins de mémoire (que lesdites propriétés dans leurs tuiles respectives en tant que propriétés d'objet), j'ai découvert que non seulement il m'a fallu beaucoup de temps pour essayer cela, mais n'a apporté aucune amélioration des performances et a rendu le jeu terriblement difficile à déboguer.

user1306322
la source
8
Ne chargez que les zones autour des joueurs.
MichaelHouse
4
20 millions de tuiles à 4 octets par tuile est seulement d'environ 80 Mo - il semble donc que vos tuiles ne sont pas si petites. Par magie, nous ne pouvons pas vous donner plus de mémoire, nous devons donc trouver un moyen de réduire la taille de vos données. Alors, montrez-nous ce qu'il y a dans ces tuiles.
Kylotan
Que fait la méthode de chargement? Code postal, utilisez-vous le pipeline de contenu? Charge-t-il des textures en mémoire ou simplement des métadonnées? Quelles métadonnées? Les types de contenu XNA font généralement référence à des éléments DirectX non gérés, de sorte que les objets gérés sont très petits, mais du côté non géré, il peut y avoir des tonnes de choses que vous ne voyez pas, sauf si vous exécutez également un profileur DirectX. stackoverflow.com/questions/3050188/…
Oskar Duveborn
2
Si le système lève une exception de mémoire insuffisante, la mémoire disponible est insuffisante, malgré ce que vous pourriez penser. Il n'y aura pas de ligne de code secrète que nous pouvons vous donner pour activer la mémoire supplémentaire. Le contenu de chaque tuile et la façon dont vous les allouez sont importants.
Kylotan
3
Qu'est-ce qui vous fait penser que vous avez de la mémoire libre? Exécutez-vous un processus 32 bits dans un système d'exploitation 64 bits?
Dalin Seivewright

Réponses:

58

l'application se bloque lorsqu'elle atteint 1,5 Go.

Cela suggère fortement que vous ne représentez pas correctement vos tuiles, car cela signifierait que chaque tuile a une taille d'environ 80 octets.

Ce que vous devez comprendre, c'est qu'il doit y avoir une séparation entre le concept de gameplay d'une tuile et la tuile visuelle que l'utilisateur voit. Ces deux concepts ne sont pas la même chose .

Prenez Terraria par exemple. Le plus petit monde Terraria occupe 4200x1200 tuiles, soit 5 millions de tuiles. Maintenant, combien de mémoire faut-il pour représenter ce monde?

Eh bien, chaque carreau a une couche de premier plan, une couche d'arrière-plan (les murs d'arrière-plan), une "couche de fils" où vont les fils et une "couche de meubles" où vont les meubles. Combien de mémoire prend chaque tuile? Encore une fois, nous parlons simplement conceptuellement, pas visuellement.

Une tuile de premier plan pourrait facilement être stockée dans un court non signé. Il n'y a pas plus de 65536 types de tuiles de premier plan, il est donc inutile d'utiliser plus de mémoire que cela. Les tuiles d'arrière-plan peuvent facilement être dans un octet non signé, car il existe moins de 256 types différents de tuiles d'arrière-plan. La couche de fil est purement binaire: soit une tuile contient un fil, soit elle n'en a pas. C'est donc un bit par tuile. Et la couche de meubles pourrait à nouveau être un octet non signé, selon le nombre possible de meubles différents.

Taille totale de la mémoire par tuile: 2 octets + 1 octet + 1 bit + 1 octet: 4 octets + 1 bit. Ainsi, la taille totale d'une petite carte Terraria est de 20790000 octets, soit ~ 20 Mo. (Remarque: ces calculs sont basés sur Terraria 1.1. Le jeu s'est beaucoup développé depuis lors, mais même Terraria moderne pourrait tenir dans les 8 octets par emplacement de tuile, soit ~ 40 Mo. Toujours tout à fait tolérable).

Vous ne devriez jamais avoir cette représentation stockée sous forme de tableaux de classes C #. Ils doivent être des tableaux d'entiers ou quelque chose de similaire. La structure AC # fonctionnerait également.

Maintenant, quand vient le temps de dessiner une partie d'une carte (notez l'accent), Terraria doit convertir ces tuiles conceptuelles en tuiles réelles . Chaque tuile doit réellement choisir une image de premier plan, une image d'arrière-plan, une image de meuble en option et avoir une image filaire. C'est là que XNA entre en scène avec ses diverses feuilles de sprites et autres.

Ce que vous devez faire est de convertir la partie visible de votre carte conceptuelle en tuiles de feuille de sprite XNA réelles. Vous ne devriez pas essayer de convertir le tout en une seule fois . Chaque tuile que vous stockez doit simplement être un index indiquant que «Je suis de type tuile X», où X est un entier. Vous utilisez cet index entier pour récupérer le sprite que vous utilisez pour l'afficher. Et vous utilisez les feuilles de sprite de XNA pour rendre cela plus rapide que de simplement dessiner des quads individuels.

Maintenant, la zone visible des tuiles doit être divisée en différents morceaux, de sorte que vous ne construisez pas constamment de feuilles de sprites chaque fois que la caméra se déplace. Donc, vous pourriez avoir des morceaux 64x64 du monde sous forme de feuilles de sprite. Quels que soient les morceaux 64x64 du monde visibles depuis la position actuelle de la caméra du joueur, ce sont les morceaux que vous dessinez. Tous les autres morceaux n'ont même pas de feuilles de sprite; si un morceau tombe de l'écran, vous jetez cette feuille (remarque: vous ne le supprimez pas vraiment; vous le conservez et le spécifiez pour un nouveau morceau qui peut devenir visible plus tard).

J'aimerais que toute la carte soit traitée au moins sur l'application serveur (et sur le client si possible).

Votre serveur n'a pas besoin de connaître ou de se soucier de la représentation visuelle des tuiles. Il lui suffit de se soucier de la représentation conceptuelle. L'utilisateur ajoute une tuile ici, donc il change cet index de tuile.

Nicol Bolas
la source
4
+1 Excellente réponse. Doit être voté sur plus que le mien.
MichaelHouse
22

Divisez le terrain en régions ou en morceaux. Ensuite, ne chargez que les morceaux visibles par les joueurs et déchargez ceux qui ne le sont pas. Vous pouvez y penser comme un tapis roulant, où vous chargez des morceaux à une extrémité et les déchargez à l'autre pendant que le joueur avance. Toujours en avance sur le joueur.

Vous pouvez également utiliser des astuces comme l'instanciation. Où si toutes les tuiles d'un même type n'ont qu'une seule instance en mémoire et sont dessinées plusieurs fois dans tous les emplacements où cela est nécessaire.

Vous pouvez trouver plus d'idées comme celle-ci ici:

Comment l'ont-ils fait: des millions de carreaux à Terraria

«Zonage» des zones sur une grande carte de tuiles, ainsi que des donjons

MichaelHouse
la source
Le problème est qu'une telle approche est bonne pour l'application cliente, mais pas aussi bonne pour le serveur. Lorsque certaines tuiles dans le monde se déclenchent et qu'une réaction en chaîne commence (et cela arrive souvent), toute la carte doit être en mémoire pour cela et c'est un problème quand elle se vide de mémoire.
user1306322
6
Que contient chaque tuile? Quelle quantité de mémoire est utilisée par tuile? Même à 10 octets chacun, vous n'êtes qu'à 190 mégaoctets. Le serveur ne doit pas charger la texture ou toute autre information inutile. Si vous avez besoin d'une simulation pour 20 millions de tuiles, vous devez retirer autant que possible de ces tuiles pour former un ensemble de simulation qui ne possède que les informations requises pour vos réactions en chaîne.
MichaelHouse
5

La réponse de Byte56 est bonne, et j'ai commencé à écrire ceci en tant que commentaire, mais elle est devenue trop longue et contient quelques conseils qui pourraient être plus utiles dans une réponse.

L'approche est tout aussi bonne pour le serveur que pour le client. En fait, c'est probablement plus approprié, étant donné que le serveur sera appelé à se préoccuper de bien plus de domaines qu'un client. Le serveur a une idée différente de ce qui doit être chargé qu'un client (il se soucie de toutes les régions qui intéressent tous les clients connectés), mais il est également capable de gérer un ensemble de régions de travail.

Même si vous pouviez mettre en mémoire une carte aussi gigantesque (et vous ne pouvez probablement pas) en mémoire, vous ne le souhaitez pas . Il est absolument inutile de charger des données que vous n'avez pas à utiliser immédiatement, c'est inefficace et lent. Même si vous pouviez le mettre en mémoire, vous ne seriez probablement pas en mesure de tout traiter dans un laps de temps raisonnable.

Je soupçonne votre objection au déchargement de certaines régions parce que votre mise à jour mondiale est en train d'itérer sur toutes les tuiles et de les traiter? C'est certainement valable, mais cela n'empêche pas d'avoir seulement certaines pièces chargées. Au lieu de cela, lorsqu'une région est chargée, appliquez les mises à jour manquées pour la mettre à jour. C'est également beaucoup plus efficace en termes de traitement (en concentrant une grande quantité d'efforts sur une petite zone de mémoire). S'il y a des effets de traitement qui pourraient traverser les limites des régions (par exemple, une coulée de lave ou une coulée d'eau qui se poursuivra dans une autre région), alors liez ces deux régions ensemble de sorte que lorsqu'une est chargée, l'autre l'est aussi. Idéalement, ces situations devraient être minimisées, sinon vous vous retrouverez rapidement dans le cas «toutes les régions doivent être chargées à tout moment».

MrCranky
la source
3

Un moyen efficace que j'ai utilisé dans un jeu XNA était:

  • utilisez des feuilles de sprites, pas une image pour chaque tuile / objet, et chargez juste ce dont vous aurez besoin sur cette carte;
  • divisez la carte en parties plus petites, par exemple des cartes en morceaux de 500 x 200 tuiles si votre carte est 4 fois cette taille, ou quelque chose que vous pouvez manipuler;
  • charger ce morceau en mémoire et peindre uniquement les parties visibles de la carte (par exemple, les tuiles visibles plus 1 tuile pour chaque direction dans laquelle le joueur se déplace) dans un tampon hors écran;
  • effacez la fenêtre et copiez les pixels du tampon (cela s'appelle un double tampon et lisse le mouvement);
  • lorsque vous vous déplacez, suivez les bouds de la carte et chargez le morceau suivant quand il s'en approche et ajoutez-le au courant;
  • une fois que le joueur est entièrement dans cette nouvelle carte, vous pouvez décharger la dernière.

Vous pouvez également utiliser une seule grande carte de cette façon si elle n'est pas si grande et utiliser la logique présentée.

Bien sûr, la taille de vos textures doit être équilibrée et la résolution paie un grand prix. Essayez de travailler à une résolution inférieure et, si vous en avez besoin, créez un pack haute résolution à charger si un paramètre de configuration est défini sur.

Vous devrez boucler à chaque fois uniquement les parties pertinentes de cette carte et afficher uniquement ce qui est nécessaire. De cette façon, vous améliorerez considérablement les performances.

Un serveur peut traiter une énorme carte plus rapidement car il n'a pas à rendre (l'une des opérations les plus lentes) le jeu, donc la logique peut être l'utilisation de blocs plus gros ou même de la carte entière, mais ne traite que la logique autour des joueurs, comme dans une fenêtre d'affichage de 2 fois la zone autour du joueur.

Un conseil ici est que je ne suis en aucun cas un expert en développement de jeu et que mon jeu a été fait pour le projet final de mon diplôme (que j'avais le meilleur score), donc je ne suis peut-être pas si juste à chaque point, mais j'ai fait des recherches sur le Web et des sites comme http://www.gamasutra.com et le site des créateurs XNA creators.xna.com (à ce moment http://create.msdn.com ) pour rassembler des connaissances et des compétences et cela a bien fonctionné pour moi .

Ricardo Souza
la source
2

Soit

  • acheter plus de mémoire
  • représenter vos structures de données de manière plus compacte
  • ne conservez pas toutes les données en mémoire à la fois.
Thomas
la source
J'ai 8 Go de RAM sur Win7 64 bits et l'application se bloque lorsqu'elle atteint 1,5 Go. Il n'y a donc vraiment aucun intérêt à acheter plus de RAM, sauf si je manque quelque chose.
user1306322
1
@ user1306322: Si vous avez 20 millions de tuiles et que cela prend environ ~ 1,5 Go de mémoire, cela signifie que vos tuiles font environ 80 octets par tuile . Que stockez-vous exactement dans ces choses?
Nicol Bolas
@NicolBolas comme je l'ai dit, quelques ints, octets et un texture2d. De plus, ils ne prennent pas tous 1,5 Go, l'application se bloque lorsqu'elle atteint ce coût de mémoire.
user1306322
2
@ user1306322: C'est encore beaucoup plus que nécessaire. Quelle est la taille d'un texture2d? Chaque tuile en a-t-elle une unique ou partage-t-elle des textures? Aussi, avez-vous dit cela? Vous ne l'avez certainement pas mis dans votre question, qui est où l'information devrait être. Expliquez mieux votre situation particulière, s'il vous plaît.
Nicol Bolas
@ user1306322: Franchement, je ne comprends pas très bien pourquoi ma réponse a été rejetée. J'ai énuméré trois remèdes aux situations de mémoire insuffisante - bien sûr, cela ne signifie pas que tous s'appliquent nécessairement. Mais je pense toujours qu'ils sont corrects. J'aurais probablement dû écrire "étendre votre mémoire" au lieu de "acheter" pour inclure le cas des machines virtuelles. Suis-je déçu parce que ma réponse était concise plutôt que verbeuse? Je pense que ces points sont assez simples pour être compris sans autre explication?!
Thomas
1

la question ici est de savoir comment empêcher le jeu de planter lorsqu'il y a encore de la mémoire disponible pour l'utiliser. En ce qui concerne la division de la carte en morceaux, c'est une bonne approche, mais pas ce que je veux dans mon cas.

En réponse à votre mise à jour, vous ne pouvez pas allouer Int32.MaxValuede taille supérieure aux tableaux . Vous devez le diviser en morceaux. Vous pouvez même l'encapsuler dans une classe wrapper qui expose une façade de type tableau:

Jimmy
la source