Как я могу случайным образом разместить несколько не конфликтующих прямоугольников?

Я работаю над некоторыми 2D-играми с Pygame. Мне нужно разместить несколько объектов одновременно случайным образом, чтобы они не пересекались. Я пробовал несколько очевидных методов, но они не сработали.

Следуют очевидные методы (псевдо):

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 объектов. Какие-либо предложения?

РЕДАКТИРОВАТЬ: Все прямоугольники случайны и разного размера.


person John    schedule 07.12.2010    source источник
comment
У прямоугольников случайные размеры? Предварительно определенные размеры? Все одного размера?   -  person sje397    schedule 07.12.2010
comment
Какое разрешение холста и размеры объектов?   -  person Kabie    schedule 07.12.2010


Ответы (7)


Я немного изменил свой ответ, чтобы ответить на ваш последующий вопрос о том, можно ли его изменить, чтобы вместо него генерировались случайные, не конфликтующие квадраты, а не произвольные прямоугольники. Я сделал это самым простым способом, который мог сработать, заключался в постобработке прямоугольного вывода моего исходного ответа и преобразовании его содержимого в квадратные подобласти. Я также обновил дополнительный код визуализации, чтобы показать оба вида вывода. Очевидно, этот вид фильтрации может быть расширен, чтобы делать другие вещи, например, слегка вставлять каждый прямоугольник или квадрат, чтобы они не касались друг друга.

В моем ответе я избегаю того, что делают многие из уже опубликованных ответов - случайного создания прямоугольников, отклоняя любые, которые конфликтуют с уже созданными, - потому что это звучит по своей сути медленным и расточительным с точки зрения вычислений. Вместо этого мой подход концентрируется только на создании тех, которые вообще не пересекаются.

Это делает то, что необходимо сделать, относительно простым, превращая его в простую задачу разделения области, которую можно решить очень быстро. Ниже приводится одна из реализаций того, как это можно сделать. Он начинается с прямоугольника, определяющего внешнюю границу, который он делит на четыре меньших неперекрывающихся прямоугольника. Это достигается путем выбора полуслучайной внутренней точки и использования ее вместе с четырьмя существующими угловыми точками внешнего прямоугольника для формирования четырех подсекций.

Большинство действий происходит в функции 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

второй пример выходного изображения

person martineau    schedule 07.12.2010
comment
+1, отличный подход! Визуализация PIL - тоже приятный штрих :) - person Kiv; 08.12.2010
comment
+1 Хорошо смотрится! Мне это нравится! Я обязательно буду использовать это в одном из своих проектов. Есть ли шанс, что вы могли бы придумать вариант, который дает идеальные квадраты случайных размеров в случайных местах? - person John; 09.12.2010
comment
@John: Ну да, я почти уверен, что смогу, хотя технически это новый вопрос. - person martineau; 09.12.2010
comment
@Martineau: Вот что мне было интересно, как вы думаете, мне следует задать новый вопрос, или он будет закрыт как обман? - person John; 09.12.2010
comment
@John: Хммм, никогда не думал о возможности обмануть. Вот что я вам скажу: если вы примете мой текущий ответ таким, какой он есть, я обновлю его, чтобы ответить на ваш следующий вопрос. - person martineau; 09.12.2010

Три идеи:

Уменьшайте размер ваших объектов

Первый метод не работает, потому что попадание в случайный массив из 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)

Это значительно улучшит вашу производительность, поскольку наличие одного пересечения не заставит вас создать совершенно новый набор, просто чтобы выбрать другой отдельный прямоугольник.

Предварительный расчет

Как часто вы будете использовать эти наборы в своей игре? Использование другого набора каждую секунду - это другой сценарий, чем использование набора один раз в час. Если вы не используете эти наборы слишком часто, предварительно рассчитайте достаточно большой набор, чтобы игрок, вероятно, никогда не увидел один и тот же набор дважды. При предварительном вычислении вы не слишком заботитесь о затраченном времени (так что вы даже можете использовать свой неэффективный первый алгоритм).

Даже если вам действительно нужны эти прямоугольники во время выполнения, может быть хорошей идеей немного вычислить их, прежде чем они вам понадобятся, когда процессор по какой-то причине простаивает, чтобы у вас всегда был готовый набор под рукой.

Во время выполнения просто выберите набор наугад. Вероятно, это лучший подход для игр в реальном времени.

Примечание. В этом решении предполагается, что прямоугольники сохраняются в целях экономии места, например пары (x, y) координат. Эти пары занимают очень мало места, и вы действительно можете сэкономить тысячи и даже миллионы в файле разумного размера.

Полезные ссылки:

person Adam Matan    schedule 07.12.2010
comment
Вы можете использовать инструкцию continue или, если вы достаточно любите приключения, осторожное применение встроенной функции any, чтобы упростить здесь логику. - person Karl Knechtel; 07.12.2010
comment
Я не ожидал, что вы будете использовать оба! ;) Мы можем просто сделать if not any(...): # do the addition. Кроме того, квадратные скобки здесь не нужны: прочтите о генераторах. any принимает итерации, а не только последовательности. Кроме того, теперь это не работает, потому что цикл for повторяет логику, реализованную с помощью any. :( - person Karl Knechtel; 07.12.2010
comment
Я никогда не должен писать код до 9:00 утра и двух кружек кофе. - person Adam Matan; 07.12.2010

