Як у XNA динамічно завантажувати частини великої двовимірної карти світу?


17

Я хочу розробити величезну карту світу; розміром не менше 8000 × 6000 пікселів. Я розбив її на 10 × 10 сітку з зображеннями PNG 800 × 600 пікселів. Щоб не завантажувати все в пам'ять, зображення слід завантажувати та вивантажувати залежно від позиції гравця в сітці.

Наприклад, ось гравець на позиції (3,3):

Гравець у (3,3)

Коли він рухається праворуч до (4,3), три зображення в крайній лівій частині розміщуються, а три зображення праворуч виділяються:

Гравець переходить до (4,3)

Напевно, повинен бути поріг всередині комірки сітки, що ініціює завантаження та вивантаження. Завантаження, ймовірно, має відбуватися в окремому потоці.

Як я спроектую таку систему?


1
Швидка пропозиція. У вас тут два питання: "як я динамічно завантажую плитки в грі" та "як мені робити нарізки на XNA для XBox 360". Видаліть з цієї публікації специфічні для ОС сегменти - код завантаження плитки буде ідентичним для XB360, Linux і Nintendo Wii - і створіть новий пост для нанизування на XBox360.
ZorbaTHut

Що стосується вашої реальної проблеми, це карта з гладкою прокруткою чи ряд окремих екранів, що не прокручуються?
ZorbaTHut

Якби я зрозумів "карту плавного прокрутки", так, це так. Щодо відокремлення питання, я подумав над цим, але якось вагався, щоб потім задати питання. Але я зараз це зроблю.
пік

Відповіді:


10

Тут потрібно вирішити кілька проблем. Перший - як завантажувати та вивантажувати плитку. За замовчуванням ContentManager не дозволяє вивантажувати певні частини вмісту. Однак спеціальна реалізація цього:

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

Друге питання - як вирішити, яку плитку завантажувати та вивантажувати. Наступне вирішило б цю проблему:

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

Остаточне питання - як вирішити, коли завантажувати та вивантажувати. Це, мабуть, найпростіша частина. Це можна було просто зробити методом Update () після визначення позиції екрана гравця:

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

Звичайно, вам також потрібно намалювати плитки, але, враховуючи ширину плитки, висоту плитки та масив Tiles [], це має бути тривіально.


Дякую. Дуже красиво сказано. Я хотів би зробити пропозицію, якщо ви знаєте: чи могли б ви описати, як виконується завантаження плитки в нитку. Я думаю, що завантаження 3 плиток 800х600 відразу призупинить гру.
пік

У створенні об’єкта Texture2D потрібно залучати відеокарту, тому, наскільки я знаю, вона все-таки спричинить пропуск, навіть якщо завантаження буде здійснено на другому потоці.
Шон Джеймс

0

Те, що ви хочете зробити, досить поширене. Щоб отримати цікавий підручник з цієї та інших поширених методик, ознайомтеся з цією серією двигунів з плитки .

Якщо ви ще нічого подібного не робили, рекомендую переглянути серіал. Однак, якщо ви хочете, ви можете отримати останній підручник код. Якщо ви пізніше, перегляньте метод малювання.

Коротше кажучи, ви повинні знайти свої мінімум та максимум X / Y очок навколо гравця. Коли ви це зробите, ви просто проведіть кожну та намалюйте цю плитку.

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

Як бачите, багато чого відбувається. Вам потрібна камера, яка створить вашу матрицю, вам потрібно перетворити поточне місце пікселя в індекс плитки, ви знайдете свої мінімальні / максимальні точки (я додаю трохи до свого MAX, щоб він трохи виводив за межі видимого екрана ), і тоді ви можете намалювати його.

Я дуже пропоную переглянути серіал підручників. Він висвітлює вашу поточну проблему, як створити редактор плиток, утримуючи програвач у межах, спрайт-анімацію, AI-взаємодію тощо ...

Зі сторони примітки TiledLib має вбудований модуль . Ви також можете вивчити їх код.


Хоча це дуже корисно, воно охоплює лише відображення карти плитки. А як щодо завантаження та вивантаження відповідної плитки? Я не можу завантажити в пам'ять 100 плиток 800х600. Я погляну на відеоуроки; Спасибі.
пік

