简体   繁体   English

使用PyQt4的十六进制网格图

[英]Hex grid map with PyQt4

I am trying to create a map editor. 我正在尝试创建地图编辑器。 I intend the map to be an hexagonal grid where each hexagon is a tile of the map. 我希望地图是一个六边形网格,其中每个六边形都是地图的图块。 A tile will be a graphical representation of that area (sea, meadow, desert, mountain, etc). 图块将是该区域(海洋,草地,沙漠,山脉等)的图形表示。 The map is intended to be of any size. 该地图的大小不限。 Let's freeze the requirements here for now :) 让我们暂时冻结需求:)

I want to use PyQt4 (take it as a design requirement). 我想使用PyQt4(将其作为设计要求)。 As I am just starting with Qt/PyQt, I am facing the problem of vastness: so big this Qt thing that I cannot grasp it all. 当我刚开始使用Qt / PyQt时,我面临着一个巨大的问题:这个Qt太大了,我无法全部掌握。 And here I am, asking for your kind and most welcome experience. 我在这里,要求您的亲切和最欢迎的经历。

After a bit of googling, I've decided to use the QGraphicalView/Scene approach. 经过一番谷歌搜索后,我决定使用QGraphicalView / Scene方法。 In fact, I was thinking about creating my own hexgrid class inheriting from QGraphicalView and creating my RegularPolygon class inheriting from QGraphicalPolygonItem. 实际上,我正在考虑创建自己的继承自QGraphicalView的hexgrid类,以及创建继承自QGraphicalPolygonItem的RegularPolygon类。

Now they come the doubts and problems. 现在他们来了疑问和问题。

My main doubt is "Is my approach a correct one?" 我的主要疑问是“我的方法正确吗?” Think about the needs I have explained at the beginning of the post: hexagonal map, where each hexagon will be a tile of a given type (sea, desert, meadows, mountains, etc). 考虑一下我在文章开头所解释的需求:六角形地图,其中每个六角形将是给定类型的瓷砖(海洋,沙漠,草地,山脉等)。 I am concerned about performance once the editor works (scrolling will feel nice? and this kind of things). 编辑器工作后,我会担心性能(滚动会感觉不错吗?这类事情)。

And so far, the problem is about precision. 到目前为止,问题在于精度。 I am drawing the hexgrid by creating and drawing all its hexagons (this even sounds bad to me... thinking about performance). 我通过创建和绘制所有六边形来绘制六边形网格(这甚至听起来对我来说很糟糕……考虑性能)。 I used some formulas to calculate the vertices of each hexagon and creating the polygon from there. 我使用了一些公式来计算每个六边形的顶点并从中创建多边形。 I expect the sides of two consecutive hexagons to coincide exactly at the same location, but the rounding seems to be playing a bit with my desires, as sometimes the hexagon sides perfectly matches in the same location (good) and sometimes they do not match by what seems to be 1 pixel difference (bad). 我希望两个连续的六边形的边恰好在同一位置重合,但是四舍五入似乎有点符合我的愿望,因为有时六边形的边在同一位置完全匹配(良好),有时它们不匹配。似乎有1个像素的差异(不好)。 This gives a poor visual impression of the grid. 这给网格的视觉印象很差。 Maybe I have not explained myself quite well... it's better if I give you the code and you run it by yourselves 也许我对自己的解释不是很好...最好给我代码,然后由您自己运行

So summarizing: 总结一下:

  1. Do you think my approach will give future performance issues? 您认为我的方法会给将来的性能带来问题吗?
  2. Why are not the hexagons placed exactly so that they share sides? 为什么六边形不能正确放置,以使它们共享边? How to avoid this problem? 如何避免这个问题?

The code: 编码:

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

__meta__ =  \
{
    (0,0,1): (
              [ "Creation" ],
              [ ("Victor Garcia","vichor@xxxxxxx.xxx") ]
             )
} 

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

and last but not least, sorry for the long post :) 最后但并非最不重要的一点,很长的遗憾:)

Thanks Victor 谢谢维克多

