Unity寻路导航NavMesh

寻路导航NavMesh

Unity中对于寻路算法进行了封装,支持提交将地图进行导航烘焙,降低了实时计算的消耗
菜单Window–>Navigation,打开导航面板

操作

  • 选择不移动的游戏对象,勾选Navigation Static

  • 如果是不连接的游戏对象,勾选 Generate OffMeshLinks

  • 如果需要设置区域,可以在属性 Navigation Area下选择,使用方式与层一样

  • 点击Areas标签,可以添加区域,内置创建了三个区域,表示可以行走、不可以行走、可跳跃

  • 选择完成后,点击Back按钮完成烘焙,点击 Clear按钮清除已烘焙的数据

Demo

建立一个简单的demo来演示寻路导航效果,在一个拥有一堵墙的平面上,要求使用navigation自动导航将小球运动到鼠标点击的位置。

建立的场景如图:

Unity寻路导航NavMesh_第1张图片

将平面设置为walkable、墙设置为unwalkable.生成的导航线路图如蓝色区域。

Unity寻路导航NavMesh_第2张图片

为小球添加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);
            }
        }
    }
}

Unity寻路导航NavMesh_第3张图片

分层区域导航

根据不同的分层区域可以为物体设定不同的导航路线,下列是实现改分层区域导航的demo。通过为不同颜色小球设定不同颜色通行的路径来进行分层区域导航,使得小球可以到达方块的位置。

为不同的路径设定不同的area:

Unity寻路导航NavMesh_第4张图片

Unity寻路导航NavMesh_第5张图片

为不同颜色小球设定不同的Area Mask,以红色小球为例:

Unity寻路导航NavMesh_第6张图片

为小球建立相应的脚本:

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()
    {
        
    }
}

最终的效果图:

Unity寻路导航NavMesh_第7张图片

动态区域导航

接下来的demo展示了如何进行动态的区域导航,通过建立一个类似于桥型的结构,如下图所示,中间蓝色部分的桥每隔一段时间会抬起或者放下,有一个小球从左边走到右边,但是在桥抬起的时候无法走过去。

Unity寻路导航NavMesh_第8张图片

先建立对应的场景,使得平面可以定时绕着绿色小球的位置进行旋转。

//使平面绕着一个球体做旋转
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),等到桥放下后小球才能继续前行。

Unity寻路导航NavMesh_第9张图片

//小球的导航
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);
    }
}

手动编写物体导航demo

有一个如下图的地图,希望小球可以自动移动到鼠标点击的区域,且梯子只能往上爬。所使用的自动导航off mesh link有三种,一种是手动设置的off mesh link,一种是向下跳,一种是水平跳跃。

Unity寻路导航NavMesh_第10张图片

后面两种的off mesh link可以在navigation中进行设置:

Unity寻路导航NavMesh_第11张图片

而手动设置的赋在梯子上的连接线可以通过为梯子添加off mesh link组件来设置。分别在梯子下部和上部放置一个小方块作为start和end

Unity寻路导航NavMesh_第12张图片

最后编写代码让小球可以运动:

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;
                
            }
        }
    }
}

最终可以得到一个逐渐向上爬的一个效果。

Unity寻路导航NavMesh_第13张图片

实现水平跳跃和往下跳的效果

实现水平跳跃的效果实际上要求在边缘的两点做抛物线运动。在没有刚体的情况下,可以自己写代码来模拟中立的作用。水平方向是匀速的,垂直方向上是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;

    }
}

你可能感兴趣的:(Unity笔记)