如何解决Godot 引擎用 Tilemap 瓷砖填充 2D 多边形
我在 Godot 引擎中遇到了一个我无法解决的问题: 怎么可能(在代码中)用瓷砖填充 polygon2D 区域? 我试图获得积分位置。使用 2D for 循环遍历线的顶点,我就是无法理解这一点。
这是我期望的结果的模型
解决方法
我有解决方案,但有一点“hacky”部分,但我们会解决这个问题。
迭代多边形的段
首先,这个想法是迭代构成多边形的线段。为此,让我们在从 index
到多边形中的点数的范围内使用 0
。
我们得到 index
的点和 index - 1
处的点是我们段的结束。 请注意,索引 -1
将为我们提供最后一点。
for index in range(0,polygon.polygon.size()):
var segment_start = polygon.polygon[index - 1]
var segment_end = polygon.polygon[index]
我们需要处理瓦片地图坐标。因此,让我们转换它们,为此我将有一个自定义函数 polygon_to_map
,我们稍后会返回。
for index in range(0,polygon.polygon.size()):
var segment_start = polygon_to_map(polygon.polygon[index - 1])
var segment_end = polygon_to_map(polygon.polygon[index])
然后我们需要获取组成该段的单元格。同样,另一个自定义函数 segment
将处理该问题。我们可以遍历单元格,并将它们设置在瓦片地图上:
for index in range(0,polygon.polygon.size()):
var segment_start = polygon_to_map(polygon.polygon[index - 1])
var segment_end = polygon_to_map(polygon.polygon[index])
var cells = segment(segment_start,segment_end)
for cell in cells:
tile_map.set_cell(celle.x,cell.y,0)
我正在设置磁贴 0
,您可以设置对您有意义的内容。
坐标转换
瓦片地图有一个方便的 world_to_map
函数,我们可以使用它进行转换,这意味着我们可以像这样简单地实现我们的自定义函数 polygon_to_map
:
func polygon_to_map(vector:Vector2) -> Vector2:
return tile_map.world_to_map(vector)
然而,这将始终返回整数坐标。因此,我们丢失了 segment
函数的信息。相反,我将使用 cell_size
:
func polygon_to_map(vector:Vector2) -> Vector2:
return vector/tile_map.cell_size
遍历 Segment 的单元格
我们必须定义我们的 segment
函数......
func segment(start:Vector2,end:Vector2) -> Array:
# ???
为此,我将使用“快速体素遍历算法”。 其他画线算法也可以,我喜欢这个。
我经常发现“快速体素遍历算法”的解释有点难以解析,不如让我们来推导一下。
我们希望以参数方式处理细分市场。让我们以遍历长度作为参数开始。如果遍历的长度是 0
,我们在 start
。如果遍历的长度是段的长度,我们在 end
。使用沿线段的遍历长度,我们可以沿线段选取任何单元格。
事实上,我们定义了组成线段的点的形式:
point = start + direction_of_the_segment * traversed_length
看,我们将在这里讨论的所有内容都是关于细分市场的,让我们将该变量简称为 direction
,好吗?
point = start + direction * traversed_length
好的,我说过我们将使用 traversed_length
选择单元格,因此我们将有一个循环,如下所示:
func segment(start:Vector2,end:Vector2) -> Array:
var length = start.distance_to(end) # (end - start).length()
var direction = start.direction_to(end) # (end - start).normalized()
var traversed_length = 0
while traversed_length < length:
var cell = start + direction * traversed_length
# something that increments traversed_length and other stuff
现在,诀窍是知道在到达 x 轴上的下一个整数值之前需要遍历多少,以及在 y 轴上到达下一个整数值之前需要遍历多少。
现在,假设我们有一个函数 next2
,它根据方向给出沿轴的下一个值:
var next = next2(start,direction)
我们稍后会讨论 next2
的细节。
现在,我们需要计算到达下一个值之前需要遍历的长度。让我们之前使用我们的方程形式:
point = start + direction * traversed_length
但是现在,点将是开始之后的下一个,长度就是我们要找的
next = start + direction * length_to_next
=>
next - start = direction * length_to_next
=>
(next - start) / direction = length_to_next
因此:
var length_to_next = (next - start)/direction
让我们更新我们的代码(请记住,当我们递增 length_to_next
时,我们必须更新 traversed_length
):
func segment(start:Vector2,end:Vector2) -> Array:
var length = start.distance_to(end) # (end - start).length()
var direction = start.direction_to(end) # (end - start).normalized()
var next = next2(start,direction)
var length_to_next = div2(next - start,direction)
var traversed_length = 0
while traversed_length < length:
var cell = start + direction * traversed_length
if length_to_next.x < length_to_next.y:
traversed_length += length_to_next.x
length_to_next.x = # ??
length_to_next.y -= length_to_next.x
else:
traversed_length += length_to_next.y
length_to_next.x -= length_to_next.y
length_to_next.y = # ??
那个div2
是什么?啊,对,我们有它,因为零除以零。当线段完全垂直或水平时会发生这种情况。在这种情况下,我们想要的值是 INF
,因此它永远不会更小,并且代码不会进入该分支。为此,您可以定义:
func div2(numerator:Vector2,denominator:Vector2) -> Vector2:
return Vector2(div(numerator.x,denominator.x),div(numerator.y,denominator.y))
func div(numerator:float,denominator:float) -> float:
return numerator / denominator if denominator != 0 else INF
好的,我们还有一个问题。例如,如果我们遍历到 x 上的下一个位置,那么到 x 的下一个位置的长度是多少(对 y 也是如此)?
回到我们的等式,这次我们假设 start
为 0:
point = start + direction * length_between
=>
point = direction * length_between
=>
point/direction = length_between
点应该是什么?好吧,如果我们从 0 开始,然后朝着下一个整数的方向前进……根据段的方向。那是方向的 -1
。
因此,我们有(我们这次不需要 0
):
1
更新我们的代码:
sign
啊,对了,我们该回去了。让我们构建一个结果数组,填充它并返回它:
div2
这行得通吗?是的,这有效。这是“快速体素遍历算法”吗?不完全是。让我们称其为“慢体素遍历算法”以备将来参考(也是因为从这里开始它主要优化)。
让我们从保留一个 var direction_sign = direction.sign()
var length_between = direction_sign/direction
向量开始(这将允许我们进行下一次重构):
func segment(start:Vector2,end:Vector2) -> Array:
var length = start.distance_to(end) # (end - start).length()
var direction = start.direction_to(end) # (end - start).normalized()
var direction_sign = direction.sign()
var next = next2(start,direction)
var length_between = direction_sign/direction
var traversed_length = 0
while traversed_length < length:
var cell = start + direction * traversed_length
if length_to_next.x < length_to_next.y:
traversed_length += length_to_next.x
length_to_next.x = length_between.x
length_to_next.y -= length_to_next.x
else:
traversed_length += length_to_next.y
length_to_next.x -= length_to_next.y
length_to_next.y = length_between.y
在我的测试中,这个版本有时会跳过图块。
现在,我们有了 func segment(start:Vector2,direction)
var length_between = direction_sign/direction
var traversed_length = 0
var result = []
result.append(start)
while traversed_length < length:
if length_to_next.x < length_to_next.y:
traversed_length += length_to_next.x
length_to_next.x = length_between.x
length_to_next.y -= length_to_next.x
else:
traversed_length += length_to_next.y
length_to_next.x -= length_to_next.y
length_to_next.y = length_between.y
result.append(start + direction * traversed_length)
return result
,拥有 current
就不那么重要了。要删除它,我们将累积长度,从不减去(这样我们每个周期都做的更少):
func segment(start:Vector2,direction)
var length_between = direction_sign/direction
var traversed_length = 0
var result = []
var current = start
while traversed_length < length:
if length_to_next.x < length_to_next.y:
current += direction * length_to_next.x
traversed_length += length_to_next.x
length_to_next.x = length_between.x
length_to_next.y -= length_to_next.x
else:
current += direction * length_to_next.y
traversed_length += length_to_next.y
length_to_next.x -= length_to_next.y
length_to_next.y = length_between.y
result.append(current)
return result
这种作品。舍入有问题。请参阅“hacky”部分。
让我们将长度缩小 current
(好像我们的原始参数是从 0 到 1)。我们也可以去掉 traversed_length
,当我们这样做时,内联 func segment(start:Vector2,direction)
var length_between = direction_sign/direction
var result = []
var current = start
result.append(current)
while length_to_next.x < length or length_to_next.y < length:
if length_to_next.x < length_to_next.y:
current.x += direction_sign.x
length_to_next.x += length_between.x
else:
current.y += direction_sign.y
length_to_next.y += length_between.y
result.append(current)
return result
。通过这样做,我们不需要平方根。此外,我们不再需要 length
。
direction
如果您更喜欢更接近论文“快速体素遍历算法”的命名法,则将 next
重命名为 div2
,将 func segment(start:Vector2,end:Vector2) -> Array:
var difference = end - start
var direction_sign = difference.sign()
var length_to_next = (next2(start,direction_sign) - start)/difference
var length_between = direction_sign/difference
var result = []
var current = start
result.append(current)
while length_to_next.x < 1 or length_to_next.y < 1:
if length_to_next.x < length_to_next.y:
current.x += direction_sign.x
length_to_next.x += length_between.x
else:
current.y += direction_sign.y
length_to_next.y += length_between.y
result.append(current)
return result
重命名为 length_between
,并将 { {1}} 到 tDelta
。另一个区别是我假设网格大小为 length_to_next
。
关于 tMax
和“hacky”部分
我们需要一个 direction_sign
函数,它看起来像这样:
step
还有一个 1
函数。对于“慢体素遍历算法”,是这样的:
next
那里发生了什么?好吧,如果 next2
不是整数,那么 func next2(vector:Vector2,direction:Vector2) -> Vector2:
return Vector2(next(vector.x,direction.x),next(vector.y,direction.y))
或 next
取决于 func next(value:float,direction:float) -> float:
if direction > 0:
return max(ceil(value),floor(value + 1.0))
return min(floor(value),ceil(value - 1.0))
就足够了。但是,如果 value
是整数,我们要加或减 ceil
。就这么写,我不需要检查值是否是整数。
但是请注意,我说的是“慢体素遍历算法”。对于“快速体素遍历算法”,我通过实验发现它应该是:
floor
在与问题保持距离后,我找到了一个更好的版本:
direction
我不知道为什么会这样。但是,据我所知,这是因为我们增加了 value
。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。