Personally, I'd define each hexagonal tile as a separate SVG image, and use QImage and QSvgRenderer classes to render them to QPixmaps (with an alpha channel) whenever the zoom level changes. 就个人而言,我将每个六边形图块定义为单独的SVG图像,并在缩放级别更改时使用QImage和QSvgRenderer类将它们呈现为QPixmaps(具有alpha通道)。 I'd create a QGraphicsItem subclass for displaying each tile. 我将创建一个QGraphicsItem子类来显示每个图块。

The trick is to pick the zoom level so that the width of the (upright) hexagon is a multiple of two, and the height a multiple of four, with width/height approximately sqrt(3/4). 技巧是选择缩放级别,以使(垂直)六边形的宽度是2的倍数,而高度是4的倍数,其宽度/高度大约为sqrt(3/4)。 The hexagons are slightly squished in either direction, but for all hexagons at least eight pixels in diameter, the effect is inperceptible. 六边形在任一方向上都被轻微挤压,但是对于所有六边形,直径至少为八个像素,效果是不可察觉的。

If the width of the hexagon is 2*w , and height 4*h , here's how to map the (upright) hexagons to Cartesian coordinates: 如果六角形的宽度为2*w ,高度为4*h ,这是将(直立的)六角形映射到笛卡尔坐标的方法:

矩形网格中的六角形

If each side of the hexagon is a , then h=a/2 and w=a*sqrt(3)/2 , therefore w/h=sqrt(3) . 如果六角形的每一边都是a ,则h=a/2w=a*sqrt(3)/2 ,因此w/h=sqrt(3)

For optimum display quality, pick integer w and h , so that their ratio is approximately sqrt(3) ≃ 1.732 . 为了获得最佳显示质量,请选择整数wh ,以使它们的比率约为sqrt(3) ≃ 1.732 This means your hexagons will be very slightly squished, but that's okay; 这意味着您的六角形将被轻微挤压,但这没关系; it is not perceptible. 这是不可感知的。

Because the coordinates are now always integers, you can safely (without display artefacts) use pre-rendered hexagon tiles, as long as they have an alpha channel, and perhaps a border to allow smoother alpha transitions. 因为坐标现在始终是整数,所以只要它们具有alpha通道,甚至可以使用边框以允许更平滑的alpha过渡,就可以安全地(不带显示假象)使用预渲染的六角形拼贴。 Each rectangular tile is then 2*w+2*b pixels wide and 4*h+2*b pixels tall, where b is the number of extra border (overlapping) pixels. 每个矩形图块的宽度为2*w+2*b像素,高度为4*h+2*b像素,其中b是额外边框(重叠)像素的数量。

The extra border is needed to avoid perceptible seams (background color bleeding through) where pixels are only partially opaque in all overlapping tiles. 需要额外的边框以避免在所有重叠的图块中像素仅部分不透明的可见接缝(背景色渗入)。 The border allows you to better blend the tile into the neighboring tile; 边框使您可以将图块更好地融合到相邻图块中; something the SVG renderer will do automatically if you include a small border region in your SVG tiles. 如果在SVG磁贴中包含较小的边框区域,SVG渲染器将自动执行某些操作。

If you use typical screen coordinates where x grows right and y down, then the coordinates for hexagon X,Y relative to the 0,0 one are trivial: 如果你使用典型的屏幕坐标,其中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

Obviously, odd rows of hexagons are positioned half a hexagon to the right. 显然,六边形的奇数行位于右侧六边形的一半。

Subclassing QGraphicsItem and using a bounding polygon (for mouse and interaction tests) means Qt will do all the heavy work for you, when you wish to know which hexagonal tile the mouse is hovering on top of. 子类化QGraphicsItem并使用边界多边形(用于鼠标和交互测试)意味着,当您希望知道鼠标悬停在哪个六角形瓷砖上时,Qt将为您完成所有繁重的工作。

However, you can do the inverse mapping -- from screen coordinates back to hexagons -- yourself. 但是,您可以自己进行逆映射-从屏幕坐标到六边形。

