找回密码
 立即注册
查看: 264|回复: 3

Unity寻路插件(A* Pathfinding)进阶教程六:运行时更新 ...

[复制链接]
发表于 2022-11-14 08:51 | 显示全部楼层 |阅读模式
本系列的教程文章基于 A*Pathfinding Project 4.2.8的官网教程翻译,每一章节的原文地址都会在教程最下方给出。
(译注:本篇实战 非常重要)
目录:

  • 概览
  • 哪些需要更新
  • 使用脚本
  • 调试
  • 技术细节
  • Garph更新物体
  • 检查阻塞位置
概览

有些时候,运行时更新graph十分的有必要。比如玩家见着了一个新的建筑,或者一扇门被打开了。Graphs可以整体更新,也可以局部更新。当你的地图数据整体被修改了,那么一个完全的重计算是最好的方式,但是如果你正需要更新一小部分的区域,那么局部更新会更加迅捷有效。
有很多的方式可以更新一个Graph,也有很多的场景和方式是你想要去做的,所以下面大致概括了一下大多数情况下的应对方案。不过要注意的是,游戏的类型千差万别,需求也不尽相同,所以很多情况是没有概括进来的。

  • 你想要重新计算全部的graph
参见下方 重计算整个视图


  • 设置节点是否行走
参见下方 直接访问图形数据 或者


  • 更新graph数据(当某些物体移动或者创建/删除)
参见下方 重计算graph局部数据 or
or


  • 更新单一节点属性
参见下方 直接访问图形数据 or


  • 动态加载或者卸载graph


  • 小型的动态跟随玩家的graph


  • 创建节点并且用代码将他们链接起来
or

  • 让graph的某一部分的通行代价更高
参见下方 重新计算局部数据 or 直接访问图形数据 or


  • 阻止某些单位穿过某些节点,但是允许其他的单位穿过
or 重新计算局部数据 or直接访问图形数据

  • 运动物体上的路径查找(比如一艘船)
示例场景 Moving 文档尚未准备好


  • 添加一个障碍物,但是首先确保不阻挡任何单位 (塔防游戏)
排版好乱~知乎不知道怎么弄表,贴下原文的表结构吧


哪些需要更新

完成graphs的更新,你通常需要做两件事中的一件。
你可能希望用相同的设置重新计算出graph,一如它刚开始初始化一样。但是如果你仅仅是想更新graph里很小的一个区域,那么重新计算是一个非常浪费的行为(AstarPath.Scan)。比如玩家在塔防游戏中,建造了一个防御塔。
或者你想在已有的garph上改变一些设定,比如你想改变一些节点的Tag或者penalty。
计算视图的局部区域,其实可以使用和开始一样的方法就可以了,但是NavMeshGraph类型的最好不要,因为计算一个局部的计算对于导航网格来说没有什么意义。计算的方法可以是脚本驱动,或者使用updatePhysics组件,把updatePhysics的参数设置为true。很抱歉,这个命名和用途没有对应上……
Grid graphs会按照你期待的方式进行工作。你知道给它指定边界,然后它会为你做好一切。它可能会计算一个比你指定的区域稍微大一点的范围,来确保一些渐变或者侵蚀类型的被正确计算。
Recast graphs类型的一个Tile只能整体一起计算。如果一个更新的请求被发出了,那么这个tile所有相邻的tile也需要被重新计算。一次,一个适当大小的tile设置很重要,它可以避免花费大量时间进行重计算。但是如果太小了的话,又可以把它转变成一个grid graph。如果你使用多线程去计算一个庞大的tile的话,大概率会被分配到一个单独的线程去计算,来防止对FPS产生较大影响。
Point graphs则会重新计算所有通过了边界的连接。但是它不会给它们添加新的节点。这个时候你只能用AstarPath.Scan了。
你可以改变节点的Tag属性,这可以让一些单位穿过或者不能穿过它。
你可以改变节点上的penalty,来让某些节点更慢或者更难穿过,这样agent就可以有权重的选择不同的节点来完成路径。当然也会有一些限制,你不能指定negative penalties,因为算法不支持。(如果要支持的话,那么性能会下降很多)但是一个常见的技巧就是设置一个非常大的初始penalty值。然后从最高的那个值开始递减。注意,这个会让寻路变的很慢,因为它必须要遍历和比较更多的节点来确定路线。penalty值要求的比较高,但其实并没有一个确定的单位来衡量penalty,大概1000就类似于Unity里的1吧。
节点的可行走属性也是可以直接修改的,因此,可以让没写边界里的节点都是可通行的或者不可通行的。
GraphUpdateScene组件可以查看对于的文档页面。GraphUpdateScene组件的设置表,大概是1:1的对照GraphUpdateObject。而GraphUpdateObject则是升级graph所使用的脚本。所以无论如何,我都建议你阅读一下,哪怕你不用脚本。
重计算整个Graph

