https://docs.unity3d.com/Packages/com.unity.ai.navigation@2.0/manual/index.html

官方文档

导航寻路系统概述

该系统允许您创建能够在游戏世界中智能移动的角色。

角色使用从您的场景几何体自动生成的导航网格(NavMesh)进行移动。

动态障碍物允许在运行时更改角色的导航路径,而NavMesh链接则可用于实现特定动作,如开门或跳过间隙。

说人话:生成路径,角色按照路径从起点移动到终点。而我们要去学习的就是如何生成这些路径。

我们将要学习:NavMeshAgentObstacles

导航内部工作原理

首先,我们的目的是规划整个场景的路径,找到场景中的最优路径(怎么规划?怎么找)。然后根据最优路径移动到目的地。

可行走区域

可行走区域:角色(或代理)能够站立和移动的场景位置

烘焙:生成可行走区域的过程叫烘焙

导航网格(NavMesh):一旦确定了所有代理可以站立的位置,这些位置会被连接起来形成一个覆盖在场景几何体之上的表面

路径查找机制

  1. 映射起始点和终点到最近的多边形:

首先,需要确定角色(代理)的起点和目的地,并将这两个点映射到NavMesh上的最近多边形。这是因为路径规划是在由NavMesh定义的可行走表面上进行的。

  1. 从起始位置开始搜索:

一旦确定了起始和目标多边形,接下来就是搜索过程。这个过程通常是从起始位置开始,逐步访问其所有邻近的多边形。

搜索的目标是找到一条通往目标多边形的路径。在这个过程中,算法会记录经过的所有多边形,以便之后能够回溯并确定完整的路径。

  1. 使用A*算法进行路径搜索:

Unity采用A*(A星)算法来执行这种搜索。A*是一种启发式搜索算法,广泛用于路径查找问题中因为它能够有效地平衡完整搜索空间的探索与直接朝向目标方向前进的需求。

A*算法通过评估每个可能的下一步(基于到达该点的成本加上预计到达目标的成本),选择最有可能快速且高效地到达目标的路径前进。

  1. 追踪访问过的多边形以确定路径:

在搜索过程中,A*算法不仅找到了通往目标的路径,还会返回一系列需要穿越的多边形。这些多边形共同构成了从起点到终点的最终路径。

路径走廊

假设我们有一个游戏场景,其中有三个NPC(非玩家角色):张三、李四和王五。他们都想从场景的一侧走到另一侧的不同目标点。

  • 初始设置:首先,每个NPC都使用A*算法计算出各自的路径,并开始沿着这些路径移动。

  • 冲突情况:当张三接近李四时,为了避免与李四发生碰撞,张三的局部避障系统(比如RVO)检测到了潜在的碰撞,并决定稍微减速并向左偏移一点。

  • 路径调整:尽管张三已经采取措施避免与李四相撞,但她还是稍微偏离了自己的原定路径。此时,张三并不立即重新计算整个路径,而是继续尝试沿着原来的走廊前进,只是做了一些微调以避免碰撞。

  • 重新规划路径:如果张三发现自己偏离得太远,以至于无法简单地通过微调返回到原始路径上,她可能会触发一次新的路径搜索,找到一条绕过李四的新路径继续前往目的地。

移动代理的过程

假设我们有一个游戏场景,其中有一个名为张三的NPC需要从场景的一侧走到另一侧。

  • 初始设置:张三使用A*算法找到了从起点到终点的最佳路径,并开始沿着这条路径移动。

  • 动态调整:当接近另一个NPC 李四时,为了避免碰撞,张三的局部避障系统根据RVO算法调整了她的速度和方向,稍微减速并向左偏移。

  • 速度计算与应用:经过调整后,计算出张三的新速度。如果游戏使用的是基于物理的角色控制器,这个速度会被直接应用于角色,使其以平滑的方式改变方向和速度。如果使用的是传统的动画方法,则该速度信息可能被用来驱动角色的行走动画,确保动画与实际移动相匹配。

  • 位置更新与约束:一旦张三按照新的速度移动了一步,她的新位置就会被更新。然后,系统会检查并确保张三的位置仍然位于NavMesh上。如果由于某种原因张三稍微偏离了NavMesh(例如因为碰撞检测误差),系统会自动将她拉回到最近的有效位置,确保她始终保持在可行走区域。

  1. 转向与避障后的最终速度计算

    • 在完成转向(steering)和障碍物避免(obstacle avoidance)后,会计算出代理的最终速度。

    • Unity使用一个简单的动态模型来模拟代理的行为,这个模型考虑了加速度的影响,使得移动看起来更加自然和平滑。

  2. 速度输入动画系统或由导航系统处理

    • 计算得到的速度可以被传递给动画系统,通过所谓的“根运动”(root motion)来驱动角色的移动。这种方法允许更精确地控制角色的动作,尤其是在需要高质量动画的情况下。

    • 或者,可以让导航系统自动处理角色的位置更新,这通常适用于不需要复杂动画控制的情况。

  3. 位置更新并约束至NavMesh

    • 无论采用哪种方式移动代理,最后一步都是将代理的实际位置更新,并确保其位于NavMesh上。这是至关重要的,因为它保证了代理始终在可行走区域内移动,从而实现了稳定的导航体验。

