题外话:最近想学习一下A*插件,由于在网上没有发现什么比较详细的教程,所以就只能上官网了。这是第一次看这么长的英语文章,翻译得不好,请见谅!
概述:
A*插件的核心脚本就是”astarpath.cs”,所以如果你想使用A*插件进行寻路,那么场景中一定要有一个”astarpath.cs”(并且一个场景仅有一个)。可以通过Component->Pathfinding->Pathfinder添加”astarpath.cs”。
第二重要的脚本就是”seeker.cs”,每一个需要寻路的物体最好要有这个脚本(不是一定要添加,但是有了可以让寻路更加简单)。
然后就是一个名叫"SimpleSmoothModifier.cs"的脚本,它可以让路径更加圆滑、简单,它要跟"seeker"脚本一起添加到同一个物体上。
1.创建一个新场景,创建一个plane,位置(0,0,0),缩放(10,10,10)。创建一个新层(edit->project settings->tags and layers),命名为”Ground”,把plane设为”Ground”,创建一些cube作为障碍物,把这些cube设为新的层”Obstacles”。
2.创建一个新GameObject,命名为A*,添加"AstarPath"脚本。脚本中最重要的是两个东西是“Graphs”和下方的“Scan”按钮。“Graphs”包含了所有的寻路图,最多可以有16个,但是一般1到2个已经足够了。其中主要的两个是”Grid Graph”(以格子的方式生成节点)和”Navmesh”(在可行走的区域生成mesh)。“Scan”按钮用来更新寻路图。
3.点击”Grid Graph”,Grid graph会产生一系列的格子,大小为width * height,这个网格可以放在场景中的任何地方,也可以进行旋转。“Node Size”可以设置格子的大小,这里先设为1。注意有一个由5个点组成的正方形图标,点击它的左下方,此时图标那一行前面的文字会变为”Bottom-Left”,设为(-50,-0.1,-50)。其中y方向设置为-0.1是为了避免产生浮点错误,因为地面plane的y向坐标是0,如果寻路图也是y向坐标也为0的话,在进行高度检测的raycast的时候,会产生问题。所以确保寻路图的y要略小于地面的y。为了网格可以适应我们的场景,调整Width和Depth,均为100。(在实际设置中好像有点问题,总之就是让网格跟plane大小适应就是了,可以通过"Scan"进行刷新)
高度检测
4.为了把寻路的node放置到场景中的正确高度,一般向下发射一束射线来进行检测,寻路node会被放置到碰撞点的位置。我们将mask设置为Ground,因为只希望寻路节点与Ground进行检测。如果射线没有发生碰撞,说明检测的物体没有设置为“Ground”,又或者上面的第三点中y设为了0。
碰撞检测
5.当node被放置好后,它就会被用来检测是否可行走,一般可以使用sphere,capsule或ray来进行碰撞检测(这里指的是用Physics.CapsuleCast或者Physics.SphereCast模拟AI对象)。Capsule会使用和AI对象一样的半径和高度来进行碰撞。为了让AI对象和障碍物有一些边缘,这里将Capsule的半径设置为2.另外将碰撞检测的layer设置为Obstacles,因为不想让地面成为障碍。点击底部的Scan,我们就可以看到grid Graph的生成了。
添加AI
6.创建一个capsule,添加”Character Controller”部件,放到plane上。添加”Seeker”部件,Seeker脚本是一个帮助类的脚本,用来将其他脚本的寻路请求进行处理,它也可以处理Path modifier(一般是对寻路结果进行圆滑处理的脚本)。接下来将会写一个脚本用于寻路,当然你也可以用插件内置的脚本。A* 插件自带了两个AI脚本用于挂接到对象上进行寻路:AIPah可适用于任何类型的寻路图;而RichAI主要适用于NavMesh类型。
让物体寻路
7.seek脚本中有一个重要的方法:function StartPath (Vector3 start, Vector3 end, OnPathDelegate callback = null) : Path。参数是开始位置,结束位置,回调函数。其中OnPathDelegate是一个委托,被调用的函数类似于"void SomeFunction (Path p)"
脚本如下:
using UnityEngine;
using System.Collections;
//Note this line, if it is left out, the script won't know that the class 'Path' exists and it will throw compiler errors
//This line should always be present at the top of scripts which use pathfinding
using Pathfinding;
public class AstarAI : MonoBehaviour
{
public Transform target;
public void Start()
{
//Get a reference to the Seeker component we added earlier
Seeker seeker = GetComponent<Seeker>();
//Start a new path to the targetPosition, return the result to the OnPathComplete function
seeker.StartPath(transform.position, target.position, OnPathComplete);
}
public void OnPathComplete(Path p)
{
Debug.Log("Yay, we got a path back. Did it have an error? " + p.error);
}
}
把这个脚本添加到要寻路的物体上,给target赋值,点击"Play"后,你应该会看到一条绿色的线,这就是寻路的路径了,此时物体还不会动。
如果没有看到绿线,检查一下"Seek"的脚本的"Show Gizmos"有无勾上。又或者可能是unity版本的原因,使Gizmos画出来的线被隐藏在plane下面了。
可以看到画出来的线很不圆滑,后面会进行美化的。
设置回调的方式也可以是这样:
//OnPathComplete will be called every time a path is returned to this seeker
seeker.pathCallback += OnPathComplete;
//So now we can omit the callback parameter
seeker.StartPath (transform.position,targetPosition);
注意上面的方法添加回调后,当你要移除或者摧毁该脚本,要:
public void OnDisable () {
seeker.pathCallback -= OnPathComplete;
}
回调后我们可以通过Path p得到计算后的路径,我们可以用它做什么呢?
计算后的路径有两个重要的list。
1.Path.vectorPath是一个Vector3的list,保存着每一个路径点的位置。
2.Path.path是一个node的list,保存着每一个路径点的node。
然后拓展一下上面的脚本:
using UnityEngine;
using System.Collections;
//Note this line, if it is left out, the script won't know that the class 'Path' exists and it will throw compiler errors
//This line should always be present at the top of scripts which use pathfinding
using Pathfinding;
public class AstarAI : MonoBehaviour
{
//The point to move to
public Transform target;
private Seeker seeker;
private CharacterController controller;
//The calculated path
public Path path;
//The AI's speed per second
public float speed = 100;
//The max distance from the AI to a waypoint for it to continue to the next waypoint
public float nextWaypointDistance = 3;
//The waypoint we are currently moving towards
private int currentWaypoint = 0;
public void Start()
{
seeker = GetComponent<Seeker>();
controller = GetComponent<CharacterController>();
//Start a new path to the targetPosition, return the result to the OnPathComplete function
seeker.StartPath(transform.position, target.position, OnPathComplete);
}
public void OnPathComplete(Path p)
{
Debug.Log("Yay, we got a path back. Did it have an error? " + p.error);
if (!p.error)
{
path = p;
//Reset the waypoint counter
currentWaypoint = 0;
}
}
public void FixedUpdate()
{
if (path == null)
{
//We have no path to move after yet
return;
}
if (currentWaypoint >= path.vectorPath.Count)
{
Debug.Log("End Of Path Reached");
return;
}
//Direction to the next waypoint
Vector3 dir = (path.vectorPath[currentWaypoint] - transform.position).normalized;
dir *= speed * Time.fixedDeltaTime;
controller.SimpleMove(dir);
//Check if we are close enough to the next waypoint
//If we are, proceed to follow the next waypoint
if (Vector3.Distance(transform.position, path.vectorPath[currentWaypoint]) < nextWaypointDistance)
{
currentWaypoint++;
return;
}
}
}
然后运行游戏,物体就会移动了。注意路径点不是连续的,当物体离下一个路径点小于nextWaypointDistance时才会继续前进。最后物体会停在离目标前的一段距离,这是因为对于终点,我们需要进行特殊处理,但脚本还没有进行处理。
路径平滑
8.这时要用到内置的脚本"Path Modifiers",要加在有"seeker"脚本的物体上。(不过好像找不到这个名字的脚本,也可以通过Components–>Pathfinding–>Modifiers–>Simple Smooth添加脚本)。平滑的原理就是将一段段的小路径继续细分,让路径点之间的距离缩小。这里先将Max Segment Length设为1,Iterations设为5,Strength设为0.25。运行之后会发现绿线更加平滑了。
最后:因为使用上面的脚本进行寻路并不完美,而且每一次都要写脚本的话会很麻烦,所以直接为要寻路的物体添加"AIPath"脚本,赋值target,效果会更好!上面的脚本就当理解一下内部就好了!