Как в XNA динамически загружать части большой 2D-карты мира?


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

Если я понял «карту с плавной прокруткой», то да, это так. Что касается разделения вопроса, я подумал об этом, но вроде не решался задавать вопросы последовательно. Но я сделаю это сейчас.
2010 года

Ответы:


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

Конечно, вам также нужно будет рисовать плитки, но с учетом tileWidth, tileHeight и массива Tiles [] это должно быть тривиально.


Спасибо. Очень красиво сказано. Я хотел бы сделать предложение, если вы знаете: не могли бы вы описать, как будет происходить загрузка плиток в потоке? Я полагаю, что загрузка 3 800x600 плиток сразу приостановит игру.
2010 года

Графическая карта должна быть задействована при создании объекта 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;
}

Как видите, многое происходит. Вам нужна камера, которая будет создавать вашу матрицу, вам нужно преобразовать ваше текущее местоположение в пикселях в индекс плитки, вы найдете ваши минимальные / максимальные точки (я немного добавляю к своему МАКСУ, чтобы он немного выходил за пределы видимого экрана) ), а затем вы можете нарисовать его.

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

На стороне записки TiledLib имеет эту встроенную. Вы можете изучить их код , а также.


Хотя это очень полезно, оно охватывает только рендеринг карты тайлов. А как насчет загрузки и выгрузки соответствующих плиток? Я не могу загрузить 100 800x600 плиток в память. Я посмотрю видео уроки; Благодарю.
Пек

аааа .. я неправильно понял ваш вопрос. Так вы используете традиционную карту тайлов или у вас есть большое изображение для вашего уровня, которое вы разбили на куски?

Просто мысль .... вы можете хранить имена ваших плиток в массиве и использовать ту же технику, чтобы знать, что загружать и рисовать. Имейте в виду, чтобы попытаться повторно использовать любые объекты, или вы можете создать много мусора

Второе: карта размером 8000x6000, разбитая на 10x10 плиток размером 800x600 пикселей. Что касается повторного использования, у меня уже есть менеджер контента, который отвечает за это.
2010 года

0

Я работал над чем-то очень похожим для моего текущего проекта. Это краткое изложение того, как я это делаю, с некоторыми примечаниями о том, как сделать вещи немного проще для себя.

Для меня первая проблема состояла в том, чтобы разбить мир на более мелкие куски, которые бы подходили для загрузки и выгрузки на лету. Поскольку вы используете карту на основе плитки, этот шаг становится значительно проще. Вместо того, чтобы учитывать положение каждого 3D-объекта на уровне, у вас уже есть свой уровень, аккуратно разделенный на плитки. Это позволяет вам просто разбить мир на X на Y фрагментов плитки и загрузить их.

Вы захотите сделать это автоматически, а не вручную. Поскольку вы используете XNA, у вас есть возможность использовать конвейер содержимого с настраиваемым экспортером для содержимого вашего уровня. Если вы не знаете какой-либо способ запуска процесса экспорта без перекомпиляции, я бы честно рекомендовал это сделать. Хотя компиляция C # не так медленна, как в C ++, вам все равно не нужно загружать Visual Studio и перекомпилировать игру каждый раз, когда вы вносите небольшие изменения в карту.

Еще одна важная вещь - убедиться, что вы используете хорошее соглашение об именах для файлов, содержащих чанки вашего уровня. Вы хотите знать, что хотите загрузить или выгрузить блок C, а затем сгенерировать имя файла, которое вам нужно загрузить, чтобы сделать это во время выполнения. Наконец, подумайте о любых мелочах, которые могут помочь вам в будущем. Очень приятно иметь возможность изменить размер блока, выполнить реэкспорт и сразу же увидеть его влияние на производительность.

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

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

Я столкнулся с множеством дополнительных сложностей из-за движка, который использовал, но это довольно специфично для конкретной реализации. Если у вас есть какие-либо вопросы о том, что я сделал, пожалуйста, прокомментируйте, и я сделаю все возможное, чтобы помочь.


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

@Андрей !!!!! Спасибо большое!!! Я собирался полностью отказаться от конвейера контента именно по этой причине!
2010 года
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.