Unity2D寻路

前言

        我们都知道Unity官方已经在3D中提供了导航组件来负责寻路,而2D中是没有的,如果你使用地图是以格子数计算移动,南无可以使用迷宫算法的策略解决这个地图,这种方式常见于TailMap中。但是如果我们不使用TailMap的方式来或者移动距离不是一格一格的方式,那么我们就需要思考一种解决办法来使得AI可以进行基本的寻路。

        但是我提供的方法只是一种可行方式,这并不是真正意义上的寻路算法。
解决的灵感来源。

解决的灵感来源

        想到这个方法是来源于一个视频中,图片中红点通过一个类似视野的方式来检测绿点的存在,而这种模式就正好类似Unity中的射线检测的原理,于是我就使用了这种方式来判断并检测物体周围的障碍物,来引导物体的移动。

实现过程

        根据上图的信息,我们在检测的出发点上扇形排布一堆射线来进行检测,但是检测的出发点不能是物体自身中心,因为如果障碍在物体一侧时,物体只由最边缘的线条能够检测,因此会导致物体贴这个边缘会非常的近(特别是在绕弯的时候,物体容易和墙角来一波亲密接触)所有这个出发点必须要在物体朝向向后一定距离(根据物体的大小形状而定)最后我们要实现的效果如下图所示:

        其中,我们假定红色物体是障碍,黄色的星星是我们的目标,黑色的线是检测的射线,黄色的射线的物体需要朝向的射线方向,这个标准也会在之后几张图中引用,然后我们要让每根线检测指定距离指定图层(障碍物的图层)并返回碰撞点位置信息,如果没有检测到就返回一个Vector.zero,以下是代码示例。

        使用的所有成员变量

    /// <summary>
    /// 所有控制2D横版的寻路参数
    /// </summary>
    [Header("最大移动速度")] public float Max_Speed;

    [Header("最大角速度")][Range(0, 5)] public float Angle_Bear;
    [Header("角度允许差")][Range(0, 10)] public float Angle_Dif;
    [Header("转向灵敏度")][Range(30, 90)] public float Bear_Senesitvity;
    [Header("检测范围长")] public float Route_Length;
    [Header("检测范围角")][Range(0, 90)] public float Route_Angle;
    [Header("检测间隙角")][Range(0, 90)] public float Route_GapAngle;

    [Header("被视为障碍的图层")] public LayerMask layer;
    [Header("检测频率")][Range(0.02f, 1f)] public float View_Rate;
    [Header("角差灵敏")][Range(0, 3)] public float Angle_Sensitvity = 0;
    [Header("距差灵敏")][Range(1, 3)] public float Distance_Sensitvity = 1;
    [Header("目标朝向")] protected Vector3 Aim_Ratotion = Vector3.zero;
    [Header("目标物体")] public GameObject Aim_GameObject;

    [Header("避障优先")] protected bool Hinder_Frist = false;
    [Header("目标优先")] protected bool AimObject_Frist = false;
    [Header("Rand")] protected int Rand = 0;

        实现检测并返回拿到的信息(保留了绘制射线的过程)

    /// <summary>
    /// 全向障碍射线检测,并将获得的检测结果交给点处理集在异步线程中处理
    /// 处理的结果进行返回,然后影响接下来的运动方向
    /// </summary>
    public void AllRays_Detection()
    {
        var hit = Physics2D.Raycast(transform.position - transform.right * 2, transform.right, Route_Length, layer);
        Vector2 Center = Vector2.zero;
        Debug.DrawRay(transform.position - transform.right * 2, transform.right * 10, Color.white, 0.12f);//保留绘制射线
        if (hit)//中心的检测点
            Center = hit.point;
        else
            Rand = Random.Range(0, 2);//使得中心方向可以每次检测到时随机一个方向而不是固定方向
        PointWork(RayCheck_Direction(), RayCheck_Direction(-1), Center, transform.position, transform.right);
    }
    
    /// <summary>
    /// 获取指定方向上的所有碰撞点位置信息,若为获取到则为Vector.zero
    /// 另外为了防止边缘检测,需要将检测的位置后置
    /// </summary>
    /// <param name="direciton">方向,默认不填或1为向左,-1为右</param>
    /// <returns>该方向的所有点集</returns>
    public List<Vector2> RayCheck_Direction(int direciton = 1)
    {
        List<Vector2> Hit_Points = new();
        Vector3 Max_Angle = Quaternion.Euler(0, 0, Route_Angle * direciton) * transform.right;
        Vector3 Pos = transform.position - transform.right * 2;
        for (float angle = Route_GapAngle; angle <= Route_Angle; angle += Route_GapAngle)
        { 
            RaycastHit2D hit = Physics2D.Raycast(Pos, 
                Vector2.Lerp(transform.right, Max_Angle, angle / Route_Angle), Route_Length, layer);
            if (hit)//保留射线
            {
                Hit_Points.Add(hit.point);//检测到的射线变为红色
                Debug.DrawRay(Pos, Vector3.Lerp(transform.right, Max_Angle, angle / Route_Angle) * 10, Color.red, 0.12f);
            }
            else
            {
                Hit_Points.Add(Vector2.zero);//未检测到的射线默认绿色/蓝色
                Debug.DrawRay(Pos, Vector3.Lerp(transform.right, Max_Angle, angle / Route_Angle) * 10, 
                    (direciton == 1 ? Color.blue : Color.green), 0.12f);
            }
            //Hit_Points.Add(hit ? hit.point : Vector2.zero);//不绘制射线只需要这一句就够了
        }
        return Hit_Points;
    }

        以上代码将检测分为了三个区域,分别是左扇区,右扇区和中心线,将其检测区域分成多个计算是为了更好的应对之后可能得多种容易出现的特殊情况。这是在编辑器中实现的效果图:

        注:蓝线和绿线分别是物体朝向两侧的检测线,白线是中心检测线,这些线检测到物体时变为红色,黄线是引导目标的方向(不是实际朝向),蓝色物体为障碍,红色物体为目标物体。

        当我们使用射线检测拿到所有碰撞点的信息后就需要对这些点的信息做处理,因为计算的过程可能会消耗较多的性能和时间,因此我推荐使用异步线程进行计算避免阻塞主进程。障碍物和物体间的位置取决于障碍物离物体的远近,如果物体离得近,那么驱使物体远离这个物体的这个力也要越大(映射关系就是黄线),障碍物的方向也会影响这个力的大小和偏转,比如如果从中心方向开始,障碍物越靠近检测伞的边缘,那么这个偏转的力也就只需要越小,以此来控制物体的偏转程度,根据以上的信息依据,基本的实现代码如下。

    /// <summary>
    /// 将收集到的所有点集合做处理,根据不同情况做不同的转向处理
    /// 整个点集的处理都位于异步线程中,以便节省消耗
    /// </summary>
    /// <param name="AllLeft">所有左方向集合</param>
    /// <param name="AllRight">所有右方向集合</param>
    /// <param name="Center">中心点集</param>
    /// <param name="Self">自身位置</param>
    /// <param name="Forword">自身朝向</param>
    public async void PointWork(List<Vector2> AllLeft, List<Vector2> AllRight, Vector2 Center, Vector2 Self, Vector3 Forword)
    {
        float Forword_Strength = 0;//控制偏转的轴向值
        await Task.Run(() =>
        {
            int LeftNum = 0, RightNum = 0;
            for (int i = 0; i < AllLeft.Count; i++)
            {
                //按照距离/角度差的综合权重值的方式来决定偏移值的大小
                if (AllLeft[i] != Vector2.zero)
                {
                    LeftNum++;
                    Forword_Strength -= 0.8f * Route_Length * Mathf.Cos(90.0f / AllLeft.Count * (0.5f + (float)i ) * Mathf.Deg2Rad) /
                    (Vector2.Distance(AllLeft[i], Self) * Distance_Sensitvity + i * Angle_Sensitvity + Route_Length * 0.2f);
                }
                if (AllRight[i] != Vector2.zero)
                {
                    RightNum++;
                    Forword_Strength += 0.8f * Route_Length * Mathf.Cos(90.0f / AllLeft.Count * (0.5f + (float)i) * Mathf.Deg2Rad) /
                    (Vector2.Distance(AllRight[i], Self) * Distance_Sensitvity + i * Angle_Sensitvity + Route_Length * 0.2f);
                }
            }
            //计算偏转幅度的大小
            Forword_Strength = Forword_Strength > 0 ? Forword_Strength /
            RightNum : Forword_Strength / (LeftNum == 0 ? 1 : LeftNum);
            
            //结合阻挡值进行的方向角目标的更改,影响FixedUpdate的方向修改
            Aim_Ratotion = (Forword + Quaternion.Euler(0, 0, 
                (Forword_Strength < 0 ? -90 : 90)) * Forword * Mathf.Abs(Forword_Strength)).normalized;
            //检测到障碍,将这个值设置为true
            Hinder_Frist = Forword_Strength != 0;
        });
    }

        然后这是代码实现的效果:

特殊情况

        虽然以上效果看起来能达到我们的需求,但是实际上出现一些特殊情况时往往不能够正确反应,而且这些情况的出现频率还不低。

  • 障碍物体太小或者比较极端,只有中间检测到了,这也是为什么我之前把中间的检测单独拿出来。

  • 障碍物和朝向几乎或完全垂直,计算出来的朝向偏转也很小,容易导致物体看起来完全是笔直冲上去撞墙。

  • 反射出的朝向还是有障碍物,不过这种由于物体一直在运动中,朝向线也会变化,所以往往不怎么需要注意,毕竟最多反应出的现象就是绕不过来撞上去了。

  • 目标在障碍物旁边,需要考虑物体能不能到达目标而不撞上障碍,优势也会变为考虑两个障碍的缝隙物体进不进得去等情况,相对以上几种更为复杂。

        以上几种都是非常容易出现的情况,所以我们要在处理中增加应对方式来解决遇到这种情况时物体该如何调整移动朝向,首先是针对中间检测,只需要补充一下检测的中心线有没有值,顺便检查偏转值为不为0,如果是就给一个偏转值,不过我倾向于随机左右偏转而不是固定一个方向,所以使用Rand = Random.Range(0, 2);,第二种和第三种我采用统一的解决思路,因为这都是计算出来的方向朝向,但是结果还是指向了障碍,不如我们就将这个偏向的力增大一个大的数值,让障碍物按照最快的偏转速度旋转,于是PointWork函数修改为如下:

    public async void PointWork(List<Vector2> AllLeft, List<Vector2> AllRight, Vector2 Center, Vector2 Self, Vector3 Forword)
    {
        float Forword_Strength = 0;//控制偏转的轴向值
        await Task.Run(() =>
        {
            int LeftNum = 0, RightNum = 0;
            for (int i = 0; i < AllLeft.Count; i++)
            {
                //按照距离/角度差的综合权重值的方式来决定偏移值的大小
                if (AllLeft[i] != Vector2.zero)
                {
                    LeftNum++;
                    Forword_Strength -= 0.8f * Route_Length * Mathf.Cos(90.0f / AllLeft.Count * (0.5f + (float)i ) * Mathf.Deg2Rad) /
                    (Vector2.Distance(AllLeft[i], Self) * Distance_Sensitvity + i * Angle_Sensitvity + Route_Length * 0.2f);
                }
                if (AllRight[i] != Vector2.zero)
                {
                    RightNum++;
                    Forword_Strength += 0.8f * Route_Length * Mathf.Cos(90.0f / AllLeft.Count * (0.5f + (float)i) * Mathf.Deg2Rad) /
                    (Vector2.Distance(AllRight[i], Self) * Distance_Sensitvity + i * Angle_Sensitvity + Route_Length * 0.2f);
                }
            }
            //计算偏转幅度的大小
            Forword_Strength = Forword_Strength > 0 ? Forword_Strength /
            RightNum : Forword_Strength / (LeftNum == 0 ? 1 : LeftNum);
            //中心是否被阻挡,有阻挡,如果偏转轴向值为0则随机一个朝向
            if (Center != Vector2.zero && Forword_Strength == 0)
                Forword_Strength = (Rand == 1 ? 1 : -1) * Route_Length /
                (Vector2.Distance(Center, Self) * Distance_Sensitvity + Route_Length);

            //结合阻挡值进行的方向角目标的更改,影响FixedUpdate的方向修改
            Aim_Ratotion = (Forword + Quaternion.Euler(0, 0, 
                (Forword_Strength < 0 ? -90 : 90)) * Forword * Mathf.Abs(Forword_Strength)).normalized;
            //检测到障碍,执行将这个值设置为true
            Hinder_Frist = Forword_Strength != 0;
        });

        //方向朝向有物体阻挡,则把这个朝向力度加强,使得转向力最大
        if (Forword_Strength != 0 && Physics2D.Raycast(Self, Aim_Ratotion, Route_Length, layer).collider != null)
            Aim_Ratotion = (Aim_Ratotion + (Quaternion.Euler(0, 0,
                (Forword_Strength > 0 ? 90 : -90)) * Forword * 1.2f).normalized).normalized;
    }

        而针对目标和障碍的第四种情况,我的处理方式可供参考但是我不认为这是较优解。我的思路是每次进行检测时,也会做对于目标方向的检测,如果已经存在障碍但是同样距离内目标不在范围,那么就进入优先躲避障碍,而如果存在目标,先检测角度偏差(偏差太大可以视为不可达,绕不过来),就检测朝向目标方向上是否有障碍,没有就检测(可以不加)两边是否没有,若没有就可以认为物体的周围空隙够大,可以达到,就进入无视障碍检测的状态(但是依然要检测目标距离/偏差和目标附近障碍等),集中精力追踪目标即可,以下是实现代码:

    /// <summary>
    /// 控制进行向着目标方向的引导,如果目标在避免障碍距离外,则优先避免障碍
    /// 若之内则检测方向和最小两边的范围内是否有障碍,有则也优先避免障碍
    /// 若都不满足则无视障碍直接朝向目标移动
    /// </summary>
    public void Ray_AimObject()
    {
        AimObject_Frist = false;
        if (Aim_GameObject)//有目标
        {
            Vector3 AimPos = Aim_GameObject.transform.position, Pos = transform.position;
            Vector3 Aim_Ratot = AimPos - Pos;
            float Distance = Vector2.Distance(AimPos, Pos);
            if (Hinder_Frist && Distance < Route_Length && !Physics2D.Raycast(Pos, Aim_Ratot, Distance, layer) &&
                !Physics2D.Raycast(Pos, Quaternion.Euler(0, 0, Route_GapAngle) * Aim_Ratot, Distance, layer) &&
                !Physics2D.Raycast(Pos, Quaternion.Euler(0, 0, -Route_GapAngle) * Aim_Ratot, Distance, layer) &&
                Vector2.Angle(transform.right, AimPos) < Route_Angle)
            {//最小两边可以不加也可以新增变量设置其它大小
                AimObject_Frist = true;
                Aim_Ratotion = Aim_Ratot.normalized;
            }
        }
    }

        然后在一开始调用这些方法,按一定时间间隙执行:

    public void Start()
    {
        InvokeRepeating(nameof(To_AimRatotion), View_Rate, View_Rate);
    }
    
    /// <summary>
    /// 进行引导的每隔一定时间调用的方法,根据不同的情况做不同方向引导的判断
    /// 引导依据是当检测到障碍就开始向目标方向做引导,如果满足条件就向目标做方向引导
    /// 否则进行避免障碍的引导优先
    /// </summary>
    public void To_AimRatotion()
    {
        if (!AimObject_Frist)//无目标优先
            AllRays_Detection();
        Ray_AimObject();
    }

        那么,最后实现的效果如下:

缺点分析

尽管这个方法可以克服非TailMap的格子式计算,更灵活的适应不规则障碍,动态障碍,在横版2D中可以做到较好的泛用性,竖版2D也可以以此来设计,但是依然存在巨大的限制空间。

1.只适用于有转向速度的设计,横版的2D通常都是瞬间转向,而通常这只能胜任子弹追踪而非寻路。

2.做出来的效果路径不是最优解,因为此算法主要是针对眼前的障碍物进行的避免,并没有达到真正意义上的寻路,在复杂场景的情况下甚至可能出现反复转圈圈现象。

3.性能和效果不可兼得,如果需要好的检测效果,适应更快速的变化情况,可能需要增加检测扇形的范围和密度,检测频率,但是也会造成相应的性能下降。

  • 17
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是一个简单的Unity 2D寻路算法示例,用于平台跳跃游戏: 1. 创建一个空物体作为寻路管理器,将以下脚本附加到该物体上: ```csharp using UnityEngine; using System.Collections.Generic; public class PathfindingManager : MonoBehaviour { private List<Vector2> path = new List<Vector2>(); public List<Vector2> FindPath(Vector2 start, Vector2 target) { // 使用A星算法来查找路径 // ... return path; } } ``` 2. 创建一个角色并附加以下脚本: ```csharp using UnityEngine; using System.Collections.Generic; public class CharacterController2D : MonoBehaviour { public float speed = 5.0f; public float jumpForce = 500.0f; private Rigidbody2D rigidBody; private bool isGrounded = false; private PathfindingManager pathfindingManager; private List<Vector2> currentPath; private int currentPathIndex = 0; void Start() { rigidBody = GetComponent<Rigidbody2D>(); pathfindingManager = FindObjectOfType<PathfindingManager>(); } void Update() { // 移动 float horizontal = Input.GetAxis("Horizontal"); rigidBody.velocity = new Vector2(horizontal * speed, rigidBody.velocity.y); // 跳跃 if (Input.GetKeyDown(KeyCode.Space) && isGrounded) { rigidBody.AddForce(new Vector2(0, jumpForce)); isGrounded = false; } // 跟随路径 if (currentPath != null && currentPathIndex < currentPath.Count) { Vector2 target = currentPath[currentPathIndex]; float distance = Vector2.Distance(transform.position, target); if (distance < 0.1f) { currentPathIndex++; } else { Vector2 direction = target - (Vector2)transform.position; rigidBody.velocity = new Vector2(direction.normalized.x * speed, rigidBody.velocity.y); } } } void OnCollisionEnter2D(Collision2D other) { // 判断是否着陆 if (other.gameObject.layer == LayerMask.NameToLayer("Ground")) { isGrounded = true; } } void OnTriggerEnter2D(Collider2D other) { // 当角色进入触发器时,重新计算路径 if (other.gameObject.layer == LayerMask.NameToLayer("PathfindingTarget")) { Vector2 target = other.gameObject.transform.position; currentPath = pathfindingManager.FindPath(transform.position, target); currentPathIndex = 0; } } } ``` 3. 创建一个地图,包括地面和路径目标。地面需要标记为“Ground”层,路径目标需要标记为“PathfindingTarget”层。 现在,当角色进入路径目标的触发器时,路径将被重新计算,并且角色将沿着路径移动。注意,这只是一个简单的示例,你需要根据你的游戏逻辑和地图设计进行修改和扩展。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值