【问题标题】:Most efficient way of using multidimensional array indexers使用多维数组索引器的最有效方法
【发布时间】:2013-08-12 16:55:56
【问题描述】:

假设您有一个 2D 瓷砖网格(这是基于 2D 瓷砖的游戏),大多数瓷砖占据 1 个位置,但一些较大的“对象”可以填充多个位置。我在我的数组上使用索引器来自动将这些对象“引用”到它们的基本图块。因此,如果我在 3,4 处有一个 2x2 对象,并且我访问 4,4,它将自动重定向并获取 3,4 处的图块。但是,如果我需要获取确切的图块,我可以指定一个参数来绕过此功能。 (GameDev 上的my old question 对此有更好的解释)

另一种看待它的方式是游戏世界中的门对象,用户可以单击它的任意位置来打开它,但每个单独的部分都可以包含其他属性,例如不同的背景和照明值。

注意我只是一个业余程序员,所以这可能不对(以及为什么我要征求你的意见)每个“超大”磁贴都会以X,Y 位置的形式存储对其基础磁贴的引用(这应该是对内存中实际对象的引用吗?)

public class TileWrapper
{
    public int Width = 0;
    public int Height = 0;
    private Tile[] tiles; //Backing Store
    public TileWrapper()
    {
        tiles = new Tile[Width * Height]; 
    }
    public TileWrapper(int width, int height)
    {
        Width = width;
        Height = height;
        tiles = new Tile[Width * Height];
    }
    /// <summary>
    /// Accessor for tiles
    /// </summary>
    /// <param name="x">X Position</param>
    /// <param name="y">Y Position</param>
    /// <param name="overide">Bool to "override" the get, if true, it wont get the reference tile and will bypass the checks</param>
    public Tile this[int x, int y, bool override = false]
    {
        get 
        {
             //If we do not want to bypass the checks, AND the current tile is > than 1x1
             if (!override && tiles[y * Width + x].IsLarge)
                return tiles[tiles[y * Width + x].refY * Width + tiles[y * Width + x].refX]; //Use the reference positions to get the main position of the tile
             //If we want to bypass the checks or the tile wasn't large, get the absolute position
             else 
                 return tiles[y * Width + x];
        }
        set 
        {
             //Same thing for SET
             if (!override && tiles[y * Width + x].IsLarge) //Set base tile if the large tile has a reference
                 tiles[tiles[y * Width + x].refY * Width + tiles[y * Width + x].refX] = value;
             else  //Set absolute tile
                  tiles[y * Width + x] = value;
        }
    }

抱歉,如果使用 2D 到 1D 转换有点难以阅读,但经过一些测试后,内部使用 1D 数组似乎要快一些。

IsLarge 是一个属性,用于简单地检查图块是否很大(大于 1x1)

我已经有适当的逻辑来在放置大图块时填充相邻图块的引用,并相应地删除它们。

在分析游戏时,我发现 tile 的 get 访问器占用了大量 CPU,每帧获取数百次 tile 用于照明、渲染、碰撞等。

如何提高这段代码的性能和效率?

基准测试(英特尔四核 i7 2670QM 上 30k 次迭代的平均值)

Tile t = tiles[100, 100]; - 160 ns 和 175 ns 带 2D 内部阵列

Tile t = tiles[100, 100, true]; - 137 ns 和 264 ns 带 2D 内部阵列(奇数)

100,100 顺便说一下不是大图块,注意这些图块不是很常见。我在屏幕上有一座房子,你可以有几块大瓷砖(门、桌子),但有很多泥土/石头/木头。

【问题讨论】:

  • 我的建议是重载索引器,而不是使用布尔值的默认参数。也许对其中一个使用命名方法。然后,您可以消除数组访问中的分支。
  • 目前使用这个的代码需要多长时间?需要多长时间才能达到您的目的“足够快”?您有什么迹象表明这是当前应用程序性能的瓶颈?
  • @lukegravitt 我以前做过,没有任何收获/损失@Servy 几分钟,我会回顾分析器的结果
  • Tile 是结构还是类?
  • 我肯定会使用对内存中对象的引用。这样您就可以删除额外的计算和递归方法调用。

标签: c# arrays performance xna accessor


【解决方案1】:

我建议为每个逻辑上连接在一起的正方形集合创建一个类似的实例:

private class TileRef { public Tile tile; public int X,Y,Width,Height;}

然后有一个二维或锯齿状的 TileRef 数组。连接在一起的组中的所有方格都应包含对同一 TileRef 对象的引用。这应该可以让您非常快速地找到与板上任何方块关联的TileRef,而无需条件逻辑来处理不同大小的图块。

构建一个大小为 1 的图块的网格:

TileRef[,] tiles;
TileMapper(int xsize, int ysize)
{
  tiles = new TileRef[xsize,ysize];
  for (int x=0; x < xsize; x++)
    for (int y=0; y < xsize; y++)
    {
      var thisRef = new TileRef();
      thisRef.X = x;
      thisRef.Y = y;
      thisRef.Width = 1;
      thisRef.Height = 1;
      thisRef.Tile = new Tile(); // Make a default tile instance somehow
      tiles[x][y] = thisRef;
    }
}

To join a bunch of squares together into a blob:

public JoinSquares(int x, int y, int width, int height)
{
      var thisRef = new TileRef();
      thisRef.X = x;
      thisRef.Y = y;
      thisRef.Width = 1;
      thisRef.Height = 1;
      thisRef.Tile = new Tile(); // Make a default tile instance somehow
      for (i=0; i<width; i++)
        for (j=0; j<height; j++)
          tiles[x+i,y+j] = thisRef;
}

public SeparateSquares(int x, int y)
{
      var oldRef = tiles[x,y];
      x=oldref.X;
      y=oldref.Y;
      var width=oldref.Width;
      var height=oldref.Height;

      for (i=0; i<width; i++)
        for (j=0; j<height; j++)
        {
          var thisRef = new TileRef();
          thisRef.X = x+i;
          thisRef.Y = y+j;
          thisRef.Width = 1;
          thisRef.Height = 1;
          thisRef.Tile = new Tile(); // Make a default tile instance somehow
          tiles[x+i,y+j] = thisRef;
        }
}

To change the `Tile` associated with a "blob", simply

public Tile this[int x, int y]
{
    get 
    {
        return tiles[x,y].Tile;
    }
    set
    {
        tiles[x,y].Tile = value;
    }
}

需要循环来将正方形连接在一起或将它们分开,而不是通过更改 blob 的 Tile 属性来简单地更改与 blob 关联的属性。

【讨论】:

  • 我认为你可以在Tile 类本身上引用主要的Tile,如果IsLarge 为假,它只会指向它自己。不需要第二组数据。
  • @lukegravitt:如果希望set 方法以指定的方式可用,则需要第二级间接。否则,如果在 (3,6) 处有一个左上角的 3x3 瓷砖,尝试在坐标 (4,7) 上调用 set 会导致该方块指向新对象,而周围的方块指向旧对象一。
  • set 方法在这种情况下肯定会变得更复杂,但您不一定需要下一个级别。您可以获得引用的Tile 并迭代其扩展,并根据需要进行调整。不过,这个用例是有道理的,因为您有效地分解了组,因此需要访问每个 Tile。
  • @lukegravitt:如果将Tile 存储到某个位置应该更新较大图块的所有部分并且没有额外的间接级别,则映射对象必须知道哪些正方形需要更新。用于该目的的信息应为映射对象“所有”;没有其他东西可以修改它。添加第二层间接避免了循环的需要,并允许映射器对象保持其信息私有。
  • 如果您可以向我展示代码以在没有循环的情况下执行您的建议,我会印象深刻。
猜你喜欢
  • 2023-01-11
  • 2014-04-11
  • 1970-01-01
  • 2021-03-23
  • 2020-10-25
  • 1970-01-01
  • 1970-01-01
  • 2018-01-22
  • 2013-02-13
相关资源
最近更新 更多