11

我正在使用 Pygame 开发一些 2D 游戏。我需要同时随机放置多个对象,而它们不相交。我尝试了一些明显的方法,但没有奏效。

明显的方法如下(伪):

create list of objects
for object in list:
    for other object in list:
        if object collides with other object:
            create new list of objects

这种方法花了很长时间。

我试过的其他方法:

create list of objects
for object in list:
    for other object in list:
        if object collides with other object:
             remove object from list

该方法返回接近空列表。

我正在处理一个大小为 2 到 20 个对象的列表。有什么建议么?

编辑:矩形都是随机的不同大小。

4

7 回答 7

16

我已经稍微改变了我的答案,以解决您关于是否可以对其进行修改以生成随机非碰撞正方形而不是任意矩形的后续问题。我以最简单的方式做到了这一点,即对原始答案的矩形输出进行后处理,并将其内容转换为方形子区域。我还更新了可选的可视化代码以显示两种输出。显然,这种过滤可以扩展到做其他事情,比如稍微插入每个矩形或正方形,以防止它们相互接触。

我的回答避免做许多已经发布的答案所做的事情——随机生成矩形,同时拒绝任何与已经创建的任何碰撞的矩形——因为它听起来本质上很慢并且在计算上很浪费。相反,我的方法专注于只生成那些一开始不重叠的。

这使得需要做的事情变得相对简单,因为它变成了一个可以非常快速地执行的简单区域细分问题。下面是如何做到这一点的一种实现。它从一个定义外边界的矩形开始,它分为四个较小的非重叠矩形。这是通过选择一个半随机的内部点并将其与外部矩形的四个现有角点一起使用以形成四个子部分来实现的。

大多数动作发生在quadsect()函数中。内部点的选择对于确定输出的外观至关重要。您可以以任何您希望的方式对其进行约束,例如仅选择一个会导致子矩形至少具有某个最小宽度或高度或不大于某个数量的子矩形。在我回答的示例代码中,它被定义为外部矩形宽度和高度的中心点 ± 1 / 3,但基本上任何内部点都可以在某种程度上起作用。

由于该算法生成子矩形的速度非常快,因此可以花费一些计算时间来确定内部分割点。

为了帮助可视化这种方法的结果,最后有一些非必要的代码使用PIL(Python Imaging Library)模块创建一个图像文件,显示在我进行的一些测试运行期间生成的矩形。

无论如何,这是最新版本的代码和输出示例:

import random
from random import randint
random.seed()

NUM_RECTS = 20
REGION = Rect(0, 0, 640, 480)

class Point(object):
    def __init__(self, x, y):
        self.x, self.y = x, y

    @staticmethod
    def from_point(other):
        return Point(other.x, other.y)

class Rect(object):
    def __init__(self, x1, y1, x2, y2):
        minx, maxx = (x1,x2) if x1 < x2 else (x2,x1)
        miny, maxy = (y1,y2) if y1 < y2 else (y2,y1)
        self.min, self.max = Point(minx, miny), Point(maxx, maxy)

    @staticmethod
    def from_points(p1, p2):
        return Rect(p1.x, p1.y, p2.x, p2.y)

    width  = property(lambda self: self.max.x - self.min.x)
    height = property(lambda self: self.max.y - self.min.y)

plus_or_minus = lambda v: v * [-1, 1][(randint(0, 100) % 2)]  # equal chance +/-1

