【问题标题】:Randomly generate blocks on a flat map在平面地图上随机生成块
【发布时间】:2016-05-15 16:02:42
【问题描述】:

我正在尝试在平面地图上随机生成块并使其不会相互重叠。 我制作了地图大小(500x500)的矩阵(c# 数组),块的比例在 1 到 5 之间。 代码有效,但如果生成的块与另一个块重叠,它会被销毁,不会在其他地方重新生成。

我尝试生成的 1000 个块中只有大约 80 个不会与另一个块重叠。

这是一张地图图片,生成了大约 80 个方块,绿色方块是方块

void generateElement(int ratio, int minScale, int maxScale, GameObject g) {
    bool elementFound = false;
    for (int i = 0; i < ratio * generationDefault; i++) {
        GameObject el;
        // Randomly generate block size and position
        int size = Random.Range(minScale, maxScale + 1);
        int x = Random.Range(0, mapSizex + 1 - size);
        int y = Random.Range(0, mapSizey + 1 - size);

        // Check if there is already an element 
        for (int j = x; j < x + size; j++)
            for (int k = y; k < y + size; k++)
                if (map[j][k] != null)
                    elementFound = true;
        if (elementFound)
            continue;
        else {
            el = (GameObject)Instantiate(g, new Vector3(x + (float)size / 2, (float)size / 2, y + (float)size / 2), Quaternion.Euler(0, 0, 0));
            el.transform.localScale *= size;
        }
        // Create element on map array
        for (int j = x; j < x + size; j++)
            for (int k = y; k < y + size; k++)  
                if (map[j][k] == null) {
                    map[j][k] = el.GetComponent<ObjectInterface>();
                }
    }
}

我想到了 3 个可能的修复方法

  • 我应该根据块的位置来设置块的大小。
  • 我应该使用另一种随机化算法。
  • 我做的不对。

你认为最好的主意是什么?


更新

我的代码工作得更好了。如果需要,我现在尝试多次实例化块(目前最多 5 个)并修复了错误。如果地图上已经有很多元素,它们不会总是被实例化,这就是我想要的,我只需要找到它尝试实例化块的正确次数。

我尝试在 500x500 的地图上实例化 1280 个元素。它只需要大约 1.5 秒,并且实例化了 1278/1280 个块 (99.843%)。

void generateElement(int ratio, int minScale, int maxScale, GameObject g) {
bool elementFound = false;
int cnt = 0;
// Generate every block
for (int i = 0; i < ratio * generationDefault; i++) {
    GameObject el = null;
    // Randomly generate block size and position
    int size, x, y, tryCnt = 0;

    // Try maximum 5 times to generate the block
    do {
        elementFound = false;
        // Randomly set block size and position
        size = Random.Range(minScale, maxScale + 1);
        x = Random.Range(0, mapSizex + 1 - size);
        y = Random.Range(0, mapSizey + 1 - size);

        // Check if there is already an element 
        for (int j = x; j < x + size; j++)
            for (int k = y; k < y + size; k++)
                if (map[j][k] != null)
                    elementFound = true;
        tryCnt++;
    } while (elementFound && tryCnt < 5);
    if (tryCnt >= 5 && elementFound) continue;

    // Instantiate the block
    el = (GameObject)Instantiate(g, new Vector3(x + (float)size / 2, (float)size / 2, y + (float)size / 2), Quaternion.Euler(0, 0, 0));
    el.transform.localScale *= size;
    // Create element on map array
    for (int j = x; j < x + size; j++)
        for (int k = y; k < y + size; k++)  
            if (map[j][k] == null) {
                map[j][k] = el.GetComponent<ObjectInterface>();
            }
    cnt++;
}
print("Instantiated " + cnt + "/" + ratio * generationDefault);

}

【问题讨论】:

  • 我能想到的一种方法(尽管它需要一些额外的数学运算),是将地图区域划分为区域网格,等于您想要放下的块数.然后,在每个区域中随机选择一个位置(考虑到块的预期比例)并在那里放置一个块。不幸的是,您还会遇到这种方法的“规律性”问题(特别是当块数相对于地图大小增加时)......不过,根据您的需要,一致的平均人口密度可能不会那么糟糕。 =P
  • 嗨,Serlite。这是一种非常有名的方法。确实,这正是我在下面的答案中的内容。正如您敏锐地观察到的那样,您会根据所涉及的尺寸获得“常规外观”的结果(有时很好)。一个简单的解决方案是扰动块 - 下面给出的代码。笔记!事实上,另一种方法就是这么简单......只需将它们均匀分布(甚至不要随机化位置)然后进行扰动。只需尝试 1、2、3 或更多“扰动”,看看效果如何。

标签: c# dictionary random unity3d generator


【解决方案1】:

很难做好

这里有一个您可能会喜欢的快速解决方案……取决于您的场景。

actualWidth = 500 //or whatever. assume here is square
// your blocks are up to 5 size
chunkWidth = actualWidth / 5
// it goes without saying, everything here is an int
kChunks = chunkWidth*chunkWidth
List<int> shuf = Enumerable.Range(1,kChunks).OrderBy(r=>Random.value).ToList();
howManyWanted = 1000
shuf = shuf.Take(howManyWanted)
foreach( i in shuf )
   x = i % actualWidth
   y = i / actualWidth
   make block at x y
   put block in list allBlocks

但是…………


......你会看到这看起来有点“常规”,所以这样做:

只是随机扰动所有的块。请记住,视频游戏编程关乎巧妙的技巧!

理想情况下,你必须从中间开始,然后走出去;在任何情况下,您都不能将它们排成一行。洗牌没问题。所以,这样做..

   harmonic = 3  //for example. TRY DIFFERENT VALUES
   
   function rh = Random.Range(1,harmonic) (that's 1 not 0)
   
   function rhPosNeg
       n = rh
       n = either +n or -n
       return n
   
   function onePerturbation
   {
   allBlocks = allBlocks.OrderBy(r => Random.value) //essential
   foreach b in allBlocks
      newPotentialPosition = Vector2(rhPosNeg,rhPosNeg)
      possible = your function to check if it is possible
           to have a block at newPotentialPosition,
           however be careful not to check "yourself"
      if possible, move block to newPotentialPosition
   }

最简单的方法就是运行onePerturbation,比如说,三遍。在每次运行之间查看它。还可以尝试不同的 harmonic 调整因子值。

有很多方法可以扰乱不同大小块的字段,上面是一个 KISS 解决方案,希望看起来适合您的情况。


编码说明...

如何获得一组唯一的随机数。

只是为了解释这行代码...

List<int> shuf = Enumerable.Range(1,kChunks).OrderBy(r=>Random.value).ToList();

如果您是编码新手:假设您想这样做:“获得一百个随机数,从 1 到百万,但没有重复”。

幸运的是,这是一个众所周知的问题,有一个非常简单的解决方案

获得不重复数字的方法是将所有数字随机排列,然后从顶部取出你想要的数字。

例如,假设您需要从 1 到 10 的随机数,但不能重复。

所以,这是 1-10 的数字:3,8,6,1,2,7,10,9,4,5

简单地把你需要的东西从前面拿走:所以,3、8、6 等等。

举个例子,假设你想要从 1 到 75 的 12 个数字,没有重复。所以第一个问题是,你想要一个包含不超过 75 的所有数字的列表,但是被打乱了。事实上,你是这样做的..

List<int> shuf = Enumerable.Range(1,75).OrderBy(r=>Random.value).ToList();

所以该列表有 75 项长。您可以通过说foreach(int r in shuf) Debug.Log(r); 来检查它。接下来在示例中,您只需要其中的 12 个数字。幸运的是,有一个 List 调用可以做到这一点:

shuf = shuf.Take(12)

就是这样 - 你现在有 12 个数字,没有重复,都是 1 到 75 之间的随机数。你可以再次检查 foreach(int r in shuf) Debug.Log(r);

简而言之,当你想要在 1 和 Max 之间没有重复的“n”个数字时,你所要做的就是:

List<int> shuf = Enumerable.Range(1,Max).OrderBy(r=>Random.value).ToList();
shuf = shuf.Take(n);

等等,你可以用foreach(int r in shuf) Debug.Log(r);查看结果

我只是详细解释了这一点,因为这个问题经常被问到“如何获得唯一的随机数”。这是一个“古老”的编程技巧,答案很简单,就是shuffle一个包含所有相关整数的数组。

有趣的是,如果您在谷歌上搜索这个问题(“如何获得唯一的随机数”),这是 google 帮助不大的少数情况之一,因为:每当有人问这个问题时,您会遇到大量热心的新程序员(他们还没有听说过正确执行此操作的简单技巧!)写出又长又复杂的想法,从而导致进一步的混乱和复杂化。

这就是你制作没有重复的随机数的方法,幸运的是它很简单。

【讨论】:

  • 你好,我喜欢块的想法,但问题是我会生成 3-4 种不同类型的块,但我可能可以调整这段代码。对于扰动,我的块有不同的大小(如果一个块的比例是2,它将在地图数组中占据4个位置)
  • 嗨服务。在显示 harmonic = 3 的伪代码中,它会尝试将每个单独的摇晃到最大尺寸。只需尝试不同的值。
【解决方案2】:

if (elementFound) continue; 将跳过当前循环迭代。你需要包装int x=Random..; int y=Random()..;参与while 循环,条件为while(/* position x/y already occupued*/) { /* generate new valid point */},例如:

void generateElement(int ratio, int minScale, int maxScale, GameObject g) {
    for (int i = 0; i < ratio * generationDefault; i++) {
        GameObject el;
        // Randomly generate block size and position

        bool elementFound = false;
        int size, x, y;
        do
        {
            elementFound = false;
            size = Random.Range(minScale, maxScale + 1);
            x = Random.Range(0, mapSizex + 1 - size);
            y = Random.Range(0, mapSizey + 1 - size);

            // Check if there is already an element 
            for (int j = x; j < x + size; j++)
                for (int k = y; k < y + size; k++)
                    if (map[j][k] != null)
                        elementFound = true;
        } while(elementFound);

        el = (GameObject)Instantiate(g, new Vector3(x + (float)size / 2, (float)size / 2, y + (float)size / 2), Quaternion.Euler(0, 0, 0));
        el.transform.localScale *= size;

        // Create element on map array
        for (int j = x; j < x + size; j++)
            for (int k = y; k < y + size; k++)  
                if (map[j][k] == null) {
                    map[j][k] = el.GetComponent<ObjectInterface>();
                }
    }
}

【讨论】:

  • 迭代次数是不确定的,但是算法不需要太多的迭代就能找到一个有效的地方,尤其不是一百万次迭代。它选择一个随机大小 x 和 y 并检查它在地图中是否有效。如果没有,它会选择另外几个数字。如果地图在每个地方都已满,这是非终止的。我把它留给读者在算法的开头添加一个检查。
  • 但是请注意,正如我所说,如果数字是正确的,这就是你要做的......基本上如果它相当稀疏。但是你必须非常小心,因为它是一个不确定的算法。事实上,在你不断寻找的循环中,你必须有一个安全计数,并且最多只能尝试 100 次(或某个安全限制)。
  • Joe,看看我分享的这个 tutorialspoint 项目。 goo.gl/llBa4l 它模拟一个 1000x1000 的地图,并在该地图上生成 200 个随机大小的随机元素,并为您记录迭代次数。如您所见,它几乎总是需要 1 次迭代,但您也可以看到一些需要 2 次迭代的行。您可以生成更多元素,有时您会发现它需要 3 次迭代。如您所见,这反驳了您声称需要“一百万次迭代”才能完成的说法,新的随机位置和大小已经满的概率非常低。
  • 当然,如果地图已满或无法在地图上放置所需大小的对象(例如,最多 20 次迭代尝试),它会错过安全锁,但实现这一点相当简单.这只是纠正了continue 跳过迭代并且没有重试生成新随机元素的错误。
  • 您需要将“循环安全限制”添加到您在互联网上看到的代码中。这是最基本的空间填充算法,打印时不会出错!
【解决方案3】:

您不应该遇到那么多冲突。

假设您的块都是 5 个单位宽,并且您试图将它们放入 500,500 的网格中,那么您至少有 100*100 个空间可供它们使用,这样就有 10,000 个空间可以容纳 1,000 个块。

尝试使用此代码:

using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main()
        {
            var result = PlaceNonOverlappingBlocks(1000, 5, 500, 500);
        }

        static List<Block> PlaceNonOverlappingBlocks(int count, int maxBlockSize, int mapX, int mapY)
        {
            var map    = new bool[mapY, mapX];
            var rng    = new Random();
            var result = new List<Block>(count);
            int collisions = 0; 

            while (count > 0)
            {
                int size = rng.Next(1, maxBlockSize + 1);
                int x = rng.Next(0, mapX - size);
                int y = rng.Next(0, mapY - size);

                if (fits(map, x, y, size))
                {
                    result.Add(new Block(x, y, size));
                    addToMap(map, x, y, size);
                    --count;
                }
                else
                {
                    if (++collisions> 100000)
                        throw new InvalidOperationException("Hell has frozen over");
                }
            }

            // This is just for diagnostics, and can be removed.
            Console.WriteLine($"There were {collisions} collisions.");

            return result;
        }

        static void addToMap(bool[,] map, int px, int py, int size)
        {
            for (int x = px; x < px+size; ++x)
                for (int y = py; y < py + size; ++y)
                    map[y, x] = true;
        }

        static bool fits(bool[,] map, int px, int py, int size)
        {
            for (int x = px; x < px + size; ++x)
                for (int y = py; y < py + size; ++y)
                    if (map[y, x])
                        return false;

            return true;
        }

        internal class Block
        {
            public int X    { get; }
            public int Y    { get; }
            public int Size { get; }

            public Block(int x, int y, int size)
            {
                X = x;
                Y = y;
                Size = size;
            }
        }
    }
}

【讨论】:

  • 如果我没记错的话,这与 Max 的相同(但代码更简洁!:))
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-29
  • 1970-01-01
  • 2014-02-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多