First, you calculate which rectangular grid cell (green grid lines in the image above) the coordinate pair is in: 首先,您计算坐标对位于哪个矩形网格单元(上图中的绿色网格线):

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

Let's assume all coordinates are nonnegative. 假设所有坐标均为非负值。 Otherwise, % must be read as "nonnegative remainder, when divided by" . 否则, %必须读为“除以负数” (That is, 0 <= a % b < b for all a , even negative a ; b is always a positive integer here.) (也就是说,对于所有a0 <= a % b < b ,甚至是负ab在这里始终是正整数。)

If the origin is as shown in the above image, then two rows out of every three are trivial, except that every odd row of hexagons is shifted one grid cell right: 如果原点如上图所示,那么每三行中的两行都是微不足道的,除了六边形的每一奇数行都向右移动了一个网格单元:

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)

Every third row contains rectangular grid cells with a diagonal boundary, but worry not: if the boundary is \\ (wrt. above image), you only need to check if 每三行包含一个带有对角线边界的矩形网格单元,但是不必担心:如果边界是\\ (在图像上方为wrt),则只需检查是否

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

to find out if you are in the upper right triangular part. 找出您是否在右上三角部分。 If the boundary is / wrt. 如果边界是/ wrt。 above image, you only need to check if 上图,您只需要检查

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

to find out if you are in the lower right triangular part. 找出您是否位于右下三角部分。

In each four-column and six-row section of rectangular grid cells, there are eight cases that need to be handled, using one of the above test clauses. 在矩形网格单元格的每个四列和六行部分中,需要使用上述测试条款之一处理八种情况。 (I'm too lazy to work the exact if clauses for you here; like I said, I'd let Qt do that for me.) This rectangular region repeats exactly for the entire hexagonal map; (我懒得在这里为您处理确切的if子句;就像我说的那样,我让Qt为我做这个。)这个矩形区域在整个六边形图中完全重复。 thus, a full coordinate conversion may need up to 9 if clauses (depending on how you write it), so it's a bit annoying to write. 因此,完整的坐标转换最多可能需要9个if子句(取决于您如何编写),因此编写起来有点烦人。

If you wish to determine eg the mouse cursor location relative to the hexagon it is hovering over, first use the above to determine which hexagon the mouse hovers over, then substract the coordinates of that hexagon from the mouse coordinates to get the coordinates relative to the current hexagon. 如果要确定例如鼠标光标相对于其所悬停的六边形的位置,请首先使用上面的内容确定鼠标悬停在哪个六边形上,然后从鼠标坐标中减去该六边形的坐标以获得相对于六边形的坐标当前的六角形。

Try with this main() function. 尝试使用此main()函数。 I used the radius of the inscribed circle (ri) instead of the circumscribed circle that you used (radius). 我使用了内接圆的半径(ri),而不是您使用的外接圆的半径(半径)。 It looks a bit better now, but still not perfect. 现在看起来好一些,但仍不完美。 I think the way the oblique sides are drawn at the top and bottom of the hexagon are different. 我认为在六边形的顶部和底部绘制斜边的方式不同。

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

There are multiple problems here. 这里有多个问题。 They aren't specifically related to Qt or to Python, but to general computer science. 它们与Qt或Python没有特别关系,但与一般计算机科学有关。

You have floating point geometrical shapes that you want to display on a raster device, so somehow there has to be a floating point to integer conversion. 您有要显示在栅格设备上的浮点几何形状,因此某种程度上必须存在浮点到整数的转换。 It's not in your code, so it will happen at a lower level: in the graphics library, the display driver or whatever. 它不在您的代码中,因此会在较低级别上发生:在图形库,显示驱动程序或任何其他内容中。 Since you're not happy with the result, you have to handle this conversion yourself. 由于您对结果不满意,因此您必须自己处理此转换。

