Dans XNA, comment charger dynamiquement des parties d'une grande carte du monde 2D?

17

Je veux développer une immense carte du monde; au moins 8000 × 6000 pixels. Je l'ai divisé en une grille 10 × 10 d'images 800 × 600 pixels PNG. Pour éviter de tout charger en mémoire, les images doivent être chargées et déchargées en fonction de la position du joueur dans la grille.

Par exemple, voici le joueur en position (3,3):

Joueur à (3,3)

Alors qu'il se déplace vers la droite pour (4,3), les trois images à l'extrême gauche sont désallouées tandis que les trois images à droite sont attribuées:

Le joueur passe à (4,3)

Il devrait probablement y avoir un seuil à l'intérieur de chaque cellule de la grille déclenchant le chargement et le déchargement. Le chargement devrait probablement se produire dans un thread séparé.

Comment concevoir un tel système?

pek
la source
1
Suggestion rapide: vous avez ici deux questions: "comment puis-je charger dynamiquement des tuiles dans un jeu" et "comment faire du filetage sur XNA pour XBox 360". Supprimez les segments spécifiques au système d'exploitation de ce message - le code de chargement en mosaïque sera identique sur XB360, Linux et Nintendo Wii - et créez un nouveau message pour le filetage sur XBox360.
ZorbaTHut
Quant à une question sur votre problème réel, s'agit-il d'une carte à défilement fluide ou d'une série d'écrans individuels sans défilement?
ZorbaTHut
Si j'ai compris "carte à défilement fluide", oui, ça l'est. Quant à séparer la question, j'y ai pensé mais j'ai hésité un peu à poser des questions en conséquence. Mais je vais le faire maintenant.
pek

Réponses:

10

Il y a quelques problèmes à résoudre ici. La première est de savoir comment charger et décharger des tuiles. Le ContentManager par défaut ne vous permettra pas de décharger des éléments de contenu spécifiques. Cependant, une implémentation personnalisée de ceci:

public class ExclusiveContentManager : ContentManager
{
    public ExclusiveContentManager(IServiceProvider serviceProvider,
        string RootDirectory) : base(serviceProvider, RootDirectory)
    {
    }

    public T LoadContentExclusive<T>(string AssetName)
    {
        return ReadAsset<T>(AssetName, null);
    }

    public void Unload(IDisposable ContentItem)
    {
        ContentItem.Dispose();
    }
}

Le deuxième problème est de savoir comment décider quelles tuiles charger et décharger. Ce qui suit résoudrait ce problème:

public class Tile
{
    public int X;
    public int Y;
    public Texture2D Texture;
}

int playerTileX;
int playerTileY;
int width;
int height;

ExclusiveContentManager contentEx;
Tile[,] tiles;

void updateLoadedTiles()
{
    List<Tile> loaded = new List<Tile>();

    // Determine which tiles need to be loaded
    for (int x = playerTileX - 1; x <= playerTileX + 1; x++)
        for (int y = playerTileY - 1; y <= playerTileY; y++)
        {
            if (x < 0 || x > width - 1) continue;
            if (y < 0 || y > height - 1) continue;

            loaded.Add(tiles[x, y]);
        }

    // Load and unload as necessary
    for (int x = 0; x < width; x++)
        for (int y = 0; y < height; y++)
        {
            if (loaded.Contains(tiles[x, y]))
            {
                if (tiles[x, y].Texture == null)
                    loadTile(x, y);
            }
            else
            {
                if (tiles[x, y].Texture != null)
                    contentEx.Unload(tiles[x, y].Texture);
            }
        }
}

void loadTile(int x, int y)
{
    Texture2D tex = contentEx.LoadEx<Texture2D>("tile_" + x + "_" + y);
    tiles[x, y].Texture = tex;
}

Le dernier problème est de savoir comment décider quand charger et décharger. C'est probablement la partie la plus simple. Cela pourrait simplement être fait dans la méthode Update () une fois que la position de l'écran du joueur a été déterminée:

int playerScreenX;
int playerScreenY;
int tileWidth;
int tileHeight;

protected override void Update(GameTime gameTime)
{
    // Code to update player position, etc...

    // Load/unload
    int newPlayerTileY = (int)((float)playerScreenX / (float)tileWidth);
    int newPlayerTileX = (int)((float)playerScreenY / (float)tileHeight);

    if (newPlayerTileX != playerTileX || newPlayerTileY != playerTileY)
        updateLoadedTiles();

    base.Update(gameTime);
}

