微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Godot 引擎用 Tilemap 瓷砖填充 2D 多边形

如何解决Godot 引擎用 Tilemap 瓷砖填充 2D 多边形

我在 Godot 引擎中遇到了一个我无法解决的问题: 怎么可能(在代码中)用瓷砖填充 polygon2D 区域? 我试图获得积分位置。使用 2D for 循环遍历线的顶点,我就是无法理解这一点。

Polygon2D plus TileMap with tiles on the perimeter of the polygon

这是我期望的结果的模型

解决方法

我有解决方案,但有一点“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 举报,一经查实,本站将立刻删除。