There's no right or wrong way to do this. 这样做没有对与错的方法。 For example, take your case of a hex tile that has a “radius” of 50. The hexagon is oriented so that the W vertex is at (-50,0) and the E vertex is at (50,0). 例如,以“半径”为50的六边形图块为例。六边形的方向使得W顶点在(-50,0),E顶点在(50,0)。 Now the NE vertex of this hexagon is at approximately (25,0,43.3). 现在,此六角形的NE顶点大约为(25,0,43.3)。 The hexagon that's adjacent to this one in the N direction has its center at about y=86.6 and its top edge at 129.9. 在N方向上与该六边形相邻的六边形的中心约为y = 86.6,其上边缘为129.9。 How would you like to pixellate this? 您想如何像素化? If you round 43.3 down to 43, now you no longer have a mathematically exact regular hexagon. 如果您将43.3向下舍入为43,现在您将不再具有数学上精确的正六边形。 If you round 129.9 up to 130, your first hexagon is 86 pixels in total height but the one on top of it is 87. This is an issue that you must resolve based on the project's requirements. 如果将129.9向上舍入为130,则第一个六边形的总高度为86像素,但顶部的总高度为87像素。这是一个必须根据项目要求解决的问题。

And this is just one case (radius=50). 这只是一种情况(半径= 50)。 If you allow the radius to be variable, can you come up with an algorithm to handle all cases? 如果允许半径是可变的,是否可以提出一种算法来处理所有情况? I couldn't. 我不能 I think you need to use a fixed screen dimension for your hexagons, or at least reduce the possibilities to a small number. 我认为您需要为六角形使用固定的屏幕尺寸,或者至少将可能性减小到很小的数目。

Nowhere in your code do you determine the size of the display window, so I don't understand how you intend to handle scaling issues, or determine how many hexes are needed to show the full map. 您在代码中的任何地方都无法确定显示窗口的大小,因此我不明白您打算如何处理缩放问题,也无法确定显示完整地图所需的十六进制数。

As to your first question, I am certain that the performance will be poor. 关于您的第一个问题,我相信表现会很差。 The constructor for QRegularPolygon is inside the loop that creates the hexes, so it gets called many times (800 in your example). QRegularPolygon的构造函数位于创建十六进制的循环内,因此它被调用了多次(在您的示例中为800)。 It does two trig calculations for each vertex, so you perform 9600 trig calculations as you build your list of hexes. 它为每个顶点执行两次Trig计算,因此在构建十六进制列表时将执行9600 Trig计算。 You don't need ANY of them. 您不需要任何。 The calculations are the sine and cosine of 0 degrees, 60 degrees, 120 degrees and so on. 计算是0度,60度,120度等的正弦和余弦。 Those are so easy you don't even need sin and cos. 这些是如此简单,您甚至不需要罪恶和因果关系。

The use of the trig functions exacerbates the floating point/integer problem, too. 触发函数的使用也加剧了浮点数/整数问题。 Look at this: 看这个:

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

We know it should be 25, but the computer figures it as int(24.999999999996) – I may have left out a few 9's. 我们知道应该是25,但是计算机将其表示为int(24.999999999996)–我可能遗漏了9个数字。

If you calculate the vertex positions of just one hexagon, you can get all the other ones by a simple translation. 如果仅计算一个六角形的顶点位置,则可以通过简单的平移获得所有其他六角形。 See the useful Qt functions QPolygon->translate or QPolygon->translated. 请参阅有用的Qt函数QPolygon-> translate或QPolygon-> translated。

It seems that you don't need a constructor that can handle any type of polygon when your design concept absolutely needs hexagons. 当您的设计概念绝对需要六边形时,似乎不需要构造器可以处理任何类型的多边形。 Did you just copy it from somewhere? 您只是从某处复制它吗? I think it's mostly clutter, which always opens the door to errors. 我认为这通常很混乱,总是会打开错误的大门。

Do you really need polygons here? 您真的需要多边形吗? Later on, I suppose, the game will use raster images, so the polygons are just for display purposes. 我想以后,游戏将使用光栅图像,因此多边形仅用于显示目的。 You could just take a point cloud representing all corners of the polygon and draw lines beneath them. 您可以只取一个代表多边形所有角的点云,并在其下绘制线。 With this, you avoid problems of rounding / floating point arithmetics etc. 这样,您就可以避免舍入/浮点运算等问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM