En XNA, ¿cómo cargo dinámicamente partes de un gran mapamundi 2D?

17

Quiero desarrollar un gran mapa mundial; al menos 8000 × 6000 píxeles de tamaño. Lo he dividido en una cuadrícula de 10 × 10 de imágenes PNG de 800 × 600 píxeles. Para evitar cargar todo en la memoria, las imágenes deben cargarse y descargarse según la posición del jugador en la cuadrícula.

Por ejemplo, aquí está el jugador en posición (3,3):

Jugador en (3,3)

A medida que se mueve hacia la derecha (4,3), las tres imágenes del extremo izquierdo se desasignan mientras que las tres imágenes de la derecha se asignan:

El jugador se mueve a (4,3)

Probablemente debería haber un umbral dentro de cada celda de la cuadrícula que active la carga y descarga. La carga probablemente debería ocurrir en un hilo separado.

¿Cómo diseño un sistema así?

pek
fuente
1
Sugerencia rápida: Tienes dos preguntas aquí, "¿cómo cargo dinámicamente los mosaicos en un juego" y "cómo hago el subprocesamiento en XNA para XBox 360". Elimine los segmentos específicos del sistema operativo de esta publicación (el código de carga de mosaicos será idéntico en XB360, Linux y Nintendo Wii) y haga una nueva publicación para enhebrar en XBox360.
ZorbaTHut
En cuanto a una pregunta sobre su problema real, ¿es este un mapa de desplazamiento suave o una serie de pantallas individuales que no se desplazan?
ZorbaTHut
Si entendí el "mapa de desplazamiento suave", sí, lo es. En cuanto a separar la pregunta, lo pensé pero dudé un poco en hacer preguntas consecuentemente. Pero lo haré ahora.
Pek

Respuestas:

10

Hay algunos problemas para resolver aquí. El primero es cómo cargar y descargar fichas. ContentManager de forma predeterminada no le permitirá descargar piezas específicas de contenido. Sin embargo, una implementación personalizada de esto:

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

El segundo problema es cómo decidir qué mosaicos cargar y descargar. Lo siguiente resolvería 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;
}

El último problema es cómo decidir cuándo cargar y descargar. Esta es probablemente la parte más fácil. Esto podría hacerse simplemente en el método Update () una vez que se determinó la posición de la pantalla del jugador:

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

Por supuesto, también necesitaría dibujar los mosaicos, pero dado el ancho de mosaico, la altura de mosaico y la matriz Tiles [], esto debería ser trivial.

Sean James
fuente
Gracias. Muy bien dicho. Me gustaría hacer una sugerencia, si lo sabe: ¿podría describir cómo se haría la carga de mosaicos en un hilo? Me imagino que cargar 3 fichas de 800x600 a la vez pausaría ligeramente el juego.
Pek
La tarjeta gráfica debe estar involucrada cuando se crea un objeto Texture2D, por lo que sé que aún causaría un salto, incluso si la carga se realiza en un segundo hilo.
Sean James
0

Lo que quieres hacer es bastante común. Para un buen tutorial sobre esta y otras técnicas comunes, consulte esta serie de motores de mosaico .

Si no has hecho algo así antes, te recomiendo ver la serie. Sin embargo, si lo desea, puede obtener el último código de tutoriales. Si haces lo posterior, mira el método de dibujo.

En pocas palabras, debes encontrar tus puntos X / Y mínimo y máximo alrededor del jugador. Una vez que tenga eso, simplemente recorra cada uno y dibuje ese mosaico.

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 puede ver, están sucediendo muchas cosas. Necesita una cámara que cree su matriz, necesita convertir su ubicación de píxeles actual en un índice de mosaico, encuentra sus puntos mínimo / máximo (agrego un poco a mi MAX para que se extienda un poco más allá de la pantalla visible) ), y luego puedes dibujarlo.

Realmente sugiero ver la serie de tutoriales. Cubre su problema actual, cómo crear un editor de mosaicos, mantener al jugador dentro de los límites, animación de sprites, interacción con IA, etc.

En una nota al margen , TiledLib tiene esto incorporado. También podría estudiar su código.


