我如何在2D平铺引擎中支持大于单个拼贴的对象?

我目前正在研究一个包含等轴测地图的二维引擎。 它运行得很好,但我不确定是否select了这种发动机的最佳方法。 为了给你一个想法,现在我们来看一下瓷砖贴图及其对象的基本对象:

public class TileMap { public List<MapRow> Rows = new List<MapRow>(); public int MapWidth = 50; public int MapHeight = 50; } public class MapRow { public List<MapCell> Columns = new List<MapCell>(); } public class MapCell { public int TileID { get; set; } } 

有了这些对象,就可以将一个图块分配给一个MapCell。 我想要我的引擎支持的就像有一组MapCells,因为我想添加对象到我的瓷砖地图(例如一个大小为2×2瓷砖的房子)。 我该怎么做? 我应该编辑我的MapCell对象,它可能有其他相关的瓷砖的引用,以及如何find一个对象,同时点击单个MapCells? 或者我应该做另一种方法使用一个全局容器,其中的所有对象?

我将要谈论devise和性能。

性能

contenance

你现在所称的是一个Jagged Array,它是灵活的,但在这里没有任何用处。

我的意见是,你应该将存在的对象(tile)存储到你的世界中,作为数组的一部分,以减less堆的浪费,内存碎片,增加局部性,减less间接性,完全避免参考开销。 不幸的是,你似乎正在使用java或C#,这将不允许你控制任何这些方面。 Jave / C#引用比C ++ shared_ptr快,但比原始指针慢,肯定比就地对象慢。 分配有缺点,因为在java / c#evreything必须单独分配你有一种艰难的运气情况。 但是不要误解我的意思,直到你的游戏成为双头4k分辨率,并且必须管理巨大而丰富的地形,今天的桌面机器不会被这些细节所困扰。

研究

  1. 迭代:通常在游戏中你需要2件事:快速迭代和快速的基于局部性的访问。 快速迭代是最好的与内存连续的容器,就像我之前说过的,例如std::vector ,在java / C#中,你无法避免你的容器将无论如何都存储引用的事实,所以你松散就地对象优势的迭代。 如果您绝对需要考虑所有元素(例如生物实体的逻辑更新)来运行IA例程,则迭代非常有用。 或者,对于装饰元素的animation来更新他们当前的帧,即使这是可以讨论的,animation更新也可以在渲染过程中完成,因为如果瓦片不在屏幕上,可以决定animation没有任何用途,在这种情况下,第二种方法将会足够的研究方法。
  2. 快速的基于局部的访问。 对于任何游戏对象来说,这是最有用也是最常见的访问方式,无论是装饰瓷砖还是生物实体。 您必须能够快速检索与某个任意区域重叠的内容列表。 为此,必须将数据存储在网格中,其中双重索引i,j表示瓦片位置的完整matrix,并且每个单元格可以是空的以承载单元格。 你需要为这个方法分配一个巨大的基础网格,但是它是最快的,因为基于局部的是瞬时的。 你只需要把你的像素坐标,添加屏幕视图偏移量,除以瓷砖大小,轮到下一个较小的整数,你有你的直接索引在容器中。 在世界范围很大的情况下,这是不切实际的。 在这种情况下,有些人会使用四叉树。 我更喜欢使用哈希映射。 只需要为一对整数(vector2)创建一个散列函数,并以这种方式存储您的tile ID。 访问几乎是不变的,并且没有指向子节点的游戏,特别是对于游戏环境而言优选的开放地址哈希映射,因为如果事先已知大小的话,它们不分割内存并且更高效。

将id(索引)存储到您的空间分区结构中,这样您可以获得只有一个数组的主存储区以及将id存储到主数组中的基于位置的存储区。

如果你不得不删除你的世界中的东西,只要保持一个平行arrays来存储这些洞的索引。 这将被称为freelist。 有些人更喜欢用数组中的最后一个元素来交换元素,然后popup。 这也很棒,但是已经持有最后一个对象的指针/引用/索引的人必须更新,这可能很困难。 特别是你的空间分区结构是持有指数,所以这至less要更新。 在这方面,freelist方法更简单快捷。 下一次,你想添加一个对象,只需selectfreelist中的最后一个索引,并将其popup。 如果在你的世界中添加了太多的对象,并且主arrays变满,立即崩溃游戏,find一个新的适当的大小,并重新编码新的大小。 dynamicresize是不可取的,游戏场景控制良好,只需执行“configuration文件”运行即可确定最大限制是多less。 如果您的引擎将被运送到您不知道他们将使用哪种场景的客户端,请将此常量显示在构建设置中。 (如一些CMakevariables),并logging下来。

devise

在devise方面,锯齿arrays有维护问题,因为它强制重复代码遵循行和列方面的双重间接,这实际上没有任何帮助。 线性方法具有灵活性的优点,如果必须将其更改为哈希映射,则索引仍然可以用作标识符,以便其他系统不会受到影响。 或者更疯狂的,一个可以stream式处理的虚拟容器。 指数可以在一个需要(无限?)漫游的世界中随时加载,因此容器可能特别复杂,但是标识符在这个意义上创建了一个很好的松耦合。

另外当你反序列化时,你不必考虑行列的顺序,只要按照命运决定的顺序推送到数组,就会很酷。 无论如何,地点访问都是由不同的结构加速的。 祝你好运

忽略事实上,你使用的数组看起来像数组的数据..

只需将TileID存储在多行中,即可放置单元格。

这样,如果你做一些find最接近的/ A *任何边/边将被发现。

如果你想要成为一个主要的单元格,否定了次要的单元格,所以你不要在区域searchfunction中重复计数,但仍然知道它是types的。 给你使用一个int TitleID你不会丢失任何东西从你的ID空间。