Comment gérer des images arbitrairement grandes dans les jeux 2D?

13

Question

Lorsque vous avez un jeu 2D qui utilise de très grandes images (plus grandes que ce que votre matériel peut supporter), que faites-vous? Peut-être y a-t-il une solution intéressante à ce problème qui ne m'est jamais venue à l'esprit auparavant.

Je travaille essentiellement sur une sorte de créateur de jeux d'aventure graphique. Mon public cible est constitué de personnes ayant peu ou pas d'expérience en développement (et programmation) de jeux. Si vous travailliez avec une telle application, ne préféreriez-vous pas qu'elle gère le problème en interne plutôt que de vous dire de "partager vos images"?

Le contexte

Il est bien connu que les cartes graphiques imposent un ensemble de restrictions sur la taille des textures qu'elles peuvent utiliser. Par exemple, lorsque vous travaillez avec XNA, vous êtes limité à une taille de texture maximale de 2048 ou 4096 pixels selon le profil que vous choisissez.

Habituellement, cela ne pose pas beaucoup de problèmes dans les jeux 2D car la plupart d'entre eux ont des niveaux qui peuvent être construits à partir de petites pièces individuelles (par exemple des tuiles ou des sprites transformés).

Mais pensez à quelques jeux classiques d'aventure graphique point'n'click (par exemple Monkey Island). Les pièces des jeux d'aventure graphique sont généralement conçues dans leur ensemble, avec peu ou pas de sections reproductibles, tout comme un peintre travaillant sur un paysage. Ou peut-être un jeu comme Final Fantasy 7 qui utilise de grands arrière-plans pré-rendus.

Certaines de ces pièces peuvent devenir assez grandes, s'étendant souvent sur trois écrans de largeur ou plus. Maintenant, si nous considérons ces arrière-plans dans le contexte d'un jeu haute définition moderne, ils dépasseront facilement la taille de texture maximale prise en charge.

Maintenant, la solution évidente à cela est de diviser l'arrière-plan en sections plus petites qui correspondent aux exigences et de les dessiner côte à côte. Mais est-ce que vous (ou votre artiste) devez être celui qui divise toutes vos textures?

Si vous travaillez avec une API de haut niveau, ne devrait-elle pas être prête à faire face à cette situation et à effectuer le fractionnement et l'assemblage des textures en interne (soit en tant que prétraitement tel que Content Pipeline de XNA ou au moment de l'exécution)?

Éditer

Voici ce que je pensais, du point de vue de la mise en œuvre de XNA.

Mon moteur 2D ne nécessite qu'un très petit sous-ensemble des fonctionnalités de Texture2D. Je peux en fait le réduire à cette interface:

public interface ITexture
{
    int Height { get; }
    int Width { get; }
    void Draw(SpriteBatch spriteBatch, Vector2 position, Rectangle? source, Color  color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth);
}

La seule méthode externe que j'utilise qui repose sur Texture2D est SpriteBatch.Draw () afin que je puisse créer un pont entre eux en ajoutant une méthode d'extension:

    public static void Draw(this SpriteBatch spriteBatch, ITexture texture, Vector2 position, Rectangle? sourceRectangle, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth)
    {
        texture.Draw(spriteBatch, position, sourceRectangle, color, rotation, origin, scale, effects, layerDepth);
    }

Cela me permettrait d'utiliser une ITexture de manière interchangeable chaque fois que j'utilisais un Texture2D auparavant.

Ensuite, je pourrais créer deux implémentations différentes d'ITexture, par exemple RegularTexture et LargeTexture, où RegularTexture emballerait simplement un Texture2D normal.

D'un autre côté, LargeTexture conserverait une liste de Texture2D correspondant chacun à l'un des morceaux fractionnés de l'image complète. La partie la plus importante est que l'appel de Draw () sur la LargeTexture devrait être capable d'appliquer toutes les transformations et de dessiner toutes les pièces comme si elles n'étaient qu'une seule image contiguë.

