在学习Unity Behavior插件和AStarPathFinding(即A*)插件的时候,遇到了一个小的问题。因为系统自带的Nevigation组件地形需要事先烘培,没办法对随机生成的地图和障碍进行动态Bake,在尝试了很久之后,还是决定放弃自带的,而把眼光瞄上了A*插件。从网上下载了一个学习使用。在网上参考了很多大神的学习笔记,然后开始了自己的尝试。
首先简单讲一下A*使用(网上已经很多资源,不多废话):我们先打开项目,导入A*插件,在项目中加一个Plane,将位置reset为(0,0,0)不妨把Scale设置为10,1,10,顺便可以为它设置一个layer,如Ground;再添加一个空物体,改名为A*,在Project栏搜索AstarPath脚本,给这个空物体挂上。 点击Add New Graph,这里我选择了GridGraph,然后我们来修改网格信息。如下:Width表示宽的这个方向有多少Node,Depth表示长的这个方向有多少个Node,Nodesize则表示每一个Node占了多大的面积,这里刚才设置地板Scale为10乘10,也就是长宽都是100米,所以我们网格大小设置为1,长宽都设置100个节点。往下把Center设置为(0,0,0),Connections这里是设置网格之间怎么连通的,可以选择Four 或者Six 或者 Eight,这个牵扯到人物寻路可以怎么从一个网格到另一个网格,我这里选择Eight。
再往下,我们在Collision testing中选择一些层作为障碍物,比如Wall等。因为A*是通过层来区别障碍物的。下面是表示高度的一个 参考平面,用来做高度检测,有时候我们需要多层的网格。(类似于海拔的基准平面吧,不知道是不是,只是感觉),我们这里选择Ground。这时候我们可以不用去管其他的了,直接拉到最底下点击Scan按钮,或者是按快捷键Ctrl+Alt+S键生成网格(也是更新网格),我们可以看到平面上覆盖了一层网格了,如果看不到,把AstarPath下面的Show Graphs勾选上。
好啦,我们初始工作已经做好了,接下来开始我们的正事,让Player动起来。这里先给Player加上Seeker组件和CharacterController组件(我是通过这个实现移动的),来编写我们的第一段代码吧!
新建一个C#脚本AStar.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Pathfinding;
public class AStar : MonoBehaviour {
// public Transform target;
private Seeker seeker;
private CharacterController characterController;
public Path path;
private float speed = 100;
private float angularSpeed = 10;
private float nextWaypointDistance = 1.5f;
private int currentWaypoint = 0;
private AstarPath aStarPath;
void Start () {
aStarPath = GameObject.Find("A*").GetComponent<AstarPath>();
seeker = GetComponent<Seeker>();
characterController = GetComponent<CharacterController>();
seeker.pathCallback += OnPathComplete; //寻路的一个回调
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
RaycastHit hit;
if (!Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out hit, 100))
{
return;
}
if (!hit.transform)
{
return;
}
seeker.StartPath(transform.position, hit.point, OnPathComplete); //寻路并移动到鼠标点击位置
}
if (Input.GetMouseButtonDown(1)) //按鼠标右键,更新网格,实现了动态障碍物网格生成 嘻嘻
{
aStarPath.Scan();
}
}
public void StarPath(Transform target)
{
seeker = GetComponent<Seeker>();
characterController = GetComponent<CharacterController>();
seeker.pathCallback += OnPathComplete; //寻路的一个回调
seeker.StartPath(transform.position, target.position, OnPathComplete);
}
private void FixedUpdate()
{
Move();
}
public void Move()
{
if (path == null) //如果路径为空,直接返回
{
return;
}
if (currentWaypoint >= path.vectorPath.Count)
{
return;
}
if (characterController != null && characterController.enabled == true)
{
Vector3 dir = (path.vectorPath[currentWaypoint] - transform.position).normalized;
dir *= speed * Time.deltaTime;
characterController.SimpleMove(dir);
Quaternion targetRotation = Quaternion.LookRotation(dir);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.fixedDeltaTime * angularSpeed);
if (Vector3.Distance(transform.position, path.vectorPath[currentWaypoint]) < nextWaypointDistance)
{
currentWaypoint++;
return;
}
}
}
public void OnPathComplete(Path p)
{
if (!p.error)
{
path = p; //如果不发生错误,把p传给path
currentWaypoint = 0;
}
}
private void OnDisable()
{
seeker.pathCallback -= OnPathComplete;
}
//提供get set方法供其他脚本使用
#region GetSet
public float Speed
{
get { return speed; }
set { speed = value; }
}
public float AngularSpeed
{
get { return angularSpeed; }
set { angularSpeed = value; }
}
public float ArriveDistance
{
get { return nextWaypointDistance; }
set { nextWaypointDistance = value; }
}
#endregion
}
运行后点击地面,Player便会寻路,并移动到点击位置,这时候也可以加一些障碍物,按鼠标右键可以在障碍物周围设置障碍网格,通过Scan这个方法可以更新网格信息,实现了网格动态更新。注意:
seeker.StartPath(transform.position, hit.point, OnPathComplete);
这个不能再UpDate中一直执行,否则不会移动。Move函数不做解释,也是参考了别人的一些做法。下一篇博文我将把这个寻路与行为树结合起来,实现一个简单的巡逻任务。