Bien sûr, vous devrez également dessiner les tuiles, mais étant donné tileWidth, tileHeight et le tableau Tiles [], cela devrait être trivial.

Sean James
la source
Je vous remercie. Très bien dit. Je voudrais faire une suggestion, si vous le savez: pourriez-vous décrire comment le chargement des tuiles se ferait dans un fil. J'imagine que le chargement simultané de 3 tuiles 800x600 interromprait légèrement le jeu.
pek
La carte graphique doit être impliquée lors de la création d'un objet Texture2D, pour autant que je sache, cela entraînerait toujours un saut même si le chargement était effectué sur un deuxième thread.
Sean James
0

Ce que vous voulez faire est assez courant. Pour un joli didacticiel sur cette technique et d'autres techniques courantes, consultez cette série de moteurs de tuiles .

Si vous n'avez jamais rien fait de tel auparavant, je vous recommande de regarder la série. Cependant, si vous le souhaitez, vous pouvez obtenir le dernier code des didacticiels. Si vous le faites plus tard, consultez la méthode de dessin.

En un mot, vous devez trouver vos points X et Y min et max autour du lecteur. Une fois que vous avez cela, il vous suffit de parcourir chacun d'eux et de dessiner cette tuile.

public override void Draw(GameTime gameTime)
{
    Point min = currentMap.WorldPointToTileIndex(camera.Position);
    Point max = currentMap.WorldPointToTileIndex( camera.Position +
        new Vector2(
            ScreenManager.Viewport.Width + currentMap.TileWidth,
            ScreenManager.Viewport.Height + currentMap.TileHeight));


    min.X = (int)Math.Max(min.X, 0);
    min.Y = (int)Math.Max(min.Y, 0);
    max.X = (int)Math.Min(max.X, currentMap.Width);
    max.Y = (int)Math.Min(max.Y, currentMap.Height + 100);

    ScreenManager.SpriteBatch.Begin(SpriteBlendMode.AlphaBlend
                                    , SpriteSortMode.Immediate
                                    , SaveStateMode.None
                                    , camera.TransformMatrix);
    for (int x = min.X; x < max.X; x++)
    {
        for (int y = min.Y; y < max.Y; y++)
        {

            Tile tile = Tiles[x, y];
            if (tile == null)
                continue;

            Rectangle currentPos = new Rectangle(x * currentMap.TileWidth, y * currentMap.TileHeight, currentMap.TileWidth, currentMap.TileHeight);
            ScreenManager.SpriteBatch.Draw(tile.Texture, currentPos, tile.Source, Color.White);
        }
    }

    ScreenManager.SpriteBatch.End();
    base.Draw(gameTime);
}

public Point WorldPointToTileIndex(Vector2 worldPoint)
{
    worldPoint.X = MathHelper.Clamp(worldPoint.X, 0, Width * TileWidth);
    worldPoint.Y = MathHelper.Clamp(worldPoint.Y, 0, Height * TileHeight);


    Point p = new Point();

    // simple conversion to tile indices
    p.X = (int)Math.Floor(worldPoint.X / TileWidth);
    p.Y = (int)Math.Floor(worldPoint.Y / TileWidth);

    return p;
}

Comme vous pouvez le voir, il se passe beaucoup de choses. Vous avez besoin d'une caméra qui va créer votre matrice, vous devez convertir votre emplacement de pixel actuel en un index de tuile, vous trouvez vos points min / max (j'ajoute un peu à mon MAX pour qu'il dessine un peu au-delà de l'écran visible ), puis vous pouvez le dessiner.

Je suggère vraiment de regarder la série de tutoriels. Il couvre votre problème actuel, comment créer un éditeur de tuiles, garder le joueur dans les limites, animation de sprites, interaction avec l'IA, etc.

Sur une note latérale TiledLib a cela intégré. Vous pouvez également étudier leur code.


la source
Bien que cela soit très utile, il ne couvre que le rendu de la carte des tuiles. Qu'en est-il du chargement et du déchargement des tuiles pertinentes? Je ne peux pas charger 100 tuiles 800x600 en mémoire. Je vais jeter un œil aux tutoriels vidéo; Merci.
pek
ahhh .. J'ai mal compris votre question. Alors, utilisez-vous un tilemap traditionnel ou avez-vous une grande image pour votre niveau que vous avez divisée en morceaux?
Juste une pensée ... vous pouvez stocker les noms de vos tuiles dans un tableau et utiliser la même technique pour savoir quoi charger et dessiner. Gardez à l'esprit pour essayer de réutiliser des objets ou vous pourriez créer beaucoup de déchets
La seconde: la carte est de 8000x6000 divisée en 10x10 tuiles de 800x600 px. Quant à la réutilisation, j'ai déjà un gestionnaire de contenu qui en est responsable.
pek
0