Enfin, pour rendre l'ensemble du processus transparent, je créerais une classe de chargeur de texture unifiée qui agirait comme une méthode d'usine. Il prendrait le chemin de la texture comme paramètre et retournerait une ITexture qui serait soit une RegularTexture soit une LargeTexture selon la taille de l'image dépassant la limite ou non. Mais l'utilisateur n'avait pas besoin de savoir lequel c'était.

Un peu exagéré ou ça sonne bien?

David Gouveia
la source
1
Eh bien, si vous voulez aller avec la division de l'image, étendez le pipeline de contenu pour le faire par programme. Toutes les fonctionnalités n'existent pas pour couvrir chaque scénario et à un moment donné, vous devrez l'étendre vous-même. Le fractionnement personnel semble être la solution la plus simple, car vous pouvez alors gérer des images comme des cartes de tuiles, sur lesquelles il existe de nombreuses ressources.
DMan
Le faire sur le pipeline de contenu est l'approche la plus judicieuse. Mais dans mon cas, j'avais besoin de charger mes images lors de l'exécution et j'ai déjà réussi à trouver une solution d'exécution (c'est pourquoi j'ai posé cette question hier). Mais la vraie beauté vient de prendre cette "carte de tuiles" et de la faire se comporter comme une texture normale. C'est-à-dire que vous pouvez toujours le passer à spritebatch pour dessiner, et vous pouvez toujours spécifier la position, la rotation, l'échelle, les rectangles src / dest, etc. Je suis à mi-chemin maintenant :)
David Gouveia
Mais à vrai dire, je suis déjà à un point où je me demande si tout cela en vaut vraiment la peine ou si je devrais simplement montrer un avertissement à l'utilisateur lui disant que la taille d'image maximale prise en charge est de 2048x2048 et en finir avec cela.
David Gouveia
C'est vraiment à vous de concevoir. Si vous avez terminé le projet et que vous ne pouvez pas faire défiler les pièces, ce sera mieux que l'éditeur non terminé qui vous permet de faire défiler les grandes textures: D
Aleks
Le masquer dans l'API semble être une bonne façon de faciliter la vie des auteurs de jeux. Je l'implémenterais probablement dans les classes Polygon (sprite) et Texture, et non pas dans le cas spécial des images plus petites, mais plutôt en ferais juste une tuile qui est 1x1. La classe de texture est donc un groupe de texture décrivant toutes les tuiles et le nombre de lignes / colonnes. La classe Polygon examine ensuite ces informations pour se diviser et dessiner chaque tuile avec la texture appropriée. De cette façon, il est de la même classe. Des points bonus si votre classe de sprites ne dessine que des tuiles visibles et que les groupes de textures ne chargent pas les textures avant d'être nécessaires
uliwitness

Réponses:

5

Je crois que tu es sur le bon chemin. Vous devez diviser les images en petits morceaux. Puisque vous créez un créateur de jeux, vous ne pouvez pas utiliser le pipeline de contenu de XNA. À moins que votre créateur ne ressemble davantage à un moteur qui nécessitera toujours que les développeurs utilisent Visual Studio. Vous devez donc créer vos propres routines qui feront le fractionnement. Vous devriez même envisager de diffuser les morceaux un peu avant qu'ils ne deviennent visibles afin de ne pas limiter la quantité de mémoire vidéo que les joueurs devraient avoir.

Il y a quelques astuces que vous pouvez faire spécifiquement pour les jeux d'aventure. J'imagine que comme tous les jeux d'aventure classiques, vous aurez quelques couches de ces textures pour que ... votre personnage puisse marcher derrière l'arbre ou le bâtiment ...

Si vous voulez que ces couches aient un effet de paralaxe, alors lorsque vous divisez vos tuiles, sautez simplement toutes les translucides une fois. Ici, vous devez penser à la taille des carreaux. Les tuiles plus petites ajouteront la gestion des frais généraux et des appels, mais seront moins de données là où les tuiles plus grandes seront plus conviviales pour le GPU mais auront beaucoup de translucidité.

Si vous n'avez pas d'effet paralaxé, au lieu de couvrir toute la scène avec 2-4 images géantes qui ont une profondeur de 32 bits, vous pouvez créer une seule profondeur de 32 bits. Et vous pouvez avoir une couche de pochoir ou de profondeur qui ne sera que de 8 bits par pixel.

Je sais que cela semble difficile à gérer des actifs par des développeurs autres que des jeux, mais ce n'est pas nécessairement le cas. Par exemple, si vous avez une rue avec 3 arbres et que le joueur passe derrière 2 d'entre eux et devant le troisième arbre et le reste de l'arrière-plan ... Faites simplement la disposition de la scène comme vous le souhaitez dans votre éditeur puis dans un Étape de construction de votre créateur de jeu, vous pouvez faire cuire ces images géantes (enfin de petites tuiles d'une grande image).

À propos de la façon dont les jeux classiques le faisaient. Je ne suis pas sûr qu'ils aient probablement fait des tours simmilaires mais vous devez vous rappeler qu'au moment où l'écran était en 320x240, les jeux étaient en 16 couleurs qui, lorsque vous utilisez des textures palattelisées, vous permet d'encoder 2 pixels en un seul octet. Mais ce n'est pas ainsi que fonctionnent les architectures actuelles: D

Bonne chance

Aleks
la source
Merci, Beaucoup d'idées très intéressantes ici! J'ai déjà vu la technique de couche de pochoir utilisée auparavant dans Adventure Game Studio. Quant à mon application, je fais les choses un peu différemment. Les chambres n'ont pas nécessairement une image d'arrière-plan. Au lieu de cela, il existe une palette d'images que l'utilisateur a importée et un calque d'arrière-plan / de premier plan. L'utilisateur est libre de glisser-déposer des pièces dans la pièce pour la créer. Donc, s'il le voulait, il pourrait aussi le composer à partir de morceaux plus petits sans avoir à créer des objets de jeu non interactifs. Il n'y a cependant pas de parallaxe, car il n'y a que ces deux couches.
David Gouveia
Et j'ai regardé les fichiers de données pour Monkey Island Special Edition (le remake HQ). Chaque arrière-plan est divisé en 1024 x 1024 pièces exactement (même si une grande partie de l'image n'est pas utilisée). Quoi qu'il en soit, vérifiez mes modifications pour savoir ce qui est à l'esprit afin de rendre l'ensemble du processus transparent.
David Gouveia
Je choisirais probablement quelque chose de plus raisonnable comme 256x256. De cette façon, je ne gaspillerai pas les ressources et si plus tard je décide d'implémenter le streaming, il chargera des morceaux plus raisonnables. Peut-être que je ne l'ai pas expliqué assez bien, mais j'ai suggéré que vous puissiez cuire tous ces petits objets de jeu statiques dans une grande texture. Ce que vous économiserez, si vous avez une grande texture d'arrière-plan, tous les pixels couverts par les objets statiques sur le dessus.
Aleks
Je vois, alors fusionnez tous les objets statiques dans l'arrière-plan, puis divisez et créez un atlas de texture. Je pourrais également faire cuire tout le reste dans un atlas de textures en suivant cette ligne de pensée. C'est aussi une bonne idée. Mais je vais le laisser pour plus tard, car pour le moment je n'ai pas d'étape de "build", c'est-à-dire que l'éditeur et le client de jeu fonctionnent sur le même moteur et le même format de données.
David Gouveia
J'aime l'approche au pochoir, mais si je fais un éditeur de jeux d'aventure, je ne l'utiliserais probablement pas ... Peut-être parce que si je construis un tel outil, je suis destiné aux développeurs de jeux. Eh bien, il a ses avantages et ses inconvénients. Par exemple, vous pouvez utiliser une autre image sur laquelle les bits peuvent définir des déclencheurs par pixel ou si vous avez de l'eau animée, vous pouvez facilement la dessiner au pochoir. Remarque: une étrange suggestion sans rapport avec les diagrammes d'état / d'activité UML pour vos scripts.
Aleks
2

Coupez-les pour qu'elles ne soient plus arbitrairement grandes, comme vous dites. Il n'y a pas de magie. Ces vieux jeux 2D l'ont fait ou ont été rendus dans le logiciel, auquel cas il n'y avait aucune restriction matérielle au-delà de la taille de la RAM. Il est à noter que beaucoup de ces jeux 2D n'avaient vraiment pas d'énormes arrière-plans, ils faisaient juste défiler lentement pour se sentir énormes.