аааа .. Я неправильно зрозумів ваше запитання. Отже, ви використовуєте традиційну мапу плитки, чи у вас є великий образ для вашого рівня, який ви розпалили на шматки?

Просто думка .... ви можете зберігати назви ваших плиток у масиві і користуватися тією ж технікою, щоб знати, що завантажувати та малювати. Майте на увазі, щоб спробувати повторно використовувати будь-які об’єкти, або ви могли створити багато сміття

Друга: карта 8000x6000 розбита на 10x10 плитки розміром 800x600 px. Щодо повторного використання, у мене вже є менеджер вмісту, який відповідає за це.
пік

0

Я працював над чимось дуже схожим для свого поточного проекту. Це швидкий аналіз того, як я це роблю, з деякими побічними зауваженнями про те, як зробити речі трохи простішими на собі.

Для мене першою проблемою було розбиття світу на менші шматки, які було б доречно завантажувати та розвантажувати на льоту. Оскільки ви використовуєте карту на основі плитки, цей крок стає значно простішим. Замість того, щоб розглядати положення кожного 3D-об’єкта на рівні, ви вже маєте рівень, який добре розділений на плитки. Це дозволяє вам просто розбити світ на X відрізками плитки Y і завантажити їх.

Ви хочете зробити це автоматично, а не вручну. Оскільки ви використовуєте XNA, у вас є можливість використовувати контент-конвеєр із спеціальним експортером для вмісту рівня. Якщо ви не знаєте, як запустити процес експорту без перекомпіляції, я щиро рекомендую проти цього. Хоча C # не так повільно збирається, як звичайно C ++, ви все одно не хочете завантажувати Visual Studio і перекомпілювати свою гру кожного разу, коли ви вносите незначну зміну на карту.

Ще одна важлива річ - переконатися, що ви використовуєте правильну схему іменування для файлів, що містять фрагменти вашого рівня. Ви хочете мати можливість знати, що ви хочете завантажити або вивантажити фрагмент C, а потім генерувати ім'я файлу, яке потрібно завантажити, щоб зробити це під час виконання. Нарешті, придумайте будь-які дрібниці, які можуть допомогти вам у дорозі. Дійсно, що можна змінити, наскільки великий шматок, реекспортувати, а потім побачити наслідки цього на продуктивність негайно.

На час виконання, це все ще досить просто. Вам знадобиться якийсь спосіб асинхронно завантажувати та вивантажувати шматки, але це дуже залежить від того, як працює ваша гра чи движок. Ваше друге зображення є точно правильним - вам потрібно визначити, які шматки потрібно завантажувати чи вивантажувати, і зробити відповідні запити, щоб це було так, якщо його ще немає. Залежно від того, скільки шматочків ви завантажили за один раз, ви можете це робити щоразу, коли гравець перетне межу від однієї частини до іншої. Зрештою, у будь-якому випадку, ви хочете переконатися, що достатньо завантажено, що навіть у найгірший (розумний) час завантаження шматок все ще завантажується до того, як гравець зможе це побачити. Ви, мабуть, захочете багато пограти з цим номером, поки не отримаєте хороший баланс між продуктивністю та витратою пам'яті.

Що стосується фактичної архітектури, вам потрібно буде абстрагувати процес фактичного завантаження та вивантаження даних із пам'яті з процесу визначення того, що слід завантажувати / вивантажувати. Для вашої першої ітерації я б навіть не переймався ефективністю завантаження / вивантаження, а просто отримав найпростішу річ, яка могла б працювати, і гарантую, що ви генеруєте відповідні запити у відповідний час. Після цього ви можете переглянути оптимізацію завантаження, щоб мінімізувати сміття.

У мене виникли багато додаткових складностей завдяки двигуну, який я використовував, але це досить специфічно для реалізації. Якщо у вас виникнуть запитання щодо того, що я зробив, будь ласка, прокоментуйте, і я зроблю все, що можу, щоб допомогти.


1
Ви можете використовувати msbuild для запуску конвеєра вмісту поза Visual Studio. Другий зразок WinForms охопив вас: creators.xna.com/en-US/sample/winforms_series2
Ендрю Рассел

@Andrew !!!!! Дуже дякую!!! Я збирався повністю відмовитися від контент-конвеєра саме з цієї причини!
пік
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.