1

我正在尝试创建一个地图编辑器。我希望地图是一个六边形网格,其中每个六边形都是地图的一个图块。瓦片将是该区域(海洋、草地、沙漠、山脉等)的图形表示。该地图旨在为任何大小。让我们暂时冻结这里的要求:)

我想使用 PyQt4(把它作为设计要求)。当我刚开始使用 Qt/PyQt 时,我面临着巨大的问题:这个 Qt 东西太大了,我无法掌握它。我在这里,请求您的亲切和最受欢迎的体验。

经过一番谷歌搜索,我决定使用 QGraphicalView/Scene 方法。事实上,我正在考虑创建我自己的继承自 QGraphicalView 的 hexgrid 类并创建我的继承自 QGraphicalPolygonItem 的 RegularPolygon 类。

现在他们来了疑惑和问题。

我的主要疑问是“我的方法正确吗?” 想想我在文章开头解释的需求:六边形地图,其中每个六边形将是给定类型(海洋、沙漠、草地、山脉等)的瓷砖。一旦编辑器工作,我会担心性能(滚动会感觉很好吗?和这种事情)。

到目前为止,问题在于精度。我通过创建和绘制所有六边形来绘制六边形网格(这对我来说甚至听起来很糟糕......考虑性能)。我使用了一些公式来计算每个六边形的顶点并从那里创建多边形。我希望两个连续六边形的边在同一位置完全重合,但舍入似乎有点符合我的愿望,因为有时六边形边在同一位置完美匹配(好),有时它们不匹配似乎有 1 个像素的差异(不好)。这给网格的视觉印象很差。也许我没有很好地解释自己......如果我给你代码然后你自己运行它会更好

所以总结一下:

  1. 你认为我的方法会带来未来的性能问题吗?
  2. 为什么不将六边形精确放置以便它们共享边?如何避免这个问题?

编码:

#!/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()

最后但并非最不重要的是,很抱歉这篇长文:)

谢谢维克多

4

4 回答 4

4

就个人而言,我会将每个六边形图块定义为单独的 SVG 图像,并在缩放级别发生变化时使用 QImage 和 QSvgRenderer 类将它们渲染到 QPixmaps(带有 alpha 通道)。我会创建一个 QGraphicsItem 子类来显示每个图块。

诀窍是选择缩放级别,使(直立)六边形的宽度是 2 的倍数,高度是 4 的倍数,宽度/高度约为 sqrt(3/4)。六边形在任一方向都略微挤压,但对于所有直径至少为 8 个像素的六边形,效果是难以察觉的。

如果六边形的宽度是2*w,高度是 ,下面4*h是如何将(直立的)六边形映射到笛卡尔坐标:

矩形网格中的六边形

如果六边形的每一边都是a, 那么h=a/2,w=a*sqrt(3)/2因此w/h=sqrt(3).

为获得最佳显示质量,请选择整数wh,以便它们的比率约为sqrt(3) ≃ 1.732。这意味着你的六边形会被轻微挤压,但这没关系;它是不可察觉的。

因为坐标现在总是整数,所以您可以安全地(没有显示伪影)使用预渲染的六边形图块,只要它们具有 alpha 通道,并且可能还有一个边框以允许更平滑的 alpha 过渡。然后,每个矩形图块的2*w+2*b像素宽和4*h+2*b像素高,其中b是额外边框(重叠)像素的数量。

需要额外的边框以避免可察觉的接缝(背景颜色渗出),其中像素在所有重叠图块中仅部分不透明。边框可以让您更好地将瓷砖融合到相邻的瓷砖中;如果您在 SVG 拼贴中包含一个小边框区域,SVG 渲染器将自动执行某些操作。

如果您使用向右和向下x增长的典型屏幕坐标,则六边形相对于六边形的坐标是微不足道的:yX,Y0,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)

每第三行包含带有对角线边界的矩形网格单元,但不要担心:如果边界是\(如上图所示),您只需要检查是否

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

看看你是否在右上角的三角形部分。如果边界是/wrt。上图,你只需要检查是否

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

看看你是否在右下三角部分。

在矩形网格单元的每个四列六行截面中,有八种情况需要处理,使用上述测试条款之一。(我懒得在这里为你使用确切的 if 子句;就像我说的,我会让 Qt 为我做这件事。)这个矩形区域在整个六边形地图中完全重复;因此,一个完整的坐标转换可能需要多达 9 个 if 子句(取决于您如何编写它),因此编写起来有点烦人。

如果您想确定鼠标光标相对于它悬停的六边形的位置,首先使用上面的方法确定鼠标悬停在哪个六边形上,然后从鼠标坐标中减去该六边形的坐标以获得相对于当前的六边形。

于 2013-09-21T20:13:34.587 回答
1

试试这个 main() 函数。我使用了内接圆的半径 (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_()
于 2013-09-18T12:04:08.873 回答
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度等的正弦和余弦。这些很容易,你甚至不需要 sin 和 cos。

三角函数的使用也加剧了浮点/整数问题。看这个:

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

我们知道它应该是 25,但计算机将它计算为 int(24.999999999996)——我可能遗漏了几个 9。

如果你只计算一个六边形的顶点位置,你可以通过简单的平移得到所有其他的。请参阅有用的 Qt 函数 QPolygon->translate 或 QPolygon->translated。

当您的设计概念绝对需要六边形时,您似乎不需要可以处理任何类型多边形的构造函数。你是从某个地方复制过来的吗?我认为它主要是杂乱无章的,它总是为错误打开大门。

于 2013-09-19T00:30:52.103 回答
0

你真的需要多边形吗?后来,我想,游戏将使用光栅图像,所以多边形仅用于显示目的。您可以只取一个代表多边形所有角的点云并在它们下面画线。这样,您就可以避免舍入/浮点运算等问题。

于 2013-09-19T20:26:53.000 回答