Wie lade ich in XNA dynamisch Teile einer großen 2D-Weltkarte?

17

Ich möchte eine riesige Weltkarte entwickeln. mindestens 8000 × 6000 Pixel groß. Ich habe es in ein 10 × 10-Raster von 800 × 600-Pixel-PNG-Bildern aufgeteilt. Um zu vermeiden, dass alles in den Speicher geladen wird, sollten die Bilder abhängig von der Position des Players im Raster geladen und entfernt werden.

Hier ist zum Beispiel der Spieler an der Position (3,3):

Spieler um (3,3)

Während er sich nach rechts bewegt (4,3), werden die drei Bilder ganz links freigegeben, während die drei Bilder rechts zugewiesen werden:

Spieler bewegt sich zu (4,3)

In jeder Zelle des Gitters sollte sich wahrscheinlich ein Schwellenwert befinden, der das Laden und Entladen auslöst. Das Laden sollte wahrscheinlich in einem separaten Thread erfolgen.

Wie entwerfe ich ein solches System?

pek
quelle
1
Schneller Vorschlag: Sie haben hier zwei Fragen: "Wie lade ich Kacheln dynamisch in ein Spiel?" Und "Wie fädle ich XNA für XBox 360 ein?". Entfernen Sie die betriebssystemspezifischen Segmente aus diesem Beitrag - der Tileloading-Code ist auf XB360, Linux und der Nintendo Wii identisch - und erstellen Sie einen neuen Beitrag für das Threading auf XBox360.
ZorbaTHut
Handelt es sich bei einer Frage zu Ihrem eigentlichen Problem um eine reibungslos scrollende Karte oder um eine Reihe einzelner nicht scrollender Bildschirme?
ZorbaTHut
Wenn ich "Smooth-Scrolling-Karte" verstanden habe, ist es das auch. Was die Trennung der Frage angeht, habe ich darüber nachgedacht, aber ein bisschen gezögert, konsequent Fragen zu stellen. Aber ich werde es jetzt tun.
pek

Antworten:

10

Hier sind einige Probleme zu lösen. Die erste ist das Laden und Entladen von Kacheln. Mit dem ContentManager können Sie standardmäßig keine bestimmten Inhalte entladen. Eine benutzerdefinierte Implementierung davon wird jedoch:

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

Die zweite Frage ist, wie Sie entscheiden, welche Kacheln geladen und entnommen werden sollen. Folgendes würde dieses Problem lösen:

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

Die letzte Frage ist, wie zu entscheiden ist, wann geladen und entladen werden soll. Dies ist wahrscheinlich der einfachste Teil. Dies kann einfach in der Update () -Methode durchgeführt werden, sobald die Bildschirmposition des Players bestimmt wurde:

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

Natürlich müssten Sie auch die Kacheln zeichnen, aber wenn Sie tileWidth, tileHeight und das Array Tiles [] angeben, sollte dies trivial sein.

Sean James
quelle
Vielen Dank. Sehr schön gesagt. Ich möchte einen Vorschlag machen, wenn Sie wissen: Können Sie beschreiben, wie das Laden von Fliesen in einem Faden erfolgen würde? Ich stelle mir vor, dass das Laden von 3 800x600 Kacheln auf einmal das Spiel leicht unterbrechen würde.
pek
Die Grafikkarte muss beim Erstellen eines Texture2D-Objekts einbezogen werden, so weit ich weiß, würde es auch dann zu einem Überspringen führen, wenn das Laden in einem zweiten Thread durchgeführt würde.
Sean James
0

Was Sie tun möchten, ist ziemlich verbreitet. Ein nettes Tutorial zu dieser und anderen gebräuchlichen Techniken finden Sie in dieser Kachel-Engine-Serie .

Wenn Sie so etwas noch nicht gemacht haben, empfehle ich Ihnen, sich die Serie anzuschauen. Wenn Sie jedoch möchten, können Sie den Code für die letzten Lernprogramme abrufen. Wenn Sie dies später tun, überprüfen Sie die Zeichenmethode.

Kurz gesagt, Sie müssen Ihre minimalen und maximalen X / Y-Punkte um den Spieler herum finden. Sobald Sie das haben, durchlaufen Sie einfach jedes und zeichnen dieses Plättchen.

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

Wie Sie sehen, ist viel los. Sie benötigen eine Kamera, die Ihre Matrix erstellt. Sie müssen Ihre aktuelle Pixelposition in einen Kachelindex umwandeln. Sie finden Ihre Min / Max-Punkte (ich füge ein wenig zu meinem MAX hinzu, damit es ein wenig über den sichtbaren Bildschirm hinausgeht ), und dann können Sie es zeichnen.

Ich schlage wirklich vor, die Tutorialserie anzuschauen. Er behandelt Ihr aktuelles Problem, wie Sie einen Kacheleditor erstellen, den Player in Grenzen halten, Sprite-Animationen, KI-Interaktionen usw.

Nebenbei bemerkt, TiledLib hat dies eingebaut. Sie könnten auch deren Code studieren.


