In XNA, come posso caricare dinamicamente parti di una grande mappa del mondo 2D?


17

Voglio sviluppare un'enorme mappa del mondo; dimensioni di almeno 8000 × 6000 pixel. L'ho suddiviso in una griglia 10 × 10 di immagini PNG da 800 × 600 pixel. Per evitare di caricare tutto in memoria, le immagini devono essere caricate e scaricate a seconda della posizione del giocatore nella griglia.

Ad esempio, ecco il giocatore in posizione (3,3):

Giocatore a (3,3)

Mentre si sposta a destra (4,3), le tre immagini all'estrema sinistra vengono deallocate mentre le tre immagini a destra sono allocate:

Il giocatore si sposta su (4,3)

Probabilmente dovrebbe esserci una soglia all'interno di ogni cella della griglia che avvia il caricamento e lo scaricamento. Il caricamento dovrebbe probabilmente avvenire in un thread separato.

Come posso progettare un tale sistema?


1
Suggerimento rapido: hai due domande qui, "come posso caricare dinamicamente le tessere in un gioco" e "come posso fare il thread su XNA per XBox 360". Rimuovi i segmenti specifici del sistema operativo da questo post - il codice di tileloading sarà identico su XB360, Linux e Nintendo Wii - e crea un nuovo post per il threading su XBox360.
Zorba THut,

Per quanto riguarda una domanda sul tuo vero problema, si tratta di una mappa a scorrimento uniforme o di una serie di schermate singole non a scorrimento?
Zorba THut,

Se ho capito "mappa a scorrimento uniforme", sì, lo è. Per quanto riguarda la separazione della domanda, ci ho pensato, ma ho esitato a porre le domande di conseguenza. Ma lo farò ora.
pek,

Risposte:


10

Ci sono alcuni problemi da risolvere qui. Il primo è come caricare e scaricare i riquadri. ContentManager per impostazione predefinita non consente di scaricare specifici contenuti. Un'implementazione personalizzata di questo, tuttavia, dovrà:

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();
    }
}

Il secondo problema è come decidere quali tessere caricare e scaricare. Quanto segue risolverebbe questo problema:

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;
}

Il problema finale è come decidere quando caricare e scaricare. Questa è probabilmente la parte più semplice. Questo potrebbe essere semplicemente fatto nel metodo Update () una volta determinata la posizione dello schermo del giocatore:

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);
}

Ovviamente dovresti anche disegnare le tessere, ma dati tileWidth, tileHeight e l'array Tiles [], questo dovrebbe essere banale.


Grazie. Molto ben detto. Vorrei dare un suggerimento, se lo sapete: potreste descrivere come sarebbe il caricamento delle piastrelle in un thread. Immagino che caricare 3 tessere 800x600 contemporaneamente metta in pausa il gioco.
pek

La scheda grafica deve essere coinvolta durante la creazione di un oggetto Texture2D, quindi, per quanto ne so, causerebbe comunque un salto anche se il caricamento fosse effettuato su un secondo thread.
Sean James,

0

Quello che vuoi fare è abbastanza comune. Per un bel tutorial su questa e altre tecniche comuni, dai un'occhiata a questa serie di motori per piastrelle .

Se non hai mai fatto nulla di simile prima, ti consiglio di guardare la serie. Tuttavia, se lo desideri, potresti ottenere l'ultimo codice tutorial. Se lo fai in seguito, controlla il metodo di disegno.

In poche parole, è necessario trovare i punti X / Y minimo e massimo intorno al giocatore. Una volta che hai che basta passare attraverso ciascuno di essi e disegnare quella tessera.

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;
}

Come puoi vedere, stanno succedendo molte cose. Hai bisogno di una videocamera che creerà la tua matrice, devi convertire la tua posizione attuale dei pixel in un indice di tessera, trovi i tuoi punti min / max (aggiungo un po 'al mio MAX in modo che si sposti un po' oltre lo schermo visibile ), quindi puoi disegnarlo.

Consiglio davvero di guardare la serie di tutorial. Copre il tuo problema attuale, come creare un editor di tessere, mantenendo il giocatore entro i limiti, animando lo sprite, l'interazione AI, ecc ...

Su una nota a margine TiledLib ha questo integrato. Puoi studiare anche il loro codice.


Sebbene sia molto utile, copre solo il rendering della mappa delle tessere. Che dire di caricare e scaricare le tessere pertinenti? Non riesco a caricare 100 tessere 800x600 in memoria. Darò un'occhiata ai tutorial video; Grazie.
pek,