def quadsect(rect, factor):
    """ Subdivide given rectangle into four non-overlapping rectangles.
        'factor' is an integer representing the proportion of the width or
        height the deviatation from the center of the rectangle allowed.
    """
    # pick a point in the interior of given rectangle
    w, h = rect.width, rect.height  # cache properties
    center = Point(rect.min.x + (w // 2), rect.min.y + (h // 2))
    delta_x = plus_or_minus(randint(0, w // factor))
    delta_y = plus_or_minus(randint(0, h // factor))
    interior = Point(center.x + delta_x, center.y + delta_y)

    # create rectangles from the interior point and the corners of the outer one
    return [Rect(interior.x, interior.y, rect.min.x, rect.min.y),
            Rect(interior.x, interior.y, rect.max.x, rect.min.y),
            Rect(interior.x, interior.y, rect.max.x, rect.max.y),
            Rect(interior.x, interior.y, rect.min.x, rect.max.y)]

def square_subregion(rect):
    """ Return a square rectangle centered within the given rectangle """
    w, h = rect.width, rect.height  # cache properties
    if w < h:
        offset = (h - w) // 2
        return Rect(rect.min.x, rect.min.y+offset,
                    rect.max.x, rect.min.y+offset+w)
    else:
        offset = (w - h) // 2
        return Rect(rect.min.x+offset, rect.min.y,
                    rect.min.x+offset+h, rect.max.y)

# call quadsect() until at least the number of rects wanted has been generated
rects = [REGION]   # seed output list
while len(rects) <= NUM_RECTS:
    rects = [subrect for rect in rects
                        for subrect in quadsect(rect, 3)]

random.shuffle(rects)  # mix them up
sample = random.sample(rects, NUM_RECTS)  # select the desired number
print '%d out of the %d rectangles selected' % (NUM_RECTS, len(rects))

#################################################
# extra credit - create an image file showing results

from PIL import Image, ImageDraw

def gray(v): return tuple(int(v*255) for _ in range(3))

BLACK, DARK_GRAY, GRAY = gray(0), gray(.25), gray(.5)
LIGHT_GRAY, WHITE = gray(.75), gray(1)
RED, GREEN, BLUE = (255, 0, 0), (0, 255, 0), (0, 0, 255)
CYAN, MAGENTA, YELLOW = (0, 255, 255), (255, 0, 255), (255, 255, 0)
BACKGR, SQUARE_COLOR, RECT_COLOR = (245, 245, 87), (255, 73, 73), (37, 182, 249)

imgx, imgy = REGION.max.x + 1, REGION.max.y + 1
image = Image.new("RGB", (imgx, imgy), BACKGR)  # create color image
draw = ImageDraw.Draw(image)

def draw_rect(rect, fill=None, outline=WHITE):
    draw.rectangle([(rect.min.x, rect.min.y), (rect.max.x, rect.max.y)],
                   fill=fill, outline=outline)

# first draw outlines of all the non-overlapping rectanges generated
for rect in rects:
    draw_rect(rect, outline=LIGHT_GRAY)

# then draw the random sample of them selected
for rect in sample:
    draw_rect(rect, fill=RECT_COLOR, outline=WHITE)

# and lastly convert those into squares and re-draw them in another color
for rect in sample:
    draw_rect(square_subregion(rect), fill=SQUARE_COLOR, outline=WHITE)

filename = 'square_quadsections.png'
image.save(filename, "PNG")
print repr(filename), 'output image saved'

输出样本 1

第一个样本输出图像

输出样本 2

第二个样本输出图像

于 2010-12-07T22:27:56.863 回答
1

三个想法:

减小对象的大小

第一种方法失败了,因为碰到一个由 20 个不重叠对象组成的随机数组是非常不可能的(实际上(1-p)^20,其中0<p<1是两个对象碰撞的概率)。如果你可以显着(数量级的戏剧)减小它们的大小,它可能会有所帮助。

一一挑选

最明显的改进是:

while len(rectangles)<N:
    new_rectangle=get_random_rectangle()
    for rectangle in rectangles:
        if not any(intersects (rectangle, new_rectangle) for rectangle in rectangles)
            rectangles.add(new_rectangle)

这将大大提高您的性能,因为只有一个交叉点不会强迫您生成一个全新的集合,而只是选择一个不同的单个矩形。

预计算

您多久会在游戏中使用这些套装?每秒使用一组不同的场景与每小时使用一组不同。如果您不经常使用这些集合,请预先计算足够大的集合,以便游戏玩家可能永远不会看到相同的集合两次。预先计算时,您不会太在意所花费的时间(因此您甚至可以使用效率低下的第一个算法)。

即使您在运行时确实需要这些矩形,在 CPU 出于某种原因空闲时,最好在需要它们之前先计算一下,这样您就可以随时准备好一组。

在运行时,只需随机选择一组。这可能是实时游戏的最佳方法。

注意: 此解决方案假定您的矩形以节省空间的方式保存,例如(x, y)坐标对。这些对占用的空间非常小,您实际上可以在一个合理大小的文件中保存数千甚至数百万。

有用的链接:

于 2010-12-07T05:52:59.933 回答
1

您的问题有一个非常简单的近似值,对我来说效果很好:

  • 定义一个网格。例如,一个 100 像素的网格写入 (x,y) -> (int(x/100),int(y/100))。网格元素不重叠。
  • 要么将每个对象放在不同的网格中(随机在网格内看起来更漂亮),要么在每个网格中随机放置一些对象,如果可以允许一些对象重叠的话。

我用它来随机生成一个 2D 地图(类似塞尔达)。我的对象的图像小于 <100*100>,所以我使用大小为 <500*500> 的网格,每个网格允许 1-6 个对象。

于 2011-09-09T14:24:09.027 回答
0
list_of_objects = []
for i in range(20):
    while True:
        new_object = create_object()
        if not any(collides(new_object, x) for x in list_of_objects):
            break
    list_of_objects.append(new_object)

我假设您已经拥有create_object()andcollides()功能

如果循环次数过多,您可能还需要减小矩形的大小

于 2010-12-07T05:51:26.710 回答
0

你试过了吗:

Until there are enough objects:
    create new object
    if it doesn't collide with anything in the list:
        add it to the list

重新创建整个列表或取出与碰撞有关的所有内容都没有意义。

另一个想法是通过以下任一方法“修复”碰撞:

1)找到相交区域的中心,并将每个相交矩形的适当角调整到该点,使它们现在接触角/边缘而不是相交。

2)当一个矩形与某物碰撞时,随机生成该矩形的一个子区域并尝试代替。

于 2010-12-07T05:58:36.747 回答
0

已经提到的替代伪代码:

while not enough objects:
  place object randomly
  if overlaps with anything else:
    reduce size until it fits or has zero size
  if zero size: 
    remove

或类似的东西。

但这有可能创建一些比您预期的更小的对象,并创建几乎相交(即触摸)的对象。

如果这是一张供玩家穿越的地图,他们可能仍然无法穿越它,因为他们的路径可能会被阻塞。

于 2010-12-07T07:54:48.797 回答
0

就我而言,我遇到了类似的问题,只是我在整个矩形内有一些预先退出的矩形。所以必须在现有的矩形周围放置新的矩形。

我使用了一种贪婪的方法:

  • 矩形化整个(全局)矩形:从到目前为止所有矩形的排序 x 和排序 y 坐标创建一个网格。所以这会给你一个不规则(但矩形)的网格。
  • 对于每个网格单元计算面积,这会给你一个面积矩阵。
  • 使用Kadanes 2D算法找到给你最大面积的子矩阵(= 最大的自由矩形)
  • 在该空闲空间中放置一个随机矩形
  • 重复

这需要从您的原始坐标空间到网格空间的转换,但很简单。

(请注意,直接在原始全局矩形上运行 Kadene 需要很长时间。通过网格近似对我的应用程序来说非常快)

于 2017-10-17T11:20:08.953 回答