Jak w XNA dynamicznie ładować części dużej mapy świata 2D?

17

Chcę stworzyć ogromną mapę świata; rozmiar co najmniej 8000 x 6000 pikseli. Podzieliłem go na siatkę 10 × 10 obrazów PNG o wymiarach 800 × 600 pikseli. Aby uniknąć ładowania wszystkiego do pamięci, obrazy powinny być ładowane i rozładowywane w zależności od pozycji gracza w siatce.

Na przykład tutaj jest gracz na pozycji (3,3):

Gracz w (3,3)

Gdy przesuwa się w prawo (4,3), trzy obrazy po lewej stronie zostają cofnięte, a trzy obrazy po prawej są przydzielane:

Gracz porusza się do (4,3)

Prawdopodobnie w każdej komórce siatki powinien znajdować się próg uruchamiający ładowanie i rozładowywanie. Ładowanie powinno prawdopodobnie nastąpić w osobnym wątku.

Jak zaprojektować taki system?

pek
źródło
1
Szybka sugestia: masz tutaj dwa pytania: „jak dynamicznie ładować kafelki w grze” i „jak nawiązywać wątki na XNA dla XBox 360”. Usuń segmenty specyficzne dla systemu operacyjnego z tego postu - kod ładujący będzie identyczny na XB360, Linux i Nintendo Wii - i stwórz nowy post dla wątków na XBox360.
ZorbaTHut,
Jeśli chodzi o pytanie dotyczące twojego rzeczywistego problemu, czy jest to gładko przewijana mapa, czy seria pojedynczych nie przewijanych ekranów?
ZorbaTHut,
Jeśli zrozumiałem „gładko przewijaną mapę”, tak, to prawda. Jeśli chodzi o rozdzielenie pytania, zastanowiłem się nad tym, ale nieco zawahałem się, by w konsekwencji zadać pytania. Ale zrobię to teraz.
pek

Odpowiedzi:

10

Tutaj jest kilka problemów do rozwiązania. Pierwszym jest ładowanie i rozładowywanie płytek. ContentManager domyślnie nie pozwala na rozładowanie określonych treści. Jednak niestandardowe wdrożenie tego:

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

Drugi problem dotyczy sposobu decydowania, które płytki należy załadować i rozładować. Poniższe rozwiązałoby ten problem:

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

Ostatnim problemem jest to, jak zdecydować, kiedy załadować i rozładować. To chyba najłatwiejsza część. Można to po prostu zrobić w metodzie Update () po określeniu pozycji ekranu gracza:

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

Oczywiście musisz również narysować kafelki, ale biorąc pod uwagę tablicę tileWidth, tileHeight i Tiles [], powinno to być trywialne.

Sean James
źródło
Dziękuję Ci. Bardzo ładnie powiedziane. Chciałbym coś zasugerować, jeśli wiesz: czy mógłbyś opisać, jak ładowanie płytek odbywa się w wątku. Wyobrażam sobie, że załadowanie 3 800 x 600 płytek jednocześnie wstrzymałoby nieco grę.
pek
Karta graficzna musi być zaangażowana w tworzenie obiektu Texture2D, o ile wiem, nadal powoduje pomijanie, nawet jeśli ładowanie zostało wykonane w drugim wątku.
Sean James
0

To, co chcesz zrobić, jest dość powszechne. Aby uzyskać fajny samouczek na temat tej i innych popularnych technik, sprawdź serię silników kafelkowych .

Jeśli nie zrobiłeś czegoś takiego wcześniej, polecam obejrzenie serialu. Jeśli jednak chcesz, możesz uzyskać ostatni kod z samouczków. Jeśli zrobisz to później, sprawdź metodę losowania.

Krótko mówiąc, musisz znaleźć swoje minimalne i maksymalne punkty X / Y wokół gracza. Kiedy już to zrobisz, po prostu przejrzyj każdy z nich i narysuj ten kafelek.

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

Jak widać, wiele się dzieje. Potrzebujesz kamery, która utworzy matrycę, musisz przekonwertować bieżącą lokalizację pikseli na indeks kafelków, znajdziesz swoje punkty min / max (dodaję trochę do mojego MAXa, aby nieco wykraczał poza widoczny ekran) ), a następnie możesz go narysować.

Naprawdę sugeruję obejrzenie serii samouczków. Omawia twój obecny problem, jak stworzyć edytor kafelków, utrzymywać gracza w granicach, animację sprite, interakcję AI itp.

Na marginesie, TiledLib ma to wbudowane. Możesz także przestudiować ich kod.


