2013-08-27 2 views
0

Я пытаюсь создать редактор карт. Я предполагаю, что карта представляет собой гексагональную сетку, где каждый шестиугольник является плитой карты. Плитка будет графическим изображением этой области (море, луг, пустыня, гора и т. Д.). Карта предназначена для любого размера. Давайте сейчас заморозим требования :)Hex grid map с PyQt4

Я хочу использовать PyQt4 (возьмите его как требование к дизайну). Поскольку я только начинаю с Qt/PyQt, я столкнулся с проблемой огромности: такая большая вещь Qt, которую я не могу понять. И вот я, прошу вас о вашем доброжелательном и приятном опыте.

После небольшого поиска в Google, я решил использовать подход QGraphicalView/Scene. Фактически, я думал о создании собственного класса hexgrid, наследующего от QGraphicalView, и создания моего класса RegularPolygon, наследующего от QGraphicalPolygonItem.

Теперь у них возникают сомнения и проблемы.

Мое главное сомнение заключается в том, что мой подход правильный? Подумайте о потребностях, которые я объяснил в начале сообщения: гексагональная карта, где каждый шестиугольник будет плитой определенного типа (море, пустыня, луга, горы и т. Д.). Меня беспокоит производительность после того, как редактор работает (прокрутка будет приятной и что-то подобное).

И до сих пор проблема в точности. Я рисую гексдрид, создавая и рисуя все его шестиугольники (это даже звучит плохо для меня ... думая о производительности). Я использовал некоторые формулы для вычисления вершин каждого шестиугольника и создания там многоугольника. Я ожидаю, что стороны двух последовательных шестиугольников совпадут точно в одном и том же месте, но округление, похоже, немного походит на мои желания, так как иногда стороны шестиугольника идеально совпадают в одном и том же месте (хорошо), и иногда они не совпадают что кажется 1 пиксельной разницей (плохой). Это дает плохое визуальное представление о сетке. Может быть, я не объяснил себя очень хорошо ... это лучше, если я дам вам код и запустить его сами

Так суммирующих:

  1. Как вы думаете, что мой подход даст в будущем проблемы с производительностью?
  2. Почему гексагоны не расположены точно так, что они разделяют стороны? Как избежать этой проблемы?

Код:

#!/usr/bin/python 
""" 
Editor of the map. 
""" 

__meta__ = \ 
{ 
    (0,0,1): (
       [ "Creation" ], 
       [ ("Victor Garcia","[email protected]") ] 
      ) 
} 

import sys, math 
from PyQt4 import QtCore, QtGui 

# ============================================================================== 
class HexGrid(QtGui.QGraphicsView): 
    """ 
    Graphics view for an hex grid. 
    """ 

    # -------------------------------------------------------------------------- 
    def __init__(self, rect=None, parent=None): 
     """ 
     Initializes an hex grid. This object will be a GraphicsView and it will 
     also handle its corresponding GraphicsScene. 
      rect -- rectangle for the graphics scene. 
      parent -- parent widget 
     """ 
     super(HexGrid,self).__init__(parent) 

     self.scene = QtGui.QGraphicsScene(self) 
     if rect != None: 
      if isinstance(rect, QtCore.QRectF): self.scene.setSceneRect(rect) 
      else: raise StandardError ('Parameter rect should be QtCore.QRectF') 
     self.setScene(self.scene) 

# ============================================================================== 
class QRegularPolygon(QtGui.QGraphicsPolygonItem): 
    """ 
    Regular polygon of N sides 
    """ 

    def __init__(self, sides, radius, center, angle = None, parent=None): 
     """ 
     Initializes an hexagon of the given radius. 
      sides -- sides of the regular polygon 
      radius -- radius of the external circle 
      center -- QPointF containing the center 
      angle -- offset angle in radians for the vertices 
     """ 
     super(QRegularPolygon,self).__init__(parent) 

     if sides < 3: 
      raise StandardError ('A regular polygon at least has 3 sides.') 
     self._sides = sides 
     self._radius = radius 
     if angle != None: self._angle = angle 
     else: self._angle = 0.0 
     self._center = center 

     points = list() 
     for s in range(self._sides): 
      angle = self._angle + (2*math.pi * s/self._sides) 
      x = center.x() + (radius * math.cos(angle)) 
      y = center.y() + (radius * math.sin(angle)) 
      points.append(QtCore.QPointF(x,y)) 

     self.setPolygon(QtGui.QPolygonF(points)) 