Ce que vous cherchez à faire dans votre créateur de jeux d'aventure graphique, c'est de prétraiter ces grandes images pendant une sorte d'étape de "construction" ou "d'intégration" avant que le jeu ne soit préparé pour fonctionner.

Si vous souhaitez utiliser une API et une bibliothèque existantes au lieu d'écrire les vôtres, je vous suggère de convertir ces grandes images en mosaïques lors de cette "construction" et d'utiliser un moteur de mosaïques 2D, qui sont nombreux.

Si vous souhaitez utiliser vos propres techniques 3D, vous souhaiterez peut-être toujours vous scinder en mosaïques et utiliser une API 2D bien connue et bien conçue pour écrire votre propre bibliothèque 3D à cet effet, de cette façon, vous êtes assuré de commencer au moins par une bonne conception de l'API et moins de conception requise de votre part.

Patrick Hughes
la source
Le faire au moment de la construction serait idéal ... Mais mon éditeur utilise également XNA pour le rendu, ce n'est pas seulement le moteur. Cela signifie qu'une fois que l'utilisateur importe l'image et la fait glisser dans une pièce, XNA devrait déjà pouvoir la restituer. Donc, mon gros problème est de savoir s'il faut faire le fractionnement en mémoire lors du chargement du contenu, ou sur disque lors de l'importation du contenu. De plus, la division physique de l'image en plusieurs fichiers signifierait que j'aurais besoin d'un moyen de les stocker différemment, alors que le faire en mémoire lors de l'exécution ne nécessiterait aucune modification des fichiers de données du jeu.
David Gouveia
1

Si vous savez à quoi ressemble un quadtree dans la pratique visuelle, j'ai utilisé une méthode qui coupe des images détaillées de grande taille en images plus petites relativement similaires à ce à quoi ressemble une preuve / visualisation d'un quadtree. Cependant, les miens ont été créés de cette façon pour découper des zones spécifiques qui pourraient être encodées ou compressées différemment en fonction de la couleur. Vous pouvez utiliser cette méthode et faire ce que j'ai décrit, car cela est également utile.

Au moment de leur exécution, les couper nécessiterait temporairement plus de mémoire et ralentirait le temps de chargement. Je dirais que cela dépend si vous vous souciez plus du stockage physique ou du temps de chargement d'un jeu d'utilisateurs créé avec votre éditeur.

Temps de chargement / exécution: La façon dont je procéderais pour le chargement consiste simplement à le découper en morceaux de taille proportionnelle. Tenez compte d'un pixel sur la partie la plus à droite si c'est un nombre impair de pixels, mais personne n'aime que leurs images soient coupées, en particulier les utilisateurs inexpérimentés qui pourraient ne pas savoir que ce n'est pas entièrement à eux. Faites-leur la moitié de la largeur OU la moitié de la hauteur (ou 3rds, 4ths quelle que soit) la raison pour laquelle ils ne sont pas en carrés ou en 4 sections (2 rangées 2 colonnes) est parce que cela rendrait la mémoire de mosaïque plus petite car vous ne vous soucieriez pas de x et y en même temps (et selon le nombre d'images aussi volumineuses, cela pourrait être assez important)

Avant la main / automatiquement: dans ce cas, j'aime l'idée de donner à l'utilisateur le choix de le stocker dans une image ou de le stocker comme des pièces distinctes (une image doit être par défaut). Le stockage de l'image au format .gif fonctionnerait très bien. L'image serait dans un fichier, et plutôt que chaque image soit une animation, ce serait plus comme une archive compressée de la même image (ce qui est essentiellement ce qu'est un .gif de toute façon). Cela permettrait une facilité de transport des fichiers physiques, une facilité de chargement:

  1. C'est un seul fichier
  2. Tous les fichiers auraient un formatage presque identique
  3. Vous pouvez facilement choisir quand ET si le découper en textures individuelles
  4. Il peut être plus facile d'accéder aux images individuelles si elles sont déjà dans un seul fichier .gif chargé, car cela ne nécessiterait pas autant de fouillis de fichiers d'image en mémoire

