Unity中对于寻路算法进行了封装,支持提交将地图进行导航烘焙,降低了实时计算的消耗
菜单Window–>Navigation,打开导航面板
选择不移动的游戏对象,勾选Navigation Static
如果是不连接的游戏对象,勾选 Generate OffMeshLinks
如果需要设置区域,可以在属性 Navigation Area下选择,使用方式与层一样
点击Areas标签,可以添加区域,内置创建了三个区域,表示可以行走、不可以行走、可跳跃
选择完成后,点击Back按钮完成烘焙,点击 Clear按钮清除已烘焙的数据
建立一个简单的demo来演示寻路导航效果,在一个拥有一堵墙的平面上,要求使用navigation自动导航将小球运动到鼠标点击的位置。
建立的场景如图:
将平面设置为walkable、墙设置为unwalkable.生成的导航线路图如蓝色区域。
为小球添加Nav Mesh Agent组件,已完成自动导航功能。
编写c#脚本完成相应功能:
public class NavMeshDemo : MonoBehaviour
{
private NavMeshAgent agent;
// Start is called before the first frame update
void Start()
{
agent = GetComponent();
}
// Update is called once per frame
void Update()
{
if (Input.GetButtonDown("Fire1")) {
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100f)) {
agent.SetDestination(hit.point);
}
}
}
}
根据不同的分层区域可以为物体设定不同的导航路线,下列是实现改分层区域导航的demo。通过为不同颜色小球设定不同颜色通行的路径来进行分层区域导航,使得小球可以到达方块的位置。
为不同的路径设定不同的area:
为不同颜色小球设定不同的Area Mask,以红色小球为例:
为小球建立相应的脚本:
public class NavMeshAreaDemo : MonoBehaviour
{
private GameObject cube;
// Start is called before the first frame update
void Start()
{
cube = GameObject.Find("Destination");
GetComponent().SetDestination(cube.transform.position);
}
// Update is called once per frame
void Update()
{
}
}
最终的效果图:
接下来的demo展示了如何进行动态的区域导航,通过建立一个类似于桥型的结构,如下图所示,中间蓝色部分的桥每隔一段时间会抬起或者放下,有一个小球从左边走到右边,但是在桥抬起的时候无法走过去。
先建立对应的场景,使得平面可以定时绕着绿色小球的位置进行旋转。
//使平面绕着一个球体做旋转
public class PlaneRotate : MonoBehaviour
{
//
public GameObject ball;
//所绕球体的坐标
public Transform spPos;
//旋转时间间隔
private float time;
//计时
private float time2;
//旋转角度
private float rotateAngle;
//旋转方向
private int direction;
// Start is called before the first frame update
void Start()
{
time = 2f;
time2 = 0f;
rotateAngle = 30f;
direction = 1;
}
// Update is called once per frame
void Update()
{
time2 += Time.deltaTime;
if (time2 >= time) { //每一秒旋转一次
transform.RotateAround(spPos.position,Vector3.forward,rotateAngle*direction);
time2 = 0;
direction *= -1;
ball.SendMessage("SetAreaMask", 6);
}
}
}
然后为小球建立地图烘焙,这里为了增加小球的运动的平顺性,可以生成offmeshlinks,这样在蓝色部分抬起时小球会在红色边缘徘徊等待蓝色部分放下。
为小球建立导航,这里设置了一个SetAreaMask的方法用来设置对应不能走的AreaMask。当桥抬起来的时候,小球不能走上对应的AreaMask(蓝色部分,对应AreaMask=6),等到桥放下后小球才能继续前行。
//小球的导航
public class BallNav : MonoBehaviour
{
//目标位置
public Transform dest;
//
private NavMeshAgent agent;
// Start is called before the first frame update
void Start()
{
agent = GetComponent();
}
//设置对应的areaMask
void SetAreaMask(int areaMask) {
agent.areaMask = agent.areaMask ^ (1 << areaMask);
}
// Update is called once per frame
void Update()
{
agent.SetDestination(dest.position);
}
}
有一个如下图的地图,希望小球可以自动移动到鼠标点击的区域,且梯子只能往上爬。所使用的自动导航off mesh link有三种,一种是手动设置的off mesh link,一种是向下跳,一种是水平跳跃。
后面两种的off mesh link可以在navigation中进行设置:
而手动设置的赋在梯子上的连接线可以通过为梯子添加off mesh link组件来设置。分别在梯子下部和上部放置一个小方块作为start和end
最后编写代码让小球可以运动:
public class BallMaze : MonoBehaviour
{
private NavMeshAgent agent;
// Start is called before the first frame update
void Start()
{
agent = GetComponent();
}
// Update is called once per frame
void Update()
{
if (Input.GetMouseButtonDown(0)) {
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 200f)) {
agent.SetDestination(hit.point);
}
}
}
}
但是使用这种方法建立off mesh link的运动效果是一个瞬间移动的效果,并不是想要的效果,因此下一步来手动进行不同物体间的连接线导航。
希望对在梯子处导航进行优化,有一个向上爬梯子的过程而不是一个瞬间移动的过程。
NavMeshAgent对象的成员
属性isOnOffMeshLink:表示是否处于连接线上
属性currentOffMeshLinkData:获取当前所在连接线的数据,返回 OffMeshLinkData类型的对象
方法stop():停止向导
方法Resume():继续向导
方法CompleteOffMeshLink():完成当前连接线OffMeshLinkData对象的成员
属性startPos、 endPos表示开始、结束的位置,结合Vector3.lerp方法进行中间点的插值计算
属性linkType:表示连接的类型, OffMeshLinkType枚举
将对应脚本改写成如下:
public class BallMaze : MonoBehaviour
{
private NavMeshAgent agent;
//nav mesh agent 数据
private OffMeshLinkData data;
//爬楼梯时长
private float climbTime;
//计时器
private float countTime;
// Start is called before the first frame update
void Start()
{
agent = GetComponent();
climbTime = 2f;
countTime = 0f;
}
// Update is called once per frame
void Update()
{
if (Input.GetMouseButtonDown(0)) {
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 200f)) {
agent.SetDestination(hit.point);
}
}
if (agent.isOnOffMeshLink) { //判断是否处于边缘的offmeshlink连接线上
data = agent.currentOffMeshLinkData; //获取相关数据
agent.isStopped = true;
//agent.Stop(); //暂停使用自动导航
switch (data.linkType) {
case OffMeshLinkType.LinkTypeManual: //手动设定的类型(梯子)
countTime += Time.deltaTime;
transform.position = Vector3.Lerp(data.startPos, data.endPos, countTime / climbTime); //在开始位置和结束位置中取相应比例的值
if (countTime >= climbTime) {
countTime = 0f;
agent.CompleteOffMeshLink();
agent.isStopped = false;
//agent.Resume();
}
break;
case OffMeshLinkType.LinkTypeDropDown: //往下跳
break;
case OffMeshLinkType.LinkTypeJumpAcross: //水平横跳
break;
}
}
}
}
最终可以得到一个逐渐向上爬的一个效果。
实现水平跳跃的效果实际上要求在边缘的两点做抛物线运动。在没有刚体的情况下,可以自己写代码来模拟中立的作用。水平方向是匀速的,垂直方向上是v=a*t(v是速度,a是加速度,t是时间).
添加一个抛物线跳跃的方法:
void JumpAcross(Vector3 jumpFrom, Vector3 jumpTo) {
jumpFrom += Vector3.up; //将出发点向上平移
jumpTo += Vector3.up; //
countTime += Time.deltaTime * (moveSpeed / moveSpeedFixed);
float f1 = countTime / climbTime;
float f2 = countTime - countTime * f1; // 竖直加速运动
Vector3 v1 = Vector3.Lerp(jumpFrom, jumpTo, f1); // 水平匀速运动
transform.position = v1 + f2 * Vector3.up;
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class BallMaze : MonoBehaviour
{
private NavMeshAgent agent;
//nav mesh agent 数据
private OffMeshLinkData data;
//爬楼梯时长
private float climbTime;
//计时器
private float countTime;
// 实际速度
private float moveSpeed = 2;
// 移动速度
private float moveSpeedFixed = 2;
// Start is called before the first frame update
void Start()
{
agent = GetComponent();
climbTime = 2f;
countTime = 0f;
}
// Update is called once per frame
void Update()
{
if (Input.GetMouseButtonDown(0)) {
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 200f)) {
agent.SetDestination(hit.point);
}
}
if (agent.isOnOffMeshLink) { //判断是否处于边缘的offmeshlink连接线上
data = agent.currentOffMeshLinkData; //获取相关数据
agent.isStopped = true;
countTime += Time.deltaTime;
//agent.Stop(); //暂停使用自动导航
switch (data.linkType) {
case OffMeshLinkType.LinkTypeManual: //手动设定的类型(梯子)
transform.position = Vector3.Lerp(data.startPos, data.endPos, countTime / climbTime); //在开始位置和结束位置中取相应比例的值
break;
case OffMeshLinkType.LinkTypeDropDown: //往下跳
JumpAcross(data.startPos, data.endPos);
break;
case OffMeshLinkType.LinkTypeJumpAcross: //水平横跳
JumpAcross(data.startPos, data.endPos);
break;
}
if (countTime >= climbTime)
{
countTime = 0f;
agent.CompleteOffMeshLink();
agent.isStopped = false;
//agent.Resume();
}
}
}
void JumpAcross(Vector3 jumpFrom, Vector3 jumpTo) {
jumpFrom += Vector3.up; //将出发点向上平移
jumpTo += Vector3.up; //
countTime += Time.deltaTime * (moveSpeed / moveSpeedFixed);
float f1 = countTime / climbTime;
float f2 = countTime - countTime * f1; // 竖直加速运动
Vector3 v1 = Vector3.Lerp(jumpFrom, jumpTo, f1); // 水平匀速运动
transform.position = v1 + f2 * Vector3.up;
}
}