Есть очень простое приближение к вашей проблеме, которое отлично сработало для меня:

  • Определите сетку. Например, 100-пиксельная сетка записывает (x, y) -> (int (x / 100), int (y / 100)). Элементы сетки не перекрываются.
  • Либо поместите каждый объект в другую сетку (случайным образом внутри сетки будет выглядеть красивее), либо случайным образом поместите несколько объектов в каждую сетку, если вы можете позволить нескольким объектам перекрываться.

Я использовал это для случайного создания 2D-карты (как в Zelda). Изображения моих объектов меньше ‹100 * 100>, поэтому я использовал сетку размером‹ 500 * 500> и допустил 1-6 объектов в каждой сетке.

person Nath    schedule 09.09.2011

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()

Вам также может потребоваться уменьшить размер прямоугольников, если это повторяется слишком много раз.

person John La Rooy    schedule 07.12.2010
comment
Я не знаю почему, но ваш метод, как и мой, необъяснимо сгенерировал перекрывающиеся прямоугольники ... Он выглядит так, как будто он должен работать ... Я не знаю, что пошло не так. - person John; 09.12.2010
comment
@John, возможно, это ошибка в вашей функции коллизий. Можете выложить для него код? - person John La Rooy; 09.12.2010
comment
Я использую объекты Pygames Rect, встроенные в функцию colliderect (other_rect). К сожалению, я не могу найти для него код, но я попробую использовать другую функцию и вернусь к вам. - person John; 10.12.2010
comment
Я нашел другую функцию столкновения и тоже попробовал. Это тоже не сработало. Код для него находится здесь: pastebin.com/A8QVC89g - person John; 17.12.2010

Ты пробовал:

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) Когда прямоугольник с чем-то сталкивается, случайным образом сгенерируйте подобласть этого прямоугольника и попробуйте это вместо этого.

person Karl Knechtel    schedule 07.12.2010
comment
Просто попробовал. Он повторил более 50 000 раз, не найдя 20 прямоугольников. Я все еще новичок, поэтому не знаю, как найти область пересечения, не говоря уже о ее центре! Я просто использую метод pygames colliderect (), чтобы сообщить , если они сталкиваются. К сожалению, я не могу сказать где. - person John; 09.12.2010
comment
Как вы выбираете размеры прямоугольника? - person Karl Knechtel; 10.12.2010
comment
@ Карл: Извините, я не видел вашего комментария. Без @John меня об этом не уведомили. Размеры прямоугольника случайны. Я использую random.randint (начало, конец), чтобы получить их. - person John; 17.12.2010
comment
@John да, если вы выберете такие размеры прямоугольников, то, конечно, будет очень сложно получить 20 неперекрывающихся прямоугольников ... подумайте о том, какой будет средняя площадь данного прямоугольника! - person Karl Knechtel; 17.12.2010
comment
@ Карл: Хм, ты прав. Можете ли вы предложить лучший способ выбора размеров прямоугольников? - person John; 17.12.2010
comment
@John Я мог бы сделать несколько предложений, не вдаваясь в подробности, но я действительно думаю, что вам будет лучше, если вы попытаетесь что-то придумать самостоятельно. :) Я думаю, что некоторые другие ответы / комментарии также ссылались на это. - person Karl Knechtel; 18.12.2010
comment
@ Карл: Хорошо, я посмотрю, что я могу сделать. Спасибо за помощь. - person John; 18.12.2010

альтернативный псевдокод уже упомянутым:

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

Или что-то вроде того.

Но это имеет то преимущество, что возможно создание некоторых меньших объектов, чем вы предполагали, и создание объектов, которые почти пересекаются (т. Е. Соприкасаются).

Если это карта, которую должен пройти игрок, он все равно не сможет пройти по ней, потому что их путь может быть заблокирован.

person MarkR    schedule 07.12.2010

В моем случае у меня была аналогичная проблема, за исключением того, что у меня было несколько прямоугольников предварительного выхода внутри всего прямоугольника. Таким образом, новые прямоугольники пришлось разместить вокруг существующих.

Я использовал жадный подход:

  • Сделайте прямоугольный общий (глобальный) прямоугольник: создайте сетку из отсортированных x и отсортированных координат y всех прямоугольников на данный момент. Это даст вам неправильную (но прямоугольную) сетку.
  • Для каждой ячейки сетки вычислите площадь, это даст вам матрицу площадей.
  • Используйте алгоритм Kadanes 2D, чтобы найти подматрицу, которая дает вам максимальную площадь (= наибольший свободный прямоугольник)
  • Поместите случайный прямоугольник в это свободное пространство
  • Повторение

Это требует преобразования из вашего исходного координатного пространства в / из пространства сетки, но это несложно.

(Обратите внимание, что запуск Kadene непосредственно на исходном глобальном прямоугольнике занимает много времени. Для моего приложения переход через сеточную аппроксимацию выполняется достаточно быстро)

person dgorissen    schedule 17.10.2017