No XNA, como carregar dinamicamente partes de um mapa-múndi 2D grande?

17

Eu quero desenvolver um enorme mapa do mundo; pelo menos 8000 × 6000 pixels de tamanho. Dividi-o em uma grade 10 × 10 de imagens PNG de 800 × 600 pixels. Para evitar carregar tudo na memória, as imagens devem ser carregadas e descarregadas, dependendo da posição do player na grade.

Por exemplo, aqui está o jogador na posição (3,3):

Jogador em (3,3)

À medida que ele se move para a direita (4,3), as três imagens à esquerda são desalocadas enquanto as três imagens à direita são alocadas:

O jogador se move para (4,3)

Provavelmente deve haver um limite dentro de cada célula da grade acionando o carregamento e o descarregamento. O carregamento provavelmente deve ocorrer em um thread separado.

Como faço para projetar esse sistema?

pek
fonte
1
Sugestão rápida: Você tem duas perguntas aqui, "como carrego blocos dinamicamente em um jogo" e "como faço para segmentar no XNA para XBox 360". Remova os segmentos específicos do sistema operacional desta postagem - o código de carregamento automático será idêntico no XB360, Linux e Nintendo Wii - e crie uma nova postagem para segmentação no XBox360.
ZorbaTHut
Quanto a uma pergunta sobre o seu problema real, este é um mapa de rolagem suave ou uma série de telas individuais sem rolagem?
ZorbaTHut
Se eu entendi "mapa de rolagem suave", sim, é. Quanto a separar a pergunta, pensei nisso, mas meio que hesitei em fazer perguntas consequentemente. Mas, eu vou fazer isso agora.
Pek

Respostas:

10

Existem alguns problemas a serem resolvidos aqui. A primeira é como carregar e descarregar blocos. O ContentManager, por padrão, não permite descarregar partes específicas do conteúdo. Uma implementação personalizada disso, no entanto, irá:

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

A segunda questão é como decidir quais blocos carregar e descarregar. O seguinte resolveria este 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;
}

A questão final é como decidir quando carregar e descarregar. Esta é provavelmente a parte mais fácil. Isso pode ser feito simplesmente no método Update () quando a posição da tela do jogador for determinada:

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

É claro que você também precisaria desenhar os blocos, mas, considerando o tileWidth, tileHeight e a matriz Tiles [], isso deve ser trivial.

Sean James
fonte
Obrigado. Muito bem dito. Gostaria de fazer uma sugestão, se você souber: você poderia descrever como o carregamento de peças seria feito em um thread? Imagino que o carregamento de 3 blocos de 800x600 de uma vez pausaria um pouco o jogo.
Pek
A placa de vídeo precisa estar envolvida na criação de um objeto Texture2D, pelo que sei que ainda causaria uma falha, mesmo que o carregamento fosse feito em um segundo segmento.
Sean James
0

O que você quer fazer é bastante comum. Para um bom tutorial sobre essa e outras técnicas comuns, confira esta série de mecanismos de blocos .

Se você não fez nada assim antes, recomendo assistir a série. No entanto, se você quiser, poderá obter o código dos últimos tutoriais. Se você fizer isso mais tarde, confira o método draw.

Em poucas palavras, você precisa encontrar seus pontos mínimo e máximo de X / Y ao redor do player. Depois de conseguir, basta percorrer cada uma delas e desenhar essa peça.

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

Como você pode ver, há muita coisa acontecendo. Você precisa de uma câmera para criar sua matriz, precisa converter sua localização atual de pixels em um índice de blocos, encontrar seus pontos mínimo / máximo (adiciono um pouco ao meu MAX para que ele se mova um pouco além da tela visível ) e, em seguida, você pode desenhá-lo.

Eu realmente sugiro assistir a série de tutoriais. Ele aborda o seu problema atual, como criar um editor de blocos, manter o jogador dentro dos limites, animação de sprites, interação com IA, etc.

Em uma nota lateral, o TiledLib incorporou isso. Você também pode estudar o código deles.


