基于网格的寻路
在本教程中,我们将制作一个基于二维数组网格的寻路模拟。这将帮助我们了解如何为游戏制作寻路玩法。我们会使用空的方形对象作为图块,并用一个圆形对象作为化身。化身可以根据我们的指令在网格区域内移动。
I. 设置对象
我们的项目中只会用到四个对象:绿色化身、蓝色图块、白色路障,以及用于设置和显示网格的网格标签。
化身
我们的化身是一个空对象,x 和 y 缩放比例均为 50%。将它的碰撞形状设为圆形,这样它看起来就会和下图中的对象一样。该对象有三个预定义属性:所在图块、上一个图块和目标图块。将这些属性的默认值设为 0,这样之后获取它们时就不会得到 null 值。
图块对象
再创建一个空对象,用于显示网格中的图块。它的缩放比例同样为 50%,并且只有一个预定义属性:所在图块。也将此属性的值设为 0。
路障对象
再创建一个空对象,作为网格中的路障。这是一个缩放比例为 40% 的白色空对象。化身无法移动到有路障的图块上。
网格标签
这是一个标签,其中包含用于设置和显示网格的行为。它有两个预定义属性:起始 x,值为 4;起始 y,值为 3。这些值是偏移量,会在生成图块时用作参考,以便将网格整齐地放置在中央。
化身/图块的预定义属性:
这里将图块定义为一个包含两个值的数组,分别对应网格中的 x 和 y 坐标。
- 所在图块 - 对象当前在网格中所处图块的值
- 上一个图块 - 化身之前经过的图块
- 目标图块 - 化身将要前往的图块
II. 设置网格
我们会在网格标签中定义二维数组并生成网格。这个对象有两个主要部分:设置数组和显示数组。先来制作“设置数组”行为组。
设置数组 - 行为组
首先,创建一个名为“Grid”的空数组。该数组将包含我们的二维数组。我们还需要一个名为“x row count”、值为 24 的盒子容器,以及一个名为“y row”的空数组,其中包含 18 个空值。
行为组中的第一个行为是循环,重复次数为“x row count”。循环每执行一步,就向“Grid array”中添加一个空的 y 行。这样就会创建一个 24x18 的数组,其中一个数组嵌套在另一个数组中。
请注意,我们在行为组开头连接了一个未激活的注释,用来避免这个根行为自动执行。根行为是指不会由任何事件触发,而是直接放置在界面中的行为。
显示网格 - 行为组
这里主要要做的是遍历“Grid array”中的每个值,并生成一个图块对象。我们还会在循环中添加一个条件,以便在图块对象上生成白色路障。
首先,获取“y row array”的数组数量,以及此对象的“start x”和“start y”属性值。
创建一个重复次数为“x row count”的循环。每次循环时,将该循环的索引值与“start x”属性值相加。这样就能得到稍后用于放置生成图块的绝对 x 坐标。接下来创建“y row loop”,重复次数为“y row count”。每次循环时,将该循环的索引值与“start y”属性值相加,从而得到绝对 y 坐标。
在“y row loop”下方添加这些值后,生成蓝色对象。将它的存活对象数量设为 999,因为网格需要大量图块,并将持续时间设为 0。然后将生成的对象移动到一个点:x 坐标取自 x 循环的“add values”,y 坐标取自 y 循环的“add values”。
接下来添加一个名为“On tile”的数组,其中包含两个空值,我们只会将它用作数组占位符。移动图块对象后,修改“On tile”数组,将索引 0 处的值替换为“x row loop”的值。然后再添加一次数组修改,将索引 1 处的值替换为“y row index”。这就是对象将作为“on tile”属性保存的网格 x、y 坐标。
现在,为生成的对象设置属性:动态键为“on tile”,值为“On tile - array”。
仍然在 y 行循环下方,我们还要设置生成白色路障对象的条件。我们会让这个行为组有五分之二的概率执行,这样网格就会更具随机性。接着,在“Grid”数组中将该对象对应的 x、y 坐标标记为 1,表示该坐标处生成了路障。
首先,生成一个 1 到 5 的随机数。如果该数字小于或等于 2,就生成路障。将它的存活对象数量设为 999,并将对象移动到之前生成的蓝色图块所在的同一位置,持续时间也设为 0。
接下来,如果生成了路障,就记录该坐标被路障占用的信息。我们会将该坐标标记为 1。首先,获取“Grid”数组中索引为“x row loop”的值。这个值会给我们一个“y row”数组。修改这个“y row”,将索引“y row loop”处的值替换为 1,也就是标记值。由于此修改只会作用于该值,而不会直接作用于“Grid”本身,因此我们还需要修改“Grid”数组。将“Grid”数组中索引“x row loop”处的值替换为“modify y row - array”。
运行后,结果应该类似于以下形式。
III. 按下图块时
要与网格本身交互,我们需要在按下蓝色图块时执行一个行为组。第一次按下时,化身会出现在网格中;之后再次按下图块,就会开始寻路。
在蓝色对象中,有一个包含三个主要部分的行为组:重置化身的上一个图块,设置它的所在图块和目标图块,然后设置颜色动画,以便在屏幕上显示触摸事件。
首先,获取图块对象的“on tile”属性。将化身的“previous tile”属性设为 0。化身会避免返回“上一个图块”,因此每次按下图块时都要重置“previous tile”。
接下来,获取化身的“on tile”属性。第一个 if 条件用于判断“on tile”的值是否等于 0。这表示化身尚未出现。如果条件为真,就将化身的“on tile”属性设为图块对象的“on tile”属性值。然后获取图块对象的位置,并将化身移动到该位置,x 和 y 坐标分别指向对应位置,持续时间设为 0。
如果“on tile”的值不等于 0,就将化身的“target tile”属性设为图块对象的“on tile”属性值。这表示化身已经在网格中,此时我们可以执行“find path”行为组。我们还没有创建这个行为,稍后在本教程中设置“find path”行为组时再完成它。
将化身正确放置到网格后,我们只需显示这次按下事件。首先添加一个根行为“get color”,获取对象的原始颜色。按下图块后,将图块对象的颜色设为黑色,持续时间为 0;然后再将颜色恢复为原始值,持续时间为 0.2。
按下蓝色图块后,效果应该如下所示。
IV. 执行“Find Path”行为组
在本教程的最后一部分,我们将编写一种算法,用来寻找化身到达目的地时可以前往的最近图块。化身在网格中每移动一步,就会重复执行这个行为组。
我们会在化身对象中设置该算法,主要分为三个部分:查找相邻图块、查找最近的相邻图块,以及移动到该相邻图块。
查找相邻图块
在这部分中,我们会收集化身可以移动到的所有相邻图块。我们会检查四个方向的相邻图块,判断这些图块上是否有路障,或化身之前是否经过了这些图块。
首先添加“Adjacent tiles”数组。然后添加一个包含两个空值的“Direction”数组,以及四个分别包含两个值的方向数组:N (0,1)、S (0,-1)、E (1,0)、W (-1,0)。
行为组中的第一个行为是清空相邻图块数组。由于我们会重复执行这个行为组,因此每次都需要重置收集到的“adjacent tiles”。接下来的四个行为用于修改“Direction”数组:依次将四个方向数组设为“Direction”数组,然后执行下方的行为组。这样就能创建一个循环,每次循环中的“Direction”数组值都不同。
有了方向后,只需将方向的 x、y 值分别与化身的“on tile”属性相加。这样就能获取“Grid”数组中的目标值,判断该图块是否被标记为有路障。
“Direction x”是“Direction”数组索引 0 处的值,“get on tile x”是“get on tile”索引 0 处的值。将“direction x”和“get on tile x”相加,即可得到“Target x”。y 值的处理方式相同,但目标索引为 1。
“Target y row”是“Grid”数组中索引为“Target x”的数组值。然后,“Target index in y row”是“Target y row”数组中索引为“Target y”的值。这就是网格中的对应坐标,我们可以据此判断该坐标是否被标记为有路障。
如果“Target index in y row”的值为 1,则目标图块被标记为阻挡。如果该值不等于 1,就可以继续执行行为组。
由于“Target x”和“Target y”通过了条件检查,我们添加了一个名为“New adjacent tile”的空图块数组,用来存储作为图块数组的“Target x”和“Target y”值。将“Target x”的值设为“New adjacent tile”的索引 0,将“Target y”的值设为索引 1。
现在获取化身的“previous tile”属性。如果“previous tile”不等于“new adjacent tile”,就表示这个新图块之前没有经过。此时可以安全地将“New Adjacent tile”追加到“Adjacent tiles”数组中。等化身成功移动到另一个图块后,我们再设置“previous tile”的值。
“你可以扩展此行为组,创建八个方向的数组,让化身也能斜向移动。”
查找最近的相邻图块
收集到可用的相邻图块后,我们只需找出与化身目标图块距离最近的相邻图块。我们会使用目标图块和相邻图块的 x、y 值,通过距离公式进行计算。
这个行为组有两个关键部分:初始化计算变量,以及在循环中计算图块距离。
a. 初始化变量
首先,获取相邻图块的数组数量。然后获取化身的“target tile”属性,并取出它的 x、y 值。我们要添加两个新的盒子容器:“Min distance”和“Target tile index”。将“Min distance”的初始值设为 9999,并将“Target tile index”的值设为 0。将最小距离设为一个较大的数,是因为我们需要从起点开始寻找最小距离。“Target tile index”则是我们在“Adjacent tiles”数组中的参考索引,它对应的图块与“target tile”的距离最小。
我们的主要目标是获取“Adjacent tiles”数组中与“target tile”距离最近的图块索引。
b. 计算图块距离
在这个行为组中,我们会在循环中计算每个相邻图块的距离。循环会重复执行“adjacent tiles count”次,并使用循环索引获取“Adjacent tiles”数组中的图块值。
获取该相邻图块的 x、y 值,然后使用以下公式计算“target tile”与“adjacent tile”之间的距离:
distance = sqrt((x2 - x1)^2+(y2 - y1)^2)
x1 = Target x
x2 = Adjacent x
y1 = Target y
y2 = Adjacent y
先分别将它们的 x、y 值相减,再将差值与自身相乘,得到平方值;然后将平方值相加,最后对总和开平方,得到距离值。
如果“Distance”的值小于“Min distance”盒子容器中的值,就将“target tile index”设为当前循环索引,也就是我们从“Adjacent tiles”数组中获取的图块索引。同时,将“Min distance”盒子容器的值设为“Distance”,以便与循环中的下一个图块进行比较。
循环结束后,我们就比较了每一个相邻图块,并得到了与“target tile”距离最近的相邻图块索引。
移动到相邻图块
在本教程的最后一个行为组中,我们会将化身移动到最近的相邻图块;如果尚未到达“target tile”,就重新执行整个“Find Path”行为组。
首先检查“Adjacent tiles count”是否大于 0。这样可以确保只有在获取到至少一个可靠的相邻图块时才会移动。
然后使用上一个行为组中的“Target tile index”值,获取“Adjacent tiles”数组中的数组值,也就是“Get the nearest tile”。
为了获取化身要移动到的准确位置,我们需要该图块的 x、y 坐标,以及“Grid”标签的“start x”和“start y”属性值。将“nearest tile”的 x 值与“start x”相加,并将 y 值与“start y”相加。这两个和就是化身将要移动到的目标 x、y 坐标。
移动到该位置之前,将化身的“previous tile”设为它当前的“on tile”属性值。我们可以从前面的行为组中获取这个“on tile”值。然后将化身移动到目标坐标,持续时间设为 0.2。
“Move to point”行为完成后,将化身的“on tile”属性值设为“Get nearest tile”数组值。如果“Get nearest tile”数组值不等于“target tile”,就执行“Find Path”行为组,从而重新执行整个行为组。“target tile”属性值可以从前面的行为组中获取。
按下任意一个蓝色图块后,化身会出现在网格中,随后执行“Find Path”行为组。这会触发化身逐个图块移动。
结论
在本教程中,我们学习了如何使用名为“Grid”的数组制作对象寻路活动,该数组用于存储图块是否被占用的信息。你可以扩展这个思路,在“Grid”数组中使用对象 ID 来存储信息,而不是只使用数字。这样就能获取更可靠的信息并用于寻路。例如,我们可以获取该对象 ID 的属性,以判断该对象是否存活,以及是否允许化身进入它所在的图块。
本教程仅介绍了寻路背后的基本思路。它的一个缺点是无法保证找到前往目标图块的最短路径,而且只会检查当前相邻的图块。这可能导致化身不断绕圈,无法抵达目标。
不过,我们可以通过扩展寻路算法来解决这个问题。我们可以继续遍历已获取的相邻图块,查找它们的相邻图块,直到找到目标图块。每个找到的最近相邻图块都会添加到列表中。为防止无限循环,可以添加一个条件:如果某个相邻图块已经检查过,就不要将它添加到列表中。此外,还要为迭代次数设置上限,以防目标图块距离太远或无法到达时发生无限循环。迭代找到目标图块后,就退出迭代,并使用图块列表让化身逐个图块移动。