全局导航与局部导航

假设在一个城市模拟游戏中,有二个NPC:Alice、Bob。他们都位于城市的中心广场,并且各自的目标是不同的建筑物。

  • 全局导航阶段

    • Alice想要去北边的一个图书馆。她首先使用A*算法计算出从当前位置到图书馆的最佳路径。这条路径由一系列连续的多边形组成,形成了一个“走廊”。

  • 局部导航阶段

    • 在前往图书馆的路上,Alice遇到了正在横穿广场的Bob。为了避免与Bob相撞,Alice的局部导航系统启动了避障机制(比如RVO算法),暂时改变了她的行进路线,稍微绕过Bob继续前进。

    • 当Alice接近下一个拐角时,她再次检查是否有障碍物或其他NPC阻挡了道路。如果没有,则按照原定计划转弯;如果有,则进一步调整路线以避开障碍。

  • 持续调整

    • 在整个过程中,Alice不断重复上述步骤,每次只关注下一步的动作,即如何高效地朝向路径上的下一个点移动,同时避免碰撞。这种实时调整使得Alice即使在复杂多变的环境中也能顺利到达目的地。

NavMesh Agent

NavMesh Agent 是一个游戏对象,它被表示为一个竖直的圆柱体。这个圆柱体的大小由半径(Radius)和高度(Height)属性决定。

圆柱体的形状用于检测与其他代理和障碍物的碰撞,并做出相应的响应。

可以理解为碰撞体,只是这个是用来检测障碍物的。

创建一个NavMesh(与老版不同)

  1. 选择需要添加NavMesh的场景几何体你想让角色能够行走的场景。(地面,平台等静态几何体)

  2. 添加NavMesh Surface组件

  3. 配置NavMesh Surface组件

  4. 烘焙NavMesh

创建一个NavMesh Agent

Agent Type:代理类型

Speed:移动速度

Angular Speed:旋转速度

Acceleration:最大加速度

Stopping Distance:离目标点多少距离停止

Auto BraKing:开启后,随着离目标点越来越近,自身的移动速度会变慢

Area Mask:寻路是考虑的区域。(决定哪些区域可以行走,哪些区域不能行走)

代码相关

 //自动寻路设置目标点
//agent.SetDestination()

//停止寻路
//agent.isStopped = true;


//1.面板参数相关 速度 加速度 旋转速度等等
 print(agent.speed);
 print(agent.acceleration);
 print(agent.angularSpeed);
 //2.其它重要属性
 //2-1当前是否有路径
 if( agent.hasPath )
 {

 }
 //2-2代理目标点 可以设置 也可以得到
 print(agent.destination);

 //2-3是否停止 可以得到也可以设置
 print(agent.isStopped);

 //2-4当前路径
 print(agent.path);

 //2-5路径是否在计算中
 if( agent.pathPending )
 {

 }
 //2-6路径状态
 print(agent.pathStatus);

 //2-7是否更新位置
 agent.updatePosition = true;

 //2-8是否更新角度
 agent.updateRotation = true;

 //2-9代理速度
 print(agent.velocity);


//手动寻路
//计算生成路径
NavMeshPath path = new NavMeshPath();
if( agent.CalculatePath(Vector3.zero, path) )
{

}
//设置新路径
if(agent.SetPath(path))
{

}
//清除路径
agent.ResetPath();

//调整到指定点位置
agent.Warp(Vector3.zero);

练习1:鼠标点哪儿,人物移动就会去哪儿

using UnityEngine;
using UnityEngine.AI;
using UnityEngine.InputSystem;

public class 导航寻路 : MonoBehaviour
{
    NavMeshAgent nav;

    Ray ray;
    RaycastHit hit;

    private void Start()
    {
        nav = GetComponent<NavMeshAgent>();
    }

    private void Update()
    {
        if(Mouse.current.leftButton.wasPressedThisFrame)
        {
            Debug.Log("mosue click");
            ray = Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue());
            if (Physics.Raycast(ray, out hit, 100))
            {
                // 如果设置了isStopped为true,就不会动了。要想再次动起来,需要改为false
                nav.isStopped = false;
                nav.SetDestination(hit.point);
            }
        }

