我正在尝试创建一个地图编辑器。我打算将地图设为六边形网格,其中每个六边形都是地图的图块。瓷砖将是该区域(海洋,草地,沙漠,山脉等)的图形表示。地图应具有任何尺寸。我们暂时冻结这里的要求:)
我想使用PyQt4(将其作为设计要求)。由于我刚刚开始使用Qt / PyQt,我面临着巨大的问题:这个Qt这么大,我无法理解它。在这里,我要求你的亲切和最受欢迎的经历。
经过一番谷歌搜索后,我决定使用QGraphicalView / Scene方法。实际上,我正在考虑创建自己的hexgrid类,继承自QGraphicalView并创建继承自QGraphicalPolygonItem的RegularPolygon类。
现在他们遇到了疑惑和问题。
我的主要怀疑是“我的方法是正确的吗?”想想我在帖子开头解释的需求:六边形地图,每个六边形将是特定类型的瓷砖(海洋,沙漠,草地,山脉等)。一旦编辑器工作,我就会关注性能(滚动会感觉很好吗?还有这种事情)。
到目前为止,问题在于精确度。我通过创建和绘制所有六边形来绘制hexgrid(这对我来说甚至听起来很糟糕......考虑性能)。我使用了一些公式来计算每个六边形的顶点并从那里创建多边形。我希望两个连续六边形的边完全在同一个位置重合,但是四舍五入似乎与我的欲望有点相似,因为有时六边形边在同一个位置完美匹配(好)有时它们不匹配什么似乎是1像素差异(坏)。这给网格带来了糟糕的视觉印象。也许我没有很好地解释自己...如果我给你代码你自己运行它会更好
总结:
代码:
#!/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()
最后但并非最不重要的是,对于长篇文章感到抱歉:)
由于 维克多
答案 0 :(得分:3)
就个人而言,我将每个六边形图块定义为单独的SVG图像,并且每当缩放级别改变时,使用QImage和QSvgRenderer类将它们渲染到QPixmaps(带有alpha通道)。我将创建一个QGraphicsItem子类来显示每个tile。
诀窍是选择缩放级别,使(直立)六边形的宽度为2的倍数,高度为4的倍数,宽度/高度约为sqrt(3/4)。六边形在任一方向上略微压扁,但对于直径至少为8像素的所有六边形,效果都不易察觉。
如果六边形的宽度为2*w
,高度为4*h
,则以下是将(直立)六边形映射到笛卡尔坐标的方法:
如果六边形的每一边都是a
,那么h=a/2
和w=a*sqrt(3)/2
,w/h=sqrt(3)
。
为获得最佳显示质量,请选择整数w
和h
,使其比率约为sqrt(3) ≃ 1.732
。这意味着你的六边形会被轻微压扁,但这没关系;它是不可察觉的。
因为坐标现在总是整数,所以你可以安全地(没有显示人工制品)使用预渲染的六边形瓷砖,只要它们有一个alpha通道,并且可能是边框以允许更平滑的alpha过渡。每个矩形图块的宽度为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
了解你是否在右上角的三角形部分。如果边界是/
wrt。在上图中,您只需要检查是否
(x % w) * h + (y % h) * w >= (w * h - (w + h) / 2)
了解你是否在右下角的三角形部分。
在矩形网格单元的每个四列和六行部分中,有八种情况需要使用上述测试条款之一进行处理。 (我懒得在这里为你准确的if条款;就像我说的那样,我让Qt为我这样做。)这个矩形区域正好重复整个六边形地图;因此,完整的坐标转换可能需要最多9个if子句(取决于你如何写它),所以写起来有点烦人。
如果您想确定例如相对于它悬停的六边形的鼠标光标位置,首先使用上面的方法确定鼠标悬停在哪个六边形上,然后从鼠标坐标中减去该六边形的坐标,得到相对于当前六边形的坐标。
答案 1 :(得分: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_()
答案 2 :(得分: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度等的正弦和余弦。那些很容易你甚至不需要罪和cos。
使用trig函数也会加剧浮点/整数问题。看看这个:
>> int(50.0*math.sin(math.pi/6))
24
我们知道它应该是25,但计算机将其视为int(24.999999999996) - 我可能已经遗漏了几个9。
如果计算一个六边形的顶点位置,您可以通过简单的平移获得所有其他六边形。请参阅有用的Qt函数QPolygon-&gt; translate或QPolygon-&gt;翻译。
当您的设计概念绝对需要六边形时,您似乎不需要可以处理任何类型多边形的构造函数。你刚从某处复制过吗?我认为这主要是混乱,总是打开错误的大门。
答案 3 :(得分:0)
你真的需要多边形吗?后来,我想,游戏将使用光栅图像,因此多边形仅用于显示目的。 您可以采用表示多边形所有角落的点云,并在其下方绘制线条。有了这个,你可以避免圆角/浮点算术等问题。