fonte
Embora isso seja muito útil, ele cobre apenas a renderização do mapa de blocos. Que tal carregar e descarregar blocos relevantes? Não consigo carregar 100 blocos de 800x600 na memória. Vou dar uma olhada nos tutoriais em vídeo; obrigado.
Pek
ahhh .. Eu entendi mal a sua pergunta. Então, você está usando um mapa de blocos tradicional ou tem uma imagem grande para o seu nível que dividiu em pedaços?
Apenas um pensamento ... você pode armazenar os nomes de seus blocos em uma matriz e usar a mesma técnica para saber o que carregar e desenhar. Tenha em mente para tentar voltar a usar quaisquer objetos ou você pode criar um monte de lixo
A segunda: o mapa é 8000x6000 dividido em blocos 10x10 de 800x600 px. Quanto à reutilização, eu já tenho um gerenciador de conteúdo responsável por isso.
Pek
0

Eu tenho trabalhado em algo muito semelhante ao meu projeto atual. Esta é uma rápida descrição de como estou fazendo isso, com algumas anotações sobre como tornar as coisas um pouco mais fáceis para você.

Para mim, o primeiro problema foi dividir o mundo em pedaços menores que seriam apropriados para carregar e descarregar em tempo real. Como você está usando um mapa baseado em blocos, essa etapa se torna significativamente mais fácil. Em vez de considerar as posições de cada objeto 3D no nível, você já tem seu nível dividido em blocos. Isso permite que você simplesmente divida o mundo em pedaços de ladrilhos X por Y e carregue-os.

Você vai querer fazer isso automaticamente, e não manualmente. Como você usa o XNA, você tem a opção de usar o Pipeline de conteúdo com um exportador personalizado para o seu nível de conteúdo. A menos que você conheça alguma maneira de executar o processo de exportação sem recompilar, eu sinceramente recomendo. Embora o C # não seja tão lento para compilar quanto o C ++, você ainda não precisa carregar o Visual Studio e recompilar seu jogo toda vez que fizer uma pequena alteração no mapa.

Outra coisa importante aqui é garantir que você use uma boa convenção de nomenclatura para os arquivos que contêm os blocos do seu nível. Você deseja saber que deseja carregar ou descarregar o pedaço C e, em seguida, gerar o nome do arquivo que você precisa carregar para fazer isso em tempo de execução. Por fim, pense em pequenas coisas que possam ajudá-lo no caminho. É muito bom poder alterar o tamanho de um pedaço, reexportar e ver os efeitos disso no desempenho imediatamente.

Em tempo de execução, ainda é bastante direto. Você precisará de alguma maneira para carregar e descarregar de forma assíncrona pedaços, mas isso depende muito de como o seu jogo ou mecanismo funciona. Sua segunda imagem está exatamente correta - você precisa determinar quais blocos devem ser carregados ou descarregados e fazer as solicitações apropriadas para que isso aconteça, caso ainda não esteja. Dependendo de quantos pedaços você carregou ao mesmo tempo, você pode fazer isso sempre que o jogador cruzar um limite de um pedaço para o outro. Afinal, de qualquer maneira, você quer ter certeza de que está carregado o suficiente para que, mesmo no pior (razoável) tempo de carregamento, o pedaço ainda esteja carregado antes que o jogador possa vê-lo. Você provavelmente vai querer jogar muito com esse número até obter um bom equilíbrio entre desempenho e consumo de memória.

No que diz respeito à arquitetura real, você desejará abstrair o processo de realmente carregar e descarregar os dados da memória do processo de determinar o que deve ser carregado / descarregado. Para sua primeira iteração, eu nem me preocuparia com o desempenho do carregamento / descarregamento e apenas obteria a coisa mais simples que poderia funcionar, além de garantir que você esteja gerando as solicitações apropriadas nos momentos apropriados. Depois disso, você pode otimizar como você carrega para minimizar o lixo.

Encontrei muita complexidade adicional devido ao mecanismo que estava usando, mas isso é bastante específico da implementação. Se você tiver alguma dúvida sobre o que eu fiz, comente e farei o que puder para ajudar.

Logan Kincaid
fonte
1
Você pode usar o msbuild para executar o pipeline de conteúdo fora do Visual Studio. A segunda amostra WinForms tem coberto: creators.xna.com/en-US/sample/winforms_series2
Andrew Russell
@Andrew !!!!! Muito obrigado!!! Eu estava prestes a abandonar completamente o pipeline de conteúdo exatamente por esse motivo!
Pek