Oh et si vous utilisez la méthode .gif, vous devez changer l'extension du fichier en quelque chose d'autre (.gif -> .kittens) pour éviter toute confusion lorsque votre .gif s'anime comme s'il s'agissait d'un fichier cassé. (ne vous inquiétez pas de la légalité de cela, .gif est un format ouvert)

Joshua Hedges
la source
Bonjour et merci pour ton commentaire! Désolé si j'ai mal compris, mais ce que vous avez écrit ne concerne-t-il pas principalement la compression et le stockage? Dans ce cas, tout ce qui m'inquiète, c'est que l'utilisateur de mon application puisse importer n'importe quelle image et que le moteur (dans XNA) soit capable de charger et d'afficher n'importe quelle image au moment de l' exécution . J'ai réussi à le résoudre simplement en utilisant GDI pour lire différentes sections de l'image dans des textures XNA distinctes, et en les enveloppant dans une classe qui se comporte comme une texture régulière (c'est-à-dire qui prend en charge le dessin et les transformations dans leur ensemble).
David Gouveia
Mais par curiosité, pourriez-vous nous en dire plus sur les critères que vous avez utilisés pour sélectionner quelle zone de l'image est entrée dans chaque section?
David Gouveia
Une partie de cela peut être au-dessus de votre tête si vous êtes moins expérimenté avec la gestion d'image. Mais essentiellement, je décrivais la découpe de l'image en segments. Et oui, le point .gif décrivait à la fois le stockage et le chargement, mais il est destiné à vous permettre de découper l'image, puis de la stocker sur l'ordinateur comme elle est. Si vous l'enregistrez en tant que gif, je disais que vous pouvez le couper en images individuelles, puis l'enregistrer en tant que gif animé, chaque coupe de l'image étant enregistrée en tant que cadres d'animation distincts. Cela fonctionne bien et j'ai même ajouté d'autres fonctionnalités avec lesquelles cela pourrait aider.
Joshua Hedges
Le système à quatre arbres que j'ai décrit est entièrement indiqué comme référence, car c'est de là que j'ai eu une idée. Bien qu'au lieu d'encombrer une question éventuellement utilisable pour les lecteurs qui peuvent avoir des problèmes similaires, nous devrions utiliser la messagerie privée si vous souhaitez en savoir plus.
Joshua Hedges
Roger, j'ai eu la partie sur l'utilisation des cadres GIF comme une sorte d'archive. Mais je me demande aussi, le GIF n'est-il pas un format indexé avec une palette de couleurs limitée? Puis-je simplement stocker des images RGBA 32bpp entières dans chaque image? Et j'imagine que le processus de chargement se termine encore plus compliqué, car XNA ne prend rien en charge.
David Gouveia
0

Cela va sembler évident, mais pourquoi ne pas simplement diviser votre pièce en petits morceaux? Choisissez une taille arbitraire qui ne heurtera pas les cartes graphiques (comme 1024x1024, ou peut-être plus grandes) et divisez simplement vos pièces en plusieurs morceaux.

Je sais que cela évite le problème, et honnêtement, c'est un problème très intéressant à résoudre. Quoi qu'il en soit, c'est une sorte de solution triviale, qui peut parfois fonctionner pour vous.

cendres999
la source
Je pense que j'ai peut-être mentionné la raison quelque part dans ce fil, mais comme je ne suis pas sûr, je vais répéter. Fondamentalement, car ce n'est pas un jeu, mais plutôt un créateur de jeux pour le grand public (pensez à quelque chose de la même portée que le fabricant de RPG), donc au lieu de forcer la limitation de la taille de la texture lorsque les utilisateurs importent leurs propres actifs, il pourrait être agréable de gérer le fractionnement et la couture automatiquement dans les coulisses sans jamais les déranger avec les détails techniques des limitations de taille de texture.
David Gouveia
D'accord, cela a du sens. Désolé, je suppose que j'ai raté ça.
ashes999