用下面的方法可以完整的更新所有graph或者其中的某些Graph
// Recalculate all graphs
AstarPath.active.Scan();

// Recalculate only the first grid graph
var graphToScan = AstarPath.active.data.gridGraph;
AstarPath.active.Scan(graphToScan);

// Recalculate only the first and third graphs
var graphsToScan = new [] { AstarPath.active.data.graphs[0], AstarPath.active.data.graphs[2] };
AstarPath.active.Scan(graphsToScan);
或者异步计算(pro版本特有功能),它并不能确保帧率上的优化,但是好处是能看到进度条:
IEnumerator Start () {
    foreach (Progress progress in AstarPath.active.ScanAsync()) {
        Debug.Log("Scanning... " + progress.description + " - " + (progress.progress*100).ToString("0") + "%");
        yield return null;
    }
}
重计算graph局部数据

小的graph更新,可以通过两种方法,一种是使用GraphUpdateScene组件,好处是可以直接在Unity的Inspector面板上编辑;另外一种方法则是使用AstarPath类传入一个Bounds或者GraphUpdateObject类。
// As an example, use the bounding box from the attached collider
Bounds bounds = GetComponent<Collider>().bounds;

AstarPath.active.UpdateGraphs(bounds);
或者
// using Pathfinding; //At top of script

// As an example, use the bounding box from the attached collider
Bounds bounds = GetComponent<Collider>().bounds;
var guo = new GraphUpdateObject(bounds);

// Set some settings
guo.updatePhysics = true;
AstarPath.active.UpdateGraphs(guo);
对应的方法将会把任务推送到一个队列里,在下次路径计算之前得出结果。它不能直接计算结果的原因是因为寻路本身也是异步的,尤其是当开启了多线程之后,直接计算会干扰正在执行的路径计算,导致各种问题出现。这就代表,你可能不会直接看到当前的graph直接更新,但是总会在下一次路径计算之前得到正确的数据和结果。
Recast graphs类型可以使用navmesh cutting的方式进行伪更新。navmesh 可以帮障碍物挖一个洞出来,但是也有局限性,就是它不能增加。这种方式要比整体更新更加的快速,但是限制也大。
GraphUpdateScene组件通常在UnityEditor环境下对一个已知的graph处理更加的方便。比如,你可以非常简单的处理一些tag,而不用写一个字符的代码。相对而言,用在运行时用代码更新graph也没有那么麻烦,比如说了很多遍的塔防游戏里,新建了一座防御塔。
使用脚本

使用GraphUpdateObject更新一个graph,然后传入参数,然后调用AstarPath.active.UpdateGraphs方法更新.
// using Pathfinding; //At top of script

// As an example, use the bounding box from the attached collider
Bounds bounds = GetComponent<Collider>().bounds;
var guo = new GraphUpdateObject(bounds);

// Set some settings
guo.updatePhysics = true;
AstarPath.active.UpdateGraphs(guo);
bounds变量是指UnityEngine.Bounds,它定义了一个轴对齐的box用来更新graph。通常,你可能想更新一些新创建的物体,比如这个例子中的bounds 是从Collider组件里拿到的。
但是要确保这个物体能被graph正确的识别出来。grid graphs的graph你就要确保这个物体的layer包含在collision testing mask 和height teting mask中。
如果你不需要重新计算所有节点的参数,比如一个grid graph或者point graph中的连接。你可以将updatePhysics设置为false.
guo.updatePhysics = false;
想要详细了解各种设置选项,可以看一下 GraphUpdateObject 类详情。
直接访问图形数据
某些情况下可能不方便使用GraphUpdateObject更新graph。那么你可以直接更新graph data,但是切记要小心!在使用GraphUpdateObject的时候,它会帮你做很多很多的事情。
下面展示一个示例,使用一个prelin noise (
)来更新节点是否可行走。
AstarPath.active.AddWorkItem(new AstarWorkItem(ctx => {
    var gg = AstarPath.active.data.gridGraph;
    for (int z = 0; z < gg.depth; z++) {
        for (int x = 0; x < gg.width; x++) {
            var node = gg.GetNode(x, z);
            // This example uses perlin noise to generate the map
            node.Walkable = Mathf.PerlinNoise(x * 0.087f, z * 0.087f) > 0.4f;
        }
    }

    // Recalculate all grid connections
    // This is required because we have updated the walkability of some nodes
    gg.GetNodes(node => gg.CalculateConnections((GridNodeBase)node));

    // If you are only updating one or a few nodes you may want to use
    // gg.CalculateConnectionsForCellAndNeighbours only on those nodes instead for performance.
}));
上面的代码会生成下面这个graph


