我想绘制一张巨大的世界地图;尺寸至少为8000×6000像素。我将其分解为10×10的800×600像素PNG图像网格。为了避免将所有内容加载到内存中,应根据播放器在网格中的位置加载和卸载图像。
例如,这是位置上的玩家(3,3)
:
当他向右移动到(4,3)
时,最左边的三个图像被释放,而右边的三个图像被分配:
网格的每个单元内可能应该有一个阈值来触发加载和卸载。加载可能应该在单独的线程中进行。
如何设计这样的系统?
我想绘制一张巨大的世界地图;尺寸至少为8000×6000像素。我将其分解为10×10的800×600像素PNG图像网格。为了避免将所有内容加载到内存中,应根据播放器在网格中的位置加载和卸载图像。
例如,这是位置上的玩家(3,3)
:
当他向右移动到(4,3)
时,最左边的三个图像被释放,而右边的三个图像被分配:
网格的每个单元内可能应该有一个阈值来触发加载和卸载。加载可能应该在单独的线程中进行。
如何设计这样的系统?
Answers:
这里有一些问题要解决。第一个是如何加载和卸载图块。默认情况下,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 []数组,这应该很简单。
您想做的事很普通。要获得有关此技巧和其他常用技术的不错的教程,请查看此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内置了此功能。您也可以研究其代码。
我正在为当前项目进行非常相似的工作。这是我的工作方式的简要介绍,并附有一些有关如何使事情变得更轻松的旁注。
对我来说,第一个问题是将世界分成更小的块,这些块适合即时加载和卸载。由于您使用的是基于图块的地图,因此此步骤变得非常容易。不必考虑每个3D对象在关卡中的位置,而是已经将关卡很好地拆分为图块。这使您可以简单地将世界分解为X x Y块,并加载它们。
您将要自动执行此操作,而不要手动执行。由于使用的是XNA,因此可以选择将内容管道与自定义导出器一起用于关卡内容。除非您知道某种无需重新编译即可运行导出过程的方法,否则我不建议您这样做。尽管C#的编译速度不如C ++慢,但是您仍然不需要每次都对地图进行较小更改时都必须加载Visual Studio并重新编译游戏。
这里的另一重要事项是确保对包含关卡块的文件使用良好的命名约定。您希望能够知道要加载或卸载块C,然后生成在运行时需要加载的文件名。最后,考虑所有可能对您有所帮助的小事情。能够更改块的大小,重新导出,然后立即查看其对性能的影响,这真是太好了。
在运行时,它仍然非常简单。您将需要某种方式来异步加载和卸载块,但这在很大程度上取决于您的游戏或引擎的工作方式。您的第二个映像是完全正确的-您需要确定应该加载或卸载哪些块,如果还没有,则发出适当的请求以使情况成立。根据您一次加载的块数,只要播放器越过一个块到下一个块,您就可以执行此操作。毕竟,无论哪种方式,您都想确保已加载了足够的数据,即使在最坏的(合理的)加载时间内,仍会在玩家看到块之前加载该块。在性能和内存消耗之间达到良好的平衡之前,您可能会想经常使用这个数字。
就实际架构而言,您将要从确定应该加载/卸载哪些内容的过程中抽象出从内存实际加载和卸载数据的过程。对于您的第一个迭代,我什至不必担心加载/卸载的性能,而只是获得可能可行的最简单方法,并确保您在适当的时间生成适当的请求。之后,您可以查看如何优化加载方式以最大程度地减少垃圾。
由于我使用的引擎,我遇到了很多额外的复杂性,但这是特定于实现的。如果您对我的工作有任何疑问,请发表评论,我会尽力帮助您。