J'ai travaillé sur quelque chose de très similaire pour mon projet actuel. Voici un bref aperçu de la façon dont je le fais, avec quelques notes annexes sur la façon de vous faciliter la tâche.

Pour moi, le premier problème était de briser le monde en petits morceaux qui seraient appropriés pour charger et décharger à la volée. Puisque vous utilisez une carte basée sur des tuiles, cette étape devient beaucoup plus facile. Plutôt que de considérer les positions de chaque objet 3D dans le niveau, vous avez déjà bien divisé votre niveau en tuiles. Cela vous permet simplement de diviser le monde en morceaux de tuiles X par Y et de les charger.

Vous allez vouloir le faire automatiquement, plutôt qu'à la main. Puisque vous utilisez XNA, vous avez la possibilité d'utiliser le pipeline de contenu avec un exportateur personnalisé pour votre contenu de niveau. À moins que vous ne connaissiez un moyen d'exécuter le processus d'exportation sans recompilation, je vous le déconseille honnêtement. Bien que C # ne soit pas aussi lent à compiler que C ++ a tendance à l'être, vous ne voulez toujours pas avoir à charger Visual Studio et recompiler votre jeu chaque fois que vous apportez une modification mineure à la carte.

Une autre chose importante ici est de vous assurer que vous utilisez une bonne convention de dénomination pour les fichiers contenant des morceaux de votre niveau. Vous voulez pouvoir savoir que vous voulez charger ou décharger le bloc C, puis générer le nom de fichier que vous devez charger pour le faire au moment de l'exécution. Enfin, pensez à toutes les petites choses qui pourraient vous aider sur la route. C'est vraiment agréable de pouvoir changer la taille d'un morceau, de réexporter, puis d'en voir immédiatement les effets sur les performances.

Au moment de l'exécution, c'est encore assez simple. Vous aurez besoin d'un moyen pour charger et décharger des morceaux de manière asynchrone, mais cela dépend fortement du fonctionnement de votre jeu ou de votre moteur. Votre deuxième image est exactement correcte - vous devez déterminer quels morceaux doivent être chargés ou déchargés et faire les demandes appropriées pour que ce soit le cas si ce n'est pas déjà fait. Selon le nombre de morceaux que vous avez chargés à la fois, vous pouvez simplement le faire chaque fois que le joueur franchit une limite d'un morceau au suivant. Après tout, dans tous les cas, vous voulez vous assurer qu'il y a suffisamment de charge pour que même dans le pire temps de chargement (raisonnable), le morceau soit toujours chargé avant que le joueur puisse le voir. Vous allez probablement vouloir jouer beaucoup avec ce nombre jusqu'à ce que vous obteniez un bon équilibre entre les performances et la consommation de mémoire.

En ce qui concerne l'architecture réelle, vous souhaiterez faire abstraction du processus de chargement et de déchargement des données de la mémoire du processus de détermination de ce qui doit être chargé / déchargé. Pour votre première itération, je ne m'inquiéterais même pas des performances de chargement / déchargement et j'obtiendrais simplement la chose la plus simple qui pourrait éventuellement fonctionner, et vous assurer que vous générez les demandes appropriées aux moments appropriés. Après cela, vous pouvez envisager d'optimiser la façon dont vous chargez pour minimiser les déchets.

J'ai rencontré beaucoup de complexité supplémentaire en raison du moteur que j'utilisais, mais c'est assez spécifique à l'implémentation. Si vous avez des questions sur ce que j'ai fait, veuillez commenter et je ferai mon possible pour vous aider.

Logan Kincaid
la source
1
Vous pouvez utiliser msbuild pour exécuter le pipeline de contenu en dehors de Visual Studio. Le deuxième échantillon WinForms vous a couvert: creators.xna.com/en-US/sample/winforms_series2
Andrew Russell
@Andrew !!!!! Merci beaucoup!!! J'étais sur le point d'abandonner complètement le pipeline de contenu pour exactement cette raison!
pek