graph的data在修改的时候,一定要保证安全。路径朝赵可能会在任何时间进行,所以在更新数据的时候,一定要先暂停寻路。最简单的方式就是使用AstarPath.AddWorkItem。
AstarPath.active.AddWorkItem(new AstarWorkItem(() => {
    // Safe to update graphs here
    var node = AstarPath.active.GetNearest(transform.position).node;
    node.Walkable = false;
}));
or
AstarPath.active.AddWorkItem(() => {
    // Safe to update graphs here
    var node = AstarPath.active.GetNearest(transform.position).node;
    node.position = (Int3)transform.position;
});
当不同节点之间的连接状态发生了变化,系统会自动重计算graph的连接组件。你可以仔细阅读以下类的文档。
Documentation但是,在work items工作期间,数据可能并不是最新的。如果你想小勇一些方法诸如:PathFinding.PathUtilities.IsPathPossible或者 PathFinding.Graph.Area属性的时候,你应该先调用一下ctx.EnsureValidFloodFill,但通常情况下可以不用。
4.2之前,你可能会调用诸如QueueFloodFill的方法来保持数据更新到最新,但是4.2以后就不予要了,系统会在后台自动以更好的方式处理它。
AstarPath.active.AddWorkItem(new AstarWorkItem((IWorkItemContext ctx) => {
    ctx.EnsureValidFloodFill();

    // The above call guarantees that this method has up to date information about the graph
    if (PathUtilities.IsPathPossible(someNode, someOtherNode)) {
        // Do something
    }
}));
如果你修改了一些可行走区域,要确保它们之间的连接进行重计算。对于grid类型的graph来说尤为重要。你可以使用GridGraph.CalculateConnections方法。注意这不仅要在你更改的可移动性节点上调用,也需要在和它相邻的其他节点上调用,因为他们可能必须更改连接以添加或者删除节点才可以。如果你只是更新了少量的节点,你可以只用GridGraph.CalculateConnectionForCellAndNeighbours方法来避免他迭代到其他的节点上。
AddWorkItem方法也可以有别的用途,比如下面这个示例,如果有必要你可以把计算分散到不同的帧去做。
AstarPath.active.AddWorkItem(new AstarWorkItem(() => {
    // Called once, right before the
    // first call to the method below
},
        force => {
    // Called every frame until complete.
    // Signal that the work item is
    // complete by returning true.
    // The "force" parameter will
    // be true if the work item is
    // required to complete immediately.
    // In that case this method should
    // block and return true when done.
    return true;
}));
其他用法可以参考:
调试

有一些改变是立即可见的,比如是否可通行和位置。但是Tags and penalties是不能直观的在graph上可见的。但其实也有一些方法可以让它们变的可见。
可见模式可以在A* Inspector -> Settings -> Debug -> Graph Coloring下编辑,把它设置为Tags,那么所有的tags都会用不同的颜色表示出来。




当然你也可以把它设置为penalties。默认情况下,它会把penalty值最高的设置为红色,最低的设置为绿色。


技术细节