quelle
Dies ist zwar sehr hilfreich, deckt jedoch nur das Rendern der Kachelkarte ab. Was ist mit dem Laden und Entladen relevanter Kacheln? Ich kann keine 100 800x600 Kacheln in den Speicher laden. Ich werde mir die Video-Tutorials ansehen. Vielen Dank.
Pek
ahhh .. ich habe deine frage falsch verstanden. Verwenden Sie also eine traditionelle Tilemap oder haben Sie ein großes Bild für Ihr Level, das Sie in Stücke zerbrochen haben?
Nur ein Gedanke ... Sie können die Namen Ihrer Kacheln in einem Array speichern und mit derselben Technik wissen, was geladen und gezeichnet werden muss. Denken Sie daran, um zu versuchen, Objekte wiederzuverwenden, oder Sie könnten eine Menge Müll erstellen
Die Sekunde: Die Karte ist 8000x6000 in 10x10 Kacheln von 800x600 px unterteilt. Für die Wiederverwendung habe ich bereits einen Content Manager, der dafür verantwortlich ist.
pek
0

Ich habe für mein aktuelles Projekt an etwas sehr Ähnlichem gearbeitet. Dies ist ein kurzer Überblick darüber, wie ich es mache, mit einigen Randnotizen, wie man es sich ein bisschen einfacher machen kann.

Für mich bestand das erste Problem darin, die Welt in kleinere Teile zu zerlegen, die sich zum Laden und Entladen im Handumdrehen eignen. Da Sie eine kachelbasierte Karte verwenden, wird dieser Schritt erheblich einfacher. Anstatt die Positionen der einzelnen 3D-Objekte in der Ebene zu berücksichtigen, ist die Ebene bereits in Kacheln unterteilt. Auf diese Weise können Sie die Welt einfach in X-mal-Y-Kacheln aufteilen und diese laden.

Sie möchten dies automatisch und nicht von Hand tun. Da Sie XNA verwenden, haben Sie die Möglichkeit, die Inhaltspipeline mit einem benutzerdefinierten Exporter für Ihre Level-Inhalte zu verwenden. Sofern Sie keine Möglichkeit kennen, den Exportvorgang ohne Neukompilierung auszuführen, würde ich ehrlich davon abraten. Zwar ist das Kompilieren von C # nicht annähernd so langsam wie in C ++, dennoch müssen Sie Visual Studio nicht jedes Mal laden und Ihr Spiel neu kompilieren, wenn Sie eine geringfügige Änderung an der Map vornehmen.

Ein weiterer wichtiger Punkt ist, sicherzustellen, dass Sie eine gute Namenskonvention für die Dateien verwenden, die die Chunks Ihres Levels enthalten. Sie möchten wissen, dass Sie Block C laden oder entladen möchten, und dann den Dateinamen generieren, den Sie laden müssen, um dies zur Laufzeit zu tun. Denken Sie abschließend an Kleinigkeiten, die Ihnen später helfen könnten. Es ist wirklich schön, die Größe eines Blocks ändern zu können, erneut zu exportieren und dann die Auswirkungen auf die Leistung sofort zu sehen.

Zur Laufzeit ist es immer noch ziemlich einfach. Sie benötigen eine Möglichkeit, Blöcke asynchron zu laden und zu entladen. Dies hängt jedoch stark von der Funktionsweise Ihres Spiels oder Ihrer Engine ab. Ihr zweites Bild ist genau richtig - Sie müssen bestimmen, welche Chunks geladen oder entladen werden sollen, und die entsprechenden Anforderungen stellen, um dies zu tun, falls dies noch nicht geschehen ist. Je nachdem, wie viele Blöcke Sie gleichzeitig geladen haben, können Sie dies immer dann tun, wenn der Spieler eine Grenze von einem Block zum nächsten überschreitet. Schließlich möchten Sie so oder so sicherstellen, dass auch in der schlechtesten (angemessenen) Ladezeit der Block noch geladen ist, bevor der Player ihn sehen kann. Sie werden wahrscheinlich viel mit dieser Zahl spielen wollen, bis Sie ein ausgewogenes Verhältnis zwischen Leistung und Speicherverbrauch gefunden haben.

In Bezug auf die tatsächliche Architektur möchten Sie den Vorgang des tatsächlichen Ladens und Entladens der Daten aus dem Speicher von dem Vorgang des Bestimmens, was geladen / entladen werden soll, abstrahieren. Bei Ihrer ersten Iteration würde ich mir nicht einmal Gedanken über die Leistung beim Laden / Entladen machen und nur das Einfachste holen, das möglicherweise funktioniert, und sicherstellen, dass Sie die entsprechenden Anforderungen zu den entsprechenden Zeiten generieren. Anschließend können Sie das Laden optimieren, um den Müll zu minimieren.

Ich habe aufgrund der von mir verwendeten Engine eine Menge zusätzlicher Komplexität erlebt, aber das ist ziemlich implementierungsspezifisch. Wenn Sie Fragen zu meiner Arbeit haben, kommentieren Sie diese bitte und ich werde alles tun, um Ihnen zu helfen.

Logan Kincaid
quelle
1
Sie können msbuild verwenden, um die Inhaltspipeline außerhalb von Visual Studio auszuführen. Das zweite WinForms-Beispiel haben Sie behandelt: creators.xna.com/en-US/sample/winforms_series2
Andrew Russell
@ Andrew !!!!! Ich danke dir sehr!!! Genau aus diesem Grund wollte ich die Content-Pipeline komplett aufgeben!
pek