我正在与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个对象的列表。有什么建议?
编辑:矩形都是随机不同的大小。
我已经改变了我的答案,以解决你的后续问题,即是否可以将其修改为生成随机非碰撞方块而不是任意矩形。我用尽可能最简单的方法做到了这一点,即后处理原始答案的矩形输出并将其内容转换为方形子区域。我还更新了可选的可视化代码以显示两种输出。显然,这种过滤可以扩展到做其他事情,例如稍微插入每个矩形或正方形以防止它们彼此接触。
我的答案避免做了许多已经发布的答案 - 即随机生成矩形,同时拒绝与已经创建的任何冲突 - 因为它本身有点慢,计算上浪费。相反,它只专注于生成那些首先不重叠的。
通过将其转换为可以非常快速地完成的区域细分问题,这使得需要做的事情变得相对简单。以下是如何实现这一目标的一种实现方式。它以一个定义外边界的矩形开始,它分成四个较小的非重叠矩形。这是通过选择半随机内部点并将其与外部矩形的四个现有角点一起使用来形成四个子部分来实现的。
大部分行动发生在quadsect()
function。内点的选择对于确定输出的外观至关重要。您可以按照自己的方式约束它,例如只选择一个会产生至少某个最小宽度或高度或不大于某个量的子矩形。在我的答案中的示例代码中,它被定义为外部矩形的宽度和高度的中心点±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
三个想法:
第一种方法失败是因为击中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)
坐标。这些对占用的空间非常小,实际上你可以在一个合理大小的文件中节省数千甚至数百万。
有用的链接:
你的问题有一个非常简单的近似值,对我来说很好:
我用它来随机生成2D地图(像塞尔达一样)。我的对象的图像小于<100 * 100>,因此我使用了大小<500 * 500>的网格,并允许每个网格中有1-6个对象。
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()
和collides()
函数
如果循环次数太多,您可能还需要减小rects的大小
你试过了吗:
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)当矩形与某物碰撞时,随机生成该矩形的子区域并尝试相反。
一个替代的伪代码,对于那些已经提到的:
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
或类似的东西。
但这样做的好处是可能会创建一些比您预期的更小的对象,并创建几乎相交(即触摸)的对象。
如果它是玩家遍历的地图,他们可能仍然无法遍历它,因为他们的路径可能被阻止。
在我的情况下,我有一个类似的问题,除了我在整个矩形内部有一些预先存在的矩形。所以必须在现有的矩形周围放置新的矩形。
我用贪婪的方法:
这需要从原始坐标空间转换到网格空间或从网格空间转换,但需要直接进行。
(请注意,直接在原始的全局矩形上运行Kadene需要很长时间。通过网格近似对我的应用来说非常快)