# ============================================================================== 
def main(): 
    """ 
    That's it: the main function 
    """ 
    app = QtGui.QApplication(sys.argv) 

    grid = HexGrid(QtCore.QRectF(0.0, 0.0, 500.0, 500.0)) 

    radius = 50 
    sides = 6 

    apothem = radius * math.cos(math.pi/sides) 
    side = 2 * apothem * math.tan(math.pi/sides) 

    xinit = 50 
    yinit = 50 
    angle = math.pi/2 
    polygons = list() 

    for x in range(xinit,xinit+20): 
     timesx = x - xinit 
     xcenter = x + (2*apothem)*timesx 
     for y in range(yinit, yinit+20): 
      timesy = y - yinit 
      ycenter = y + ((2*radius)+side)*timesy 

      center1 = QtCore.QPointF(xcenter,ycenter) 
      center2 = QtCore.QPointF(xcenter+apothem,ycenter+radius+(side/2)) 

      h1 = QRegularPolygon(sides, radius, center1, angle) 
      h2 = QRegularPolygon(sides, radius, center2, angle) 

      # adding polygons to a list to avoid losing them when outside the 
      # scope (loop?). Anyway, just in case 
      polygons.append(h1) 
      polygons.append(h2) 

      grid.scene.addItem(h1) 
      grid.scene.addItem(h2) 

    grid.show() 
    app.exec_() 

# ============================================================================== 
if __name__ == '__main__': 
    main() 

и последнее, но не в последнюю очередь, извините за длинный пост :)

Благодаря Виктор

+0

Я нашел http://www.pygame.org/ с примерами гексрид, но они не используют PyQt. Я посмотрю на это, определенно, но мой вопрос по-прежнему действителен, так как я хочу знать, как правильно сделать hexmap, используя qt. – victor

+0

Я попробую другой подход. Рисование всех шестиугольников означает, что несколько вершин будут нарисованы два раза (те, которые разделены двумя шестиугольниками). Это звучит глупо: зачем делать ту же работу дважды? Я попробую рисовать первый шестиугольник (вверху слева? Может быть, начинается тот, что находится в центре сцены?). Чтобы управлять вторым, мне просто нужно нарисовать вершины, ожидающие рисования, и так далее. Это гарантирует, что общие вершины находятся в одном и том же пикселе, решая мою проблему точности. К счастью, это также позволит избежать проблем с будущей производительностью ... мне потребуется некоторое время, чтобы сделать это. – victor

+0

Просто подсказка для идентификации области щелчка, используйте 2 прямоугольника для грубого анализа (внешние границы, внутренние границы) и при необходимости (щелкните в области перекрытия внешних границ) подсчитайте, какой гексагон активирован (если он не решен с помощью сцен-мышей). –

ответ

4

Лично я определял каждую гексагональную плитку как отдельное изображение SVG и использовал классы QImage и QSvgRenderer для рендеринга их в QPixmaps (с альфа-каналом) при изменении уровня масштабирования. Я бы создал подкласс QGraphicsItem для отображения каждой плитки.

Хитрость заключается в том, чтобы выбрать уровень масштабирования так, чтобы ширина (вертикального) шестиугольника была кратной двум, а высота была кратной четыре, с шириной и высотой примерно в квадрате (3/4). Шестиугольники слегка скручены в любом направлении, но для всех шестиугольников диаметром не менее восьми пикселей эффект невозможен.

Если ширина шестиугольника 2*w, а высота 4*h, вот как отобразить (вертикально) шестиугольники в декартовых координатах:

Hexagon in a rectangular grid

Если каждая сторона шестиугольника a, то h=a/2 и w=a*sqrt(3)/2, поэтому w/h=sqrt(3).

Для обеспечения оптимального качества изображения выберите целое число w и h, чтобы их соотношение составляло приблизительно sqrt(3) ≃ 1.732. Это означает, что ваши шестиугольники будут очень слегка сплющиваться, но это нормально; это не заметно.

Поскольку координаты теперь всегда целые, вы можете безопасно (без артефактов отображения) использовать предварительно обработанные шестиугольные плитки, если у них есть альфа-канал и, возможно, граница, чтобы обеспечить более плавные альфа-переходы. Каждая прямоугольная плитка тогда 2*w+2*b пикселей в ширину и 4*h+2*b пикселей высотой, где b - это количество дополнительных пограничных (перекрывающихся) пикселей.

Дополнительная граница необходима, чтобы избежать видимых швов (цвет фона пропускает через), где пиксели только частично непрозрачны во всех перекрывающихся плитках. Граница позволяет лучше смешивать плитки с соседней плиткой; то SVG-рендеринг будет делать автоматически, если вы включите небольшой пограничный регион в своих SVG-плитках.

Если вы используете обычный экран координаты, где x растет прямо и y вниз, то координаты шестиугольника X,Y по отношению к 0,0 один тривиальны:

y = 3*h*Y 
if Y is even, then: 
    x = 2*w*X 
else: 
    x = 2*w*X + w 

Очевидно, что нечетные ряды шестиугольников расположены половину шестиугольника направо.

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

Однако может сделать обратное отображение - от экранных координат до шестиугольников - самостоятельно.

Во-первых, рассчитать, какой прямоугольной сетки ячейки (зеленые линии сетки на изображении выше) пара координат находится в:

u = int(x/w) 
v = int(y/h) 

Давайте предположим, что все координаты неотрицательны. В противном случае % следует читать как "неотрицательный остаток, если его делить на". (То есть, 0 <= a % b < b для всех a, даже отрицательный a;. b всегда положительное целое число здесь)

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

if v % 3 >= 1: 
    if v % 6 >= 4: 
     X = int((u - 1)/2) 
     Y = int(v/3) 
    else: 
     X = int(u/2) 
     Y = int(v/3) 

каждого третий ряд содержит прямоугольные ячейки сетки с диагональной границей, но не волнуйтесь: если граница \ (WRT.выше изображения), вам нужно только проверить, если

(x % w) * h >= (y % h) * w 

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

(x % w) * h + (y % h) * w >= (w * h - (w + h)/2) 

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

В каждой секции с четырьмя колонками и шестью рядами прямоугольных ячеек сетки есть восемь случаев, которые необходимо обрабатывать, используя один из вышеперечисленных тестовых предложений. (Я слишком ленив, чтобы работать с точными, если бы предложения были здесь, как я уже сказал, я бы позволил Qt сделать это для меня.) Эта прямоугольная область точно повторяется для всей гексагональной карты; таким образом, для полного преобразования координат может потребоваться до 9, если предложения (в зависимости от того, как вы его пишете), так что это немного раздражает писать.

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

+0

Замечательный. Я проверю это на следующей неделе. Я все равно закрою вопрос и дарую награду. Благодаря! – victor

+0

@victor: Я рад помочь. Я заметил, что меняли диагональные граничные тесты '/' и '\'; теперь исправлено. Если вы или другие лица найдете какие-либо другие опечатки или ошибки, пожалуйста, напишите. Логика звуковая, только мое объяснение выше может содержать опечатки или ошибки. –

1

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