ahhh .. ho frainteso la tua domanda. Quindi stai usando una piastrella tradizionale o hai una grande immagine per il tuo livello che hai diviso in pezzi?

Solo un pensiero .... potresti memorizzare i nomi delle tue tessere in un array e usare la stessa tecnica per sapere cosa caricare e disegnare. Ricorda di provare a riutilizzare qualsiasi oggetto o potresti creare un sacco di immondizia

Il secondo: la mappa è 8000x6000 suddivisa in tessere 10x10 di 800x600 px. Per quanto riguarda il riutilizzo, ho già un gestore dei contenuti che ne è responsabile.
pek,

0

Ho lavorato su qualcosa di molto simile per il mio progetto attuale. Questo è un breve riassunto di come lo sto facendo, con alcune note a margine su come rendere le cose un po 'più facili per te.

Per me, il primo problema è stato quello di spezzare il mondo in blocchi più piccoli che sarebbero appropriati per caricare e scaricare al volo. Dato che stai utilizzando una mappa basata su riquadri, questo passaggio diventa notevolmente più semplice. Piuttosto che considerare le posizioni di ogni oggetto 3D nel livello, hai già il tuo livello diviso piacevolmente in tessere. Questo ti consente semplicemente di spezzare il mondo in blocchi di tessere X per Y e caricarli.

Lo vorrai fare automaticamente, piuttosto che a mano. Dal momento che stai usando XNA, hai la possibilità di utilizzare la pipeline di contenuti con un esportatore personalizzato per i tuoi contenuti di livello. A meno che tu non conosca un modo per eseguire il processo di esportazione senza ricompilare, sinceramente lo sconsiglio. Sebbene C # non sia così lento da compilare come tende a essere C ++, non è comunque necessario caricare Visual Studio e ricompilare il gioco ogni volta che si apportano modifiche minori alla mappa.

Un'altra cosa importante qui è assicurarti di usare una buona convenzione di denominazione per i file che contengono i blocchi del tuo livello. Vuoi essere in grado di sapere che vuoi caricare o scaricare il blocco C e quindi generare il nome file che devi caricare per farlo in fase di esecuzione. Infine, pensa a tutte le piccole cose che potrebbero aiutarti lungo la strada. È davvero bello poter cambiare la dimensione di un pezzo, riesportarlo e vederne immediatamente gli effetti sulle prestazioni.

In fase di esecuzione, è ancora piuttosto semplice. Avrai bisogno di un modo per caricare e scaricare i blocchi in modo asincrono, ma questo dipende fortemente dal funzionamento del tuo gioco o motore. La tua seconda immagine è esattamente corretta: devi determinare quali blocchi devono essere caricati o scaricati e fare le richieste appropriate per fare questo caso se non lo è già. A seconda di quanti pezzi hai caricato contemporaneamente, puoi farlo ogni volta che il giocatore attraversa un confine da un pezzo all'altro. Dopotutto, in entrambi i casi, vuoi assicurarti che sia caricato abbastanza che anche nel peggiore (ragionevole) tempo di caricamento, il pezzo sia ancora caricato prima che il giocatore possa vederlo. Probabilmente vorrai giocare molto con questo numero finché non avrai un buon equilibrio tra prestazioni e consumo di memoria.

Per quanto riguarda l'architettura reale, vorrai sottrarre il processo di caricamento e scaricamento effettivo dei dati dalla memoria dal processo di determinazione di cosa dovrebbe essere caricato / scaricato. Per la tua prima iterazione, non mi preoccuperei nemmeno delle prestazioni di caricamento / scaricamento e otterrei semplicemente la cosa più semplice che potrebbe funzionare, e assicurarti che stai generando le richieste appropriate nei momenti appropriati. Successivamente, puoi vedere come ottimizzare il caricamento per ridurre al minimo i rifiuti.

Ho incontrato molta ulteriore complessità a causa del motore che stavo usando, ma è piuttosto specifico per l'implementazione. Se hai domande su ciò che ho fatto, ti preghiamo di commentare e farò il possibile per dare una mano.


1
È possibile utilizzare msbuild per eseguire la pipeline di contenuti all'esterno di Visual Studio. Il secondo esempio di WinForms è coperto: creators.xna.com/en-US/sample/winforms_series2
Andrew Russell,

@Andrea !!!!! Grazie mille!!! Stavo per abbandonare completamente la pipeline di contenuti proprio per questo motivo!
pek
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.