fuente
Si bien esto es muy útil, solo cubre la representación del mapa de mosaico. ¿Qué pasa con la carga y descarga de mosaicos relevantes? No puedo cargar 100 mosaicos de 800x600 en la memoria. Echaré un vistazo a los videos tutoriales; Gracias.
Pek
ahhh .. no entendí tu pregunta. Entonces, ¿estás utilizando un mosaico tradicional o tienes una imagen grande para tu nivel que dividiste en pedazos?
Solo un pensamiento ... podría almacenar los nombres de sus mosaicos en una matriz y utilizar la misma técnica para saber qué cargar y dibujar. Recuerde intentar reutilizar cualquier objeto o podría crear mucha basura
El segundo: el mapa tiene 8000x6000 dividido en 10x10 mosaicos de 800x600 px. En cuanto a la reutilización, ya tengo un administrador de contenido que es responsable de eso.
Pek
0

He estado trabajando en algo muy similar para mi proyecto actual. Este es un resumen rápido de cómo lo estoy haciendo, con algunas notas al margen sobre cómo hacer las cosas un poco más fáciles para usted.

Para mí, el primer problema fue dividir el mundo en trozos más pequeños que serían apropiados para cargar y descargar sobre la marcha. Dado que está utilizando un mapa basado en mosaicos, ese paso se vuelve significativamente más fácil. En lugar de considerar las posiciones de cada objeto 3D en el nivel, ya tiene su nivel bien dividido en mosaicos. Esto le permite simplemente dividir el mundo en trozos de mosaico X por Y, y cargarlos.

Vas a querer hacer eso automáticamente, en lugar de hacerlo a mano. Como está utilizando XNA, tiene la opción de usar Content Pipeline con un exportador personalizado para su contenido de nivel. A menos que conozca alguna forma de ejecutar el proceso de exportación sin volver a compilar, honestamente lo recomendaría. Si bien C # no es tan lento para compilar como C ++ tiende a ser, aún no desea tener que cargar Visual Studio y recompilar su juego cada vez que realice un cambio menor en el mapa.

Otra cosa importante aquí es asegurarse de utilizar una buena convención de nomenclatura para los archivos que contienen los fragmentos de su nivel. Desea poder saber que desea cargar o descargar el fragmento C y luego generar el nombre de archivo que necesita cargar para hacerlo en tiempo de ejecución. Finalmente, piense en cualquier pequeña cosa que pueda ayudarlo en el futuro. Es realmente agradable poder cambiar qué tan grande es un fragmento, volver a exportar y luego ver los efectos de eso en el rendimiento de inmediato.

En tiempo de ejecución, sigue siendo bastante sencillo. Necesitará alguna forma de cargar y descargar asincrónicamente fragmentos, pero esto depende en gran medida del funcionamiento de su juego o motor. Su segunda imagen es exactamente correcta: debe determinar qué fragmentos deben cargarse o descargarse, y hacer las solicitudes apropiadas para que ese sea el caso si aún no lo está. Dependiendo de cuántos fragmentos haya cargado al mismo tiempo, puede hacer esto siempre que el jugador cruce un límite de un fragmento a otro. Después de todo, de cualquier manera, debes asegurarte de que se cargue lo suficiente para que, incluso en el peor (razonable) tiempo de carga, el trozo todavía se cargue antes de que el jugador pueda verlo. Probablemente querrás jugar mucho con este número hasta que obtengas un buen equilibrio entre rendimiento y consumo de memoria.

En lo que respecta a la arquitectura real, querrá abstraer el proceso de cargar y descargar los datos de la memoria del proceso de determinar qué se debe cargar / descargar. Para su primera iteración, ni siquiera me preocuparía por el rendimiento de la carga / descarga y solo obtendría lo más simple que podría funcionar, y me aseguraría de que está generando las solicitudes adecuadas en el momento adecuado. Después de eso, puede ver cómo optimizar la carga para minimizar la basura.

Me encontré con mucha complejidad adicional debido al motor que estaba usando, pero eso es bastante específico de implementación. Si tiene alguna pregunta sobre lo que hice, comente y haré lo que pueda para ayudarlo.

Logan Kincaid
fuente
1
Puede usar msbuild para ejecutar la canalización de contenido fuera de Visual Studio. El segundo ejemplo de WinForms lo tiene cubierto: creators.xna.com/en-US/sample/winforms_series2
Andrew Russell
@Andrew !!!!! ¡¡¡Muchas gracias!!! ¡Estaba a punto de abandonar por completo la canalización de contenido por exactamente esa razón!
pek