在XNA中,如何动态加载大型2D世界地图的各个部分?


17

我想绘制一张巨大的世界地图;尺寸至少为8000×6000像素。我将其分解为10×10的800×600像素PNG图像网格。为了避免将所有内容加载到内存中,应根据播放器在网格中的位置加载和卸载图像。

例如,这是位置上的玩家(3,3)

玩家于(3,3)

当他向右移动到(4,3)时,最左边的三个图像被释放,而右边的三个图像被分配:

玩家移至(4,3)

网格的每个单元内可能应该有一个阈值来触发加载和卸载。加载可能应该在单独的线程中进行。

如何设计这样的系统?


1
快速建议:您在这里有两个问题,“如何在游戏中动态加载图块”和“如何在XNA for XBox 360上进行线程化”。从这篇博文中删除特定于操作系统的部分-在XB360,Linux和Nintendo Wii上,tileloading代码将是相同的-并在XBox360上发布新的线程化博文。
ZorbaTHut 2010年

关于您的实际问题,这是一张平滑滚动的地图,还是一系列单独的非滚动屏幕?
ZorbaTHut 2010年

如果我了解“平滑滚动地图”,是的,是。至于分离问题,我考虑了一下,但犹豫了要提出问题。但是,我现在就做。
pek

Answers:


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的图块会使游戏暂停。
pek

据我所知,创建Texture2D对象时需要使用图形卡,据我所知,即使在第二个线程上完成加载,它仍然会导致跳过。
肖恩·詹姆斯

0

您想做的事很普通。要获得有关此技巧和其他常用技术的不错的教程,请查看此tile引擎系列

如果您之前没有做过类似的事情,建议您观看该系列。但是,如果您愿意,可以获取最后的教程代码。如果稍后再做,请检查draw方法。

简而言之,您需要找到玩家周围的最小和最大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 800x600切片。我将看一下视频教程;谢谢。
pek

啊..我误解了你的问题。那么,您是使用传统的tilemap,还是将您的关卡分解为大块图像?

只是一个想法……您可以将瓦片的名称存储在一个数组中,并使用相同的技术来知道要加载和绘制的内容。请记住尝试重用任何对象,否则可能会创建大量垃圾

第二个:地图是8000x6000,分为10x10个800x600像素的图块。至于重用,我已经有一个内容管理器负责。
pek

0

我正在为当前项目进行非常相似的工作。这是我的工作方式的简要介绍,并附有一些有关如何使事情变得更轻松的旁注。

对我来说,第一个问题是将世界分成更小的块,这些块适合即时加载和卸载。由于您使用的是基于图块的地图,因此此步骤变得非常容易。不必考虑每个3D对象在关卡中的位置,而是已经将关卡很好地拆分为图块。这使您可以简单地将世界分解为X x Y块,并加载它们。

您将要自动执行此操作,而不要手动执行。由于使用的是XNA,因此可以选择将内容管道与自定义导出器一起用于关卡内容。除非您知道某种无需重新编译即可运行导出过程的方法,否则我不建议您这样做。尽管C#的编译速度不如C ++慢,但是您仍然不需要每次都对地图进行较小更改时都必须加载Visual Studio并重新编译游戏。

这里的另一重要事项是确保对包含关卡块的文件使用良好的命名约定。您希望能够知道要加载或卸载块C,然后生成在运行时需要加载的文件名。最后,考虑所有可能对您有所帮助的小事情。能够更改块的大小,重新导出,然后立即查看其对性能的影响,这真是太好了。

在运行时,它仍然非常简单。您将需要某种方式来异步加载和卸载块,但这在很大程度上取决于您的游戏或引擎的工作方式。您的第二个映像是完全正确的-您需要确定应该加载或卸载哪些块,如果还没有,则发出适当的请求以使情况成立。根据您一次加载的块数,只要播放器越过一个块到下一个块,您就可以执行此操作。毕竟,无论哪种方式,您都想确保已加载了足够的数据,即使在最坏的(合理的)加载时间内,仍会在玩家看到块之前加载该块。在性能和内存消耗之间达到良好的平衡之前,您可能会想经常使用这个数字。

就实际架构而言,您将要从确定应该加载/卸载哪些内容的过程中抽象出从内存实际加载和卸载数据的过程。对于您的第一个迭代,我什至不必担心加载/卸载的性能,而只是获得可能可行的最简单方法,并确保您在适当的时间生成适当的请求。之后,您可以查看如何优化加载方式以最大程度地减少垃圾。

由于我使用的引擎,我遇到了很多额外的复杂性,但这是特定于实现的。如果您对我的工作有任何疑问,请发表评论,我会尽力帮助您。


1
您可以使用msbuild在Visual Studio外部运行内容管道。第二个WinForms示例涵盖了您:creators.xna.com/zh-CN/sample/winforms_series2
Andrew Russell 2010年

@安德鲁 !!!!!非常感谢!!!正是由于这个原因,我将完全放弃内容管道!
pek
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.