        if(Keyboard.current.spaceKey.wasPressedThisFrame)
        {
            nav.isStopped = true;
        }
    }
}

练习2:在上一个练习的基础上,结合动画制作

需要准备2个动画,闲置动画和移动动画通过Speed来控制

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.InputSystem;

public class 导航结合动画 : MonoBehaviour
{
    private NavMeshAgent agent;
    private RaycastHit hit;
    private Animator animator;

    private void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        animator = GetComponent<Animator>();
    }

    private void Update()
    {
        if(Mouse.current.leftButton.wasPressedThisFrame)
        {
            if(Physics.Raycast(Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue()), out hit, 100))
            {
                agent.isStopped = false;
                agent.SetDestination(hit.point);
            }
        }

        if (Keyboard.current.spaceKey.wasPressedThisFrame)
        {
            agent.isStopped = true;
        }

        if(agent.velocity == Vector3.zero)
        {
            animator.SetInteger("Speed", 0);
        } else
        {
            animator.SetInteger("Speed", 1);
        }
    }

}

导航网格生成

导航网格窗口参数

我们需要安装AI Navigation组件。然后在顶部菜单>Window>AI>Navigation中打开网格面板。

Object场景设置相关页签(好像被淘汰了)

Scene Filter:场景过滤器

  • All:场景中所有的对象

  • Mesh Renderers:显示场景中挂载Mesh Renderers组件的对象

  • Terrains:显示挂载了Terrains组件的对象

Navigation Static:打开选中物体的静态导航开关。

Generate OffMeshLinks:打开选中物体的网格连接点开关。

Bake导航数据烘焙页签

Agent Radius:离边缘可行走区域的距离。值越大,在物体边缘行走的面积越少。

Agent Height:是否可以穿过拱桥下方是由高度决定的。值越小,越容易穿过。

Max SLope:斜坡多少度可以走上去。

Step Height:台阶多少高度才能够行走上去

Drop Height:跳跃的高度

Jump Distance:网格与网格之间的跳跃距离(两个网格之间必须要设置连接点)

导航网格外连接组件

在没有烘焙情况下,可以达到从有一个点跳跃到另外一个点的效果,就是需要用到外连接组件

Off Mesh Link组件参数

Start:开始点

End:结束点

Cost Override:寻路消耗值。当起始点和结束点有多条路线时,物体会选择消耗最小的那条路径。

Bidirectional:勾选后,可以从结束点跳跃到开始点,也可以从开始跳跃到结束点

Activated:是否启用连接点。取消勾选后,就不会计算这条路径。

Auto Update Position:勾选后,当跳跃点更新后,相对的路径也会更新。

Navigation Area:决定路径所属区域

如何使用?

  1. 准备两个空对象,作为起点和重点

  2. 为其中一个对象添加Off Mesh Link组件

导航动态障碍组件

组件参数详解

Shape:动态胶囊的形状

Carve:是否开启雕刻功能(通常用于不动的物体),勾选后,它会给物体挖一个孔,并且生成对应的网格信息,被认为这片区域是无法前往。

练习题3

在上一题的基础上,在场景中加入一个阻碍玩家前进的动态障碍物,玩家摧毁它,可以前往下一个区域。

给障碍物添加NavMeshObstacle组件即可,摧毁,鼠标右键进行隐藏障碍物。

using UnityEngine;
using UnityEngine.AI;
using UnityEngine.InputSystem;

public class 导航结合动画 : MonoBehaviour
{
    private NavMeshAgent agent;
    private RaycastHit hit;
    private Animator animator;

    private void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        animator = GetComponent<Animator>();
    }

    private void Update()
    {
        if(Mouse.current.leftButton.wasPressedThisFrame)
        {
            if(Physics.Raycast(Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue()), out hit, 100))
            {
                agent.isStopped = false;
                agent.SetDestination(hit.point);
            }
        }

        if(Mouse.current.rightButton.wasPressedThisFrame)
        {
            if(Physics.Raycast(Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue()), out hit, 100, 
                1 << LayerMask.NameToLayer("Wall")))
            {
                hit.collider.gameObject.SetActive(false);
            }
        }

        if (Keyboard.current.spaceKey.wasPressedThisFrame)
        {
            agent.isStopped = true;
        }

        if(agent.velocity == Vector3.zero)
        {
            animator.SetInteger("Speed", 0);
        } else
        {
            animator.SetInteger("Speed", 1);
        }
    }

}