źródło
Jest to bardzo pomocne, ale obejmuje tylko renderowanie mapy kafelków. A co z ładowaniem i rozładowywaniem odpowiednich płytek? Nie mogę załadować 100 płytek 800 x 600 w pamięci. Rzucę okiem na samouczki wideo; dzięki.
pek
ahhh .. źle zrozumiałem twoje pytanie. Czy używasz tradycyjnej mapy tilem, czy masz duży obraz swojego poziomu, który podzieliłeś na kawałki?
Tylko myśl ... możesz przechowywać nazwiska swoich płytek w tablicy i użyć tej samej techniki, aby wiedzieć, co załadować i narysować. Pamiętaj, aby spróbować ponownie użyć dowolnych obiektów lub możesz utworzyć dużo śmieci
Drugi: mapa ma wymiary 8000 x 6000 podzielone na 10 x 10 płytek o wymiarach 800 x 600 pikseli. Jeśli chodzi o ponowne użycie, mam już menedżera treści, który jest za to odpowiedzialny.
pek.
0

Pracowałem nad czymś bardzo podobnym do mojego obecnego projektu. To krótkie podsumowanie tego, jak to robię, z kilkoma dodatkowymi notatkami na temat tego, jak ułatwić sobie życie.

Dla mnie pierwszym problemem było rozbicie świata na mniejsze części, które byłyby odpowiednie do załadunku i rozładunku w locie. Ponieważ używasz mapy opartej na kafelkach, ten krok staje się znacznie łatwiejszy. Zamiast uwzględniać pozycje każdego obiektu 3D na poziomie, Twój poziom jest już ładnie podzielony na kafelki. To pozwala po prostu rozbić świat na kawałki X na Y i załadować je.

Będziesz chciał to zrobić automatycznie, a nie ręcznie. Ponieważ używasz XNA, masz możliwość korzystania z potoku treści z niestandardowym eksporterem treści poziomu. Jeśli nie znasz sposobu na uruchomienie procesu eksportu bez ponownej kompilacji, szczerze odradzam. Chociaż kompilacja C # nie jest tak wolna jak zwykle C ++, nadal nie musisz ładować Visual Studio i rekompilować gry za każdym razem, gdy wprowadzasz drobne zmiany na mapie.

Inną ważną rzeczą tutaj jest upewnienie się, że używasz dobrej konwencji nazewnictwa dla plików zawierających części twojego poziomu. Chcesz wiedzieć, że chcesz załadować lub rozładować fragment C, a następnie wygenerować nazwę pliku, którą musisz załadować, aby to zrobić w czasie wykonywania. Na koniec pomyśl o drobiazgach, które mogą ci pomóc w dalszej drodze. Naprawdę miło jest móc zmienić wielkość porcji, ponownie wyeksportować, a następnie natychmiast zobaczyć jej wpływ na wydajność.

W czasie wykonywania jest to nadal dość proste. Będziesz potrzebował sposobu asynchronicznego ładowania i rozładowywania fragmentów, ale jest to w dużej mierze zależne od tego, jak działa Twoja gra lub silnik. Drugi obraz jest dokładnie poprawny - musisz określić, które części powinny zostać załadowane lub rozładowane, i złóż odpowiednie wnioski, aby tak było, jeśli nie jest jeszcze. W zależności od liczby załadowanych fragmentów naraz możesz to zrobić za każdym razem, gdy gracz przekroczy granicę z jednej porcji do drugiej. W końcu, tak czy inaczej, chcesz się upewnić, że wystarczająco dużo jest załadowane, że nawet w najgorszym (rozsądnym) czasie ładowania część jest nadal ładowana, zanim gracz ją zobaczy. Prawdopodobnie będziesz chciał dużo grać z tym numerem, dopóki nie uzyskasz równowagi między wydajnością a zużyciem pamięci.

Jeśli chodzi o rzeczywistą architekturę, będziesz chciał wyodrębnić proces faktycznego ładowania i rozładowywania danych z pamięci od procesu określania, co powinno zostać załadowane / rozładowane. Podczas pierwszej iteracji nie martwiłbym się nawet wydajnością ładowania / rozładowywania, a jedynie najprostszą rzeczą, która mogłaby ewentualnie działać, i zapewniał, że generujesz odpowiednie żądania w odpowiednim czasie. Następnie możesz spojrzeć na optymalizację sposobu ładowania, aby zminimalizować śmieci.

Napotkałem wiele dodatkowych problemów ze względu na silnik, którego używałem, ale jest to dość specyficzne dla implementacji. Jeśli masz jakieś pytania dotyczące tego, co zrobiłem, proszę o komentarz, a ja zrobię, co w mojej mocy, aby pomóc.

Logan Kincaid
źródło
1
Możesz użyć msbuild do uruchomienia potoku treści poza Visual Studio. Druga próbka WinForms, którą obejrzałeś: creators.xna.com/en-US/sample/winforms_series2
Andrew Russell
@Andrzej !!!!! Dziękuję bardzo!!! Właśnie z tego powodu całkowicie porzuciłem potok treści!
pek