def main(): 
    """ 
    That's it: the main function 
    """ 
    app = QtGui.QApplication(sys.argv) 

    grid = HexGrid(QtCore.QRectF(0.0, 0.0, 500.0, 500.0)) 

    radius = 50 # circumscribed circle radius 
    ri = int(radius/2 * math.sqrt(3)) # inscribed circle radius 
    sides = 6 

    apothem = int(ri * math.cos(math.pi/sides)) 
    side = int(2 * apothem * math.tan(math.pi/sides)) 

    xinit = 50 
    yinit = 50 
    angle = math.pi/2 
    polygons = list() 

    for x in range(xinit,xinit+20): 
     timesx = x - xinit 
     xcenter = x + (2*apothem-1)*timesx 
     for y in range(yinit, yinit+20): 
      timesy = y - yinit 
      ycenter = y + ((2*ri)+side)*timesy 

      center1 = QtCore.QPointF(xcenter,ycenter) 
      center2 = QtCore.QPointF(xcenter+apothem,ycenter+ri+(side/2)) 

      h1 = QRegularPolygon(sides, ri, center1, angle) 
      h2 = QRegularPolygon(sides, ri, center2, angle) 

      # adding polygons to a list to avoid losing them when outside the 
      # scope (loop?). Anyway, just in case 
      polygons.append(h1) 
      polygons.append(h2) 

      grid.scene.addItem(h1) 
      grid.scene.addItem(h2) 

    grid.show() 
    app.exec_() 
1

Здесь есть несколько проблем. Они не связаны конкретно с Qt или Python, а с общей компьютерной наукой.

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

Нет правильного или неправильного способа сделать это. Например, возьмите ваш случай из шестигранной плитки с «радиусом» 50. Шестиугольник ориентирован так, что вершина W находится в точке (-50,0), а вершина E находится в точке (50,0). Теперь вершина NE этого шестиугольника находится приблизительно (25,0,43,3). Шестиугольник, который находится рядом с этим в направлении N, имеет свой центр около y = 86,6, а его верхний край - 129,9. Как вы хотели бы пикселировать это? Если вы обходите 43,3 до 43, теперь у вас больше нет математически точного правильного шестиугольника. Если вы округлите 129,9 до 130, ваш первый шестиугольник имеет общую высоту 86 пикселей, а верхняя часть - 87. Это проблема, которую вы должны решить на основе требований проекта.

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

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

Что касается вашего первого вопроса, я уверен, что производительность будет плохой. Конструктор для QRegularPolygon находится внутри цикла, который создает гексы, поэтому он вызывается много раз (800 в вашем примере). Он выполняет два триггерных вычисления для каждой вершины, поэтому вы выполняете 9600 триггерных вычислений, когда вы создаете свой список гексов. Вы не нуждаетесь ни в каких из них. Расчеты представляют собой синус и косинус 0 градусов, 60 градусов, 120 градусов и так далее. Это так легко, что вам даже не нужен грех и сон.

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

>> int(50.0*math.sin(math.pi/6)) 
24 

Мы знаем, что должно быть 25, но компьютер фигурирует как Int (24.999999999996) - Я, возможно, опустили несколько 9-х.

Если вы вычислили положения вершин всего одного шестиугольника, вы можете получить все остальные простым переводом. См. Полезные функции Qt QPolygon-> translate или QPolygon-> translated.

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

0

Вам действительно нужны полигоны здесь? Позже, я полагаю, игра будет использовать растровые изображения, поэтому полигоны предназначены только для показа. Вы можете просто взять облако точек, представляющее все углы многоугольника и нарисовать линии под ними. С этим вы избегаете проблем округления/арифметики с плавающей запятой и т. Д.

+0

Это похоже на подход, который я изложил в своем собственном комментарии к сообщению, начиная с «Я попробую другой подход». Просто я не знаю, как получить облако точек. Поскольку я работаю над python, я ожидал создать словарь, используя мировые координаты в качестве ключа, и список свойств ландшафта, предметов и других вещей в качестве значения для данного ключа. Итак, под модельной точки зрения все ясно. Мои проблемы начинаются, когда я пытаюсь привлечь эту модель, я не могу понять, как выполнить это преобразование. Комментарий от @Paul Cornelius довольно близок к реальной ситуации, с которой я столкнулся. – victor