当使用GraphUpdateObject执行graph的更新时,所有的graph都会被遍历一次,那些需要更新的(内置的)都会调用他们的UpdateArea函数。
graphs会发送给每个受影响的节点,使用PathFinding.GraphUpdateObjectApply 来改变penalty、walkability或者其他指定的参数来更新节点。Graphs也可以使用自定义的逻辑对象,比如GridGraph(参见下面的 GridGraph 特殊细节)。你其实不需要明白所有的不同之处,知道怎么用就可以了。这些是提供给那些想研究源码的朋友了解的。
GridGraph 特殊细节
updatePhysics变量在更新gird graphs的时候非常重要。如果设置为true的时候,所有受影响的节点都会重新计算他们的高度,然后检查它们是否仍然可以通行。大部分时候,保持这个变量为true吧。
更新一个GridGraph的时候,GraphUpdateObject's Apply 方法会被每一个在边界内的节点调用。它会去检查updatePhysics变量,如果是true(默认值),该区域将通过Collision Testing settings中设置的指定直径来做扩展,每一个在区域里的节点都会哪来做碰撞测试。如果是false的话,那么只对区域内的节点进行调用,不会去做其他额外事情了。
基于navmesh的graphs
主要是指 NavMeshGraph 和 RecastGraph。他们仅仅支持更新penalty、walkability或者对于一些已经存在的节点,更新它们所有的Tiles。GraphUpdateObjects并不会创建新的节点,它只会影响它所包含的那些节点/三角形(GUO bounds)。
PointGraphs
Point graphs只会计算边界内的每个节点。如果GraphUpdateObject.updatePhysics为true的话,那么它还会计算每个穿过它的连接。
Point graphs 更新只支持pro版本
GraphUpdateObjects

GraphUpdateObject包含了一些变量,用来对节点进行升级,可以查看对应的类文档。
从GraphUpdateObject继承
可以从它继承来重写一些重要的方法
using UnityEngine;
using Pathfinding;

public class MyGUO : GraphUpdateObject {
    public Vector3 offset = Vector3.up;
    public override void Apply (GraphNode node) {
        // Keep the base functionality
        base.Apply(node);
        // The position of a node is an Int3, so we need to cast the offset
        node.position += (Int3)offset;
    }
}
然后就可以这样使用GUO
public void Start () {
    MyGUO guo = new MyGUO();

    guo.offset = Vector3.up*2;
    guo.bounds = new Bounds(Vector3.zero, Vector3.one*10);
    AstarPath.active.UpdateGraphs(guo);
}
检查阻塞位置(Check for blocking placements)

塔防游戏中有个很重要的事情就是,当玩家放置了一些塔之后,一定不能隔断了怪物从出生点到目标点的路径。这看起来似乎很难去做,但是很幸运,我们给你准备了一个API。
PathFinding.GraphUpdateUtilities.UpdateGraphsNoBlock方法可以检查一个graph上两个点之间是否被阻断了。但是这个API比正常的更新graph要慢一些,所以最好不要频繁使用。
举个例子。当你在塔防游戏中,放下一座防御塔,你可以先实例化它,然后调用UpdateGraphsNoBlock方法来检查,是否刚放下的塔阻断了Path。如果是的话,那么要立刻移除塔,然后通知玩家另外选一个可用的空地防止。你也可以传递一个节点的List给UpdateGraphsNoBlock方法,所以你可以确保起点到终点之间有任意一条路径没被阻断,所有的单位能正常到达终点即可。
var guo = new GraphUpdateObject(tower.GetComponent<Collider>().bounds);
var spawnPointNode = AstarPath.active.GetNearest(spawnPoint.position).node;
var goalNode = AstarPath.active.GetNearest(goalPoint.position).node;

if (GraphUpdateUtilities.UpdateGraphsNoBlock(guo, spawnPointNode, goalNode, false)) {
    // Valid tower position
    // Since the last parameter (which is called "alwaysRevert") in the method call was false
    // The graph is now updated and the game can just continue
} else {
    // Invalid tower position. It blocks the path between the spawn point and the goal
    // The effect on the graph has been reverted
    Destroy(tower);
}
原文地址:

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-11-14 08:54 | 显示全部楼层
你好,请问当我一个建筑物使用GraphUpdateObject执行局部graph的更新之后,如果我又一次移动了这个建筑物,再次执行此建筑物的局部graph的更新.那么上一次建筑物所在的空出来的位置就还是不可行走区域.请问这个如何解决?直接挂载DynamicGridObstacle是可以实现动态修改局部graph.但是如上手动修改的话,没办法清楚上一次已经可以行走了的graph数据.
发表于 2022-11-14 08:59 | 显示全部楼层
最简单的方式:

挂 DynamicGridObstacle 但是在Update中使用标志决定是否return 移动建筑之后 重置标志

或者移动建筑之后挂DynamicGridObstacle  执行生成之后 删除脚本
发表于 2022-11-14 09:06 | 显示全部楼层
DynamicGridObstacle还会让碰撞体外一点范围内本来不能走的也设置成能走,不知道怎么处理
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-4-29 01:21 , Processed in 0.094318 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表