Unity人工智能AI编程知识

群组行为:

模拟鸟群行走或人群行走过程的称之为群组行为
分散 、队列 、 聚集
分散:在群体内,个体必须与其他个体之间保持一定的距离,当小于那个距离时,就要分散开来。
实现原理:确定需要分散的范围separationDistance,获取在该范围内的个体,计算出当前个体与(当前个体的周围个体)的相反作用力的合力,即 合力 += (当前个体位置-其他个体位置); 
但是,由于我们是想要当前个体位置与其他个体位置的距离越小,产生的相反作用力越大,所以 相反作用力= (当前个体位置 -其他个体位置).normalized / (当前个体位置-其他个体位置).magnitude ,这样就能够距离越小,产生的相反作用力越大,反之越小。(注意:个人之间的距离不能小于1,不然会发生异常,也就是除数会无穷小!导致分散力无穷大!)
相反作用力的总体意思就是 当前个体位置-其他个体位置,即得到一个 其他个体位置 指向 当前个体位置的 三维向量
这个相加之后的力就是分散合力,至于我上面说的当距离越小,作用力越大是根据实际需求来决定的。
这样,我们得到了一个分散合力


队列:在群体内,个体的朝向必须保持与队列朝向一致
实现原理:
队列朝向的获取:队列指的是 当前个体的周围的个体的平均朝向,那么我们一样根据上面的相同的原理获取到周围个体,然后周围个体的朝向就是transform.forward 将这些周围个体朝向 全部相加起来,再求平均,即得到了队列朝向。
而我们想要的目的是 将当前朝向 逐渐 朝向 队列朝向。那么我们需要加一个力,这个力叫队列趋向力
我们将队列朝向向量-当前个体朝向向量,当前个体朝向向量是transform.forward,这样获取到一个队列趋向力,为什么会要这样算?因为队列朝向向量-当前个体朝向向量是根据物理上的四边形原则,即合力、分力的一个原理来得出的。如果我们只是将队列朝向向量当做队列趋向力的话,这样变化幅度很小,而我们直接计算出一个能够让当前个体受力变为队列朝向的分力,变化幅度就很大。若还是不理解的话,可以看下物理的合力、分力的解释以及四边形原则,在这里 队列朝向就是合力,当前个体原本的受力和队列趋向力都是分力,2个分力合起来就是一个合力,即 当前个体原本的受力(当前朝向向量)+队列趋向力 = 队列朝向向量 ,这样我们也能推导出: 队列趋向力=队列朝向向量-当前朝向向量。
这样,我们就得到了一个队列趋向力

聚集:与分散相反原理。
我们在分散不发生的情况下才产生聚集,一样地获取周围个体,但是我们不是计算出聚集合力什么的,而是计算出周围个体的中心点,中心点计算方法:周围个体的位置之和/周围个体数量
然后,中心点-当前个体位置=当前个体位置 朝向 中心点 的向量,然后这个向量就是我们的聚集力
如果对聚集力的大小有点不满意的话,推荐:将聚集力的大小设置为当前速度的大小


经过以上说明,我们得到了三个力:分散合力、队列趋向力、聚集力,这三个力都可以分别设置一个权重,来调整它们的影响,加权重的意思就是给力乘以一个值
然后,我们将这三个力相加得到了一个 最终合力,我们是用这个最终合力来影响当前物体的
根据牛顿第二定律:F=ma  , 故  a= F/m  (F是最终合力,a是加速度(m/s^2) m是质量(kg) )
所以,在Update里面我们可以通过这个公式计算出 最终合力对物体产生的加速度a
当前速度+=a*Time.deltaTime 这样处理加速度对当前速度的影响,当前速度是一个三维向量
我们通过使用Quaternion.Slerp(transform.rotation,目标四元数, 变化系数)来逐渐旋转当前个体的朝向,面向当前速度的朝向,目标四元数用Quaternion.LookRotation(当前速度)来获取。
移动的话可以用Translate 直接朝向当前个体的前方forward来移动,速度大小为当前速度大小

(2019/05/06更新)在以上基础上,新增2个力:移动恒力(可选)、目标趋向力(可选)。

移动恒力 = 当前速度.normalized * 初始速度大小(用户自定义速度大小)

目标趋向力 = (自身指向目标向量.normalized - 当前朝向(transform.forward)) * 目标趋向速度(用户自定义速度大小)

注意:当前速度是一个三维向量Vector3,当前朝向是transform.forward,请勿以为当前速度就是当前朝向,当前速度会影响当前朝向。
 

FSM有限状态机:

个人理解:FSM就是一个控制状态切换的集中处理器,用一个FSMSystem管理着所有FSMState,每个FSMState(状态)都有它自身对应的转换事件(可有多个)(Transition(枚举))。
FSM根据传递进来的Transition信息,将当前状态转换为其它状态,这个转换成功条件必须是:当前状态中存在该转换事件Transition,并且转换事件对应的状态ID存在于FSM管理器中,这样才会发生转换。
每次转换时,都会触发(当前状态离开事件)和(转换后的当前状态进入事件),每个状态都有进入事件和离开事件。
注意:当前状态必须存在转换条件,并且该转换条件对应的状态必须存在于FSM管理器中!
例如:StartState、RunState、EndState 三个状态存在于FSM管理器
StartState中注册了Transition.GoToRunState转换条件,该转换条件对应的State是RunState,由于我们的状态是通过字典(键值对形式)保存在FSM状态机中的,所以我们还要有一个枚举StateID来标识每种状态,而转换条件对应的就是StateID了,那么上面的RunState的StateID属性为StateID.Run。这样StartState就存在了一个转换条件,这个转换条件也有了对应的StateID,我们把这个放入字典保存起来,key是Transition,value是StateID。
而在游戏一开始默认是StartState,那么我们就应该要有一个初始化状态机的当前状态的方法,将当前状态设置为StartState,ID为StateID.Start, 并且调用StartState的进入事件,我们状态必须要重写进入事件和离开事件,因为使得每种状态的不同就是它们的作用,不然FSM将毫无意义。
在我们给FSM状态机一个转换消息时,例如:Transition.GoToRunState,那么FSM会这样做:
1.从当前状态(StartState)中,寻找该转换条件GoToRunState对应的状态ID即StateID.Run
2.拿到StateID后,我们在FSM中保存的状态字典内 通过StateID寻找出对应的状态,即RunState
3.找到后,先调用 当前状态的离开事件(当前状态是StartState),然后将当前状态转为RunState,ID也要变,然后调用当前状态的进入事件(当前状态是RunState)。

补充:
Act事件,它是每个状态必须重写的事件,会在FSM状态机的Update中调用,FSM状态机只会调用当前状态的Act事件,Act事件相当于状态运行中的各种行为。
Reason事件,它是每个状态必须重写的事件,同样会在FSM状态机的Update调用,与Act的行为区别是它集中处理了各种转换事件的发生,就是说在什么情况下会触发什么转换条件,从而让FSM状态机自己去管理自己的状态转变。
但是,当FSM状态机不继承于Monobeviour时,Act事件和Reason事件也可能并非由状态机自身去调用,而是由创建状态机的那个脚本的Update去调用状态机的Update方法。注意,状态机不继承于MonoBehaviour的时候是这样用的,当状态机继承于Monobehaviour就不需要创建状态机自身了。
至于状态机继不继承Monobehaviour,可以自己看着办,个人认为继承于Monobehaviour不太好。

 

感知系统:

        视觉:

        1、全方位无死角视觉,实现原理:利用Vector3.Distance()判断距离是否在视觉范围内。(不常用)

        2、半圆弧视觉:利用Vector3.Distance()判断距离是否在视觉范围内,并且用Vectoe3.Angle(机器人前方,从机器人指向玩家的向量)得到夹角(<90°),判断这个夹角是否在视觉角度/2内,若在视觉角度/2内,则认为是看到了,否则反之。半圆弧视觉和全方位无死角视觉的区别就在于多了一个Angle判断。这个视觉看不到看人 还会受到障碍物影响,在确认玩家在视觉范围内时,还要发射一条射线判断是否有障碍物挡着,若射线检测到的是玩家,则表明玩家在视野内,若碰到的是障碍物则不会看到玩家,也就不会触发看到玩家后的操作了。

        听觉:

        1、玩家跑动Bool变量,当玩家跑动时,为true,否则为false

        2、听觉最大距离:利用Unity寻路系统Nav,将2个点之间的最短距离计算出,然后判断这个最短距离是否 小于 听觉最大距离,若小于,则去判断玩家跑动Bool变量是否为true,若为true就表示听到了。这2点的意思是 机器人位置和玩家位置,因为我们的听觉是会受到障碍物干扰的,所以在计算2点距离的时候就不能使用简单的Vector3.Distance了,而是使用导航系统Nav的计算方法,或者根据你自己的寻路系统方法(如A*算法)来计算2点的(受障碍物影响的)最短路径。

        根据Nav寻路系统获取两个坐标点的最短路径长度值代码如下:

    

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class NavPath : MonoBehaviour {
    
    private NavMeshAgent agent;
    public Transform end;
    private void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        DrawPath();
    }


    private void DrawPath()
    {
        float smallestLength=0;
        List<Vector3> path = GetPath(end.position);
        Vector3[] pathArray = path.ToArray();
        for(int i=0;i<pathArray.Length-1;i++)
        {            
            GameObject go= GameObject.CreatePrimitive(PrimitiveType.Sphere);
            go.transform.position = pathArray[i];
            smallestLength += Vector3.Distance(pathArray[i], pathArray[i + 1]);
        }
        Debug.Log("路径长度:" + smallestLength);
    }

    private List<Vector3> GetPath(Vector3 endPoint)
    {
        //agent.SetDestination(endPoint);          
        NavMeshPath navMeshPath = new NavMeshPath();
        agent.CalculatePath(endPoint, navMeshPath);
        List<Vector3> path = new List<Vector3>();
        for(int i=0;i<navMeshPath.corners.Length;i++)
        {
            path.Add(navMeshPath.corners[i]);
            Debug.Log(i+1+"号  "+navMeshPath.corners[i]);
        }
        path.Insert(0, transform.position);
        path.Add(endPoint);
        return path;
    }
}


A*寻路:

 

           1、制作虚拟地图(美工管的),虚拟地图一般是用(一个类class或者结构体struct)去描述地图上一个点的信息,例如:

 

一个点需要有 坐标x,y ,是否为障碍物bool标志位,父节点,F,G,H,(基本都要这些)   

            2、讲解A*寻路的一个方程式:F=G+H

            简单地说明一下,F越小越好,代表着寻找出的路径会比较短,而不是最短。其中,G是确定的,H是预判的。

            该结点G的计算方法= 父节点G值 + 父节点到达该结点的距离(直接用Vector3.Distance计算出)

            H值的计算方法可以有多种:例如:第一种是直接 是 该结点与终点的X之差的绝对值与Y之差的绝对值 之和。

                                                                第二种是 直接Vector3.Distance计算出 该结点与终点的距离。

                                                                其他的方法也有 ,自己想到什么是什么,这个会影响到最终寻找出的路径。

            这里,因为H值的计算方法是人为设定的,可能靠谱可能不靠谱,故F值也不一定是最完美的,所以A*算法计算出的路径也不一定是最短的,而只能说是比较短的。

            3、A*算法需要的5个东西,上面已经说了1个,虚拟地图map[,] 我们通过把2D/3D的物体虚拟化为一张地图,这张地图的每个点都是一个信息体,即Point,里面记录着一些信息,初始化虚拟地图的时候 我们只会将x,y值和障碍物bool初始化。

            开启列表、闭合列表

            开启列表存放了还未处理的,但是已经确定父节点的结点。

            闭合列表存放了已经经过处理了的结点。

            当一个结点被处理完了,就会进入闭合结点。

            步骤:1、将起点放入开启列表。

                      2、若开启列表不为空,则从开启列表寻找一个结点,它的F值是最小的,开始处理该结点,我们称之为当前结点

                      3、判断终点是否已经在开启列表中了,若在开启列表中了,则表示找到寻路结束了,跳出循环。

                      3、获取 在 2 找出的拥有最小值F的结点 的 周围结点。(必须是获取能到达的结点,过滤掉越界的,障碍物的)

                      4、遍历3获取到的周围结点,

                     4.1 若发现遍历到的结点 已存在于开启列表中,也就是已经有(旧)父节点,计算出该结点新的G值,计算方法:当前结点的G值+当前结点到该结点的距离=新的G值, 用该新的G值对比 原本的G值,若新的G值更小,那么更新该结点的父节点为当前结点,G值为新的G值(更小),F=G+H(变小了)。 若新的G值比原本的要大,或者等于,就不作处理了。

                     4.2 若发现遍历到的结点 不存在于开启列表,我们就要计算该结点的信息,父节点=当前结点,G=当前结点的G值+当前结点到该结点的距离,H根据自己的公式计算,F=G+H,并且加入开启列表中。

                    5、回到2 继续执行

    下面是我啰嗦的话,不明白上面一些事情的话请看看。

    解释一下为什么终点存在于开列表中就代表寻路结束了,假设我们遍历到了终点之前的一个结点,那么这个结点肯定是在开启列表中F值最小的那个,对吧。还记得我们是怎么找到下一个要处理的结点的吗?是在开启列表中找出F值最小的那个结点!所以理所当然地 终点之前的一个结点是最好的,那么在该结点下 直接遍历周围结点,肯定会找到终点的,那么这样终点就会进入开启列表,终点的父节点和F、G、H值已经计算出来了,并且在开启列表中绝对是最小的,那么理所当然下一个处理的结点就是终点了,但是终点我们是不需要处理的,我们不需要继续处理了,所以循环结束,寻路结束。

                     这样,我们可以通过parent来获得一条路径,就是从终点不断地找parent,直至空为止。

                

上面,说了那么多,我说一句,寻路系统一般不用我们纯手写的,这样会不太方便,我们开发中一般会直接使用

1、Unity自带的Navigation导航系统

2、使用插件使用A* Pathfinding Project

https://www.assetstore.unity3d.com/cn/#!/content/87744

行为树:

        Unity BehaviorDesigner行为树插件学习
        行为树插件的学习

 

Unity⼈⼯智能AI编程知识 群组⾏为: 群组⾏为: 模拟鸟群⾏⾛或⼈群⾏⾛过程的称之为群组⾏为 分散 、队列 、 聚集 分散:在群体内,个体必须与其他个体之间保持⼀定的距离,当⼩于那个距离时,就要分散开来。 实现原理:确定需要分散的范围separationDistance,获取在该范围内的个体,计算出当前个体与(当前个体的周围个体)的相反作⽤⼒的 合⼒,即 合⼒ += (当前个体位置-其他个体位置); 但是,由于我们是想要当前个体位置与其他个体位置的距离越⼩,产⽣的相反作⽤⼒越⼤,所以 相反作⽤⼒= (当前个体位置 -其他个体位 置).normalized / (当前个体位置-其他个体位置).magnitude ,这样就能够距离越⼩,产⽣的相反作⽤⼒越⼤,反之越⼩。(注意:个⼈ 之间的距离不能⼩于1,不然会发⽣异常,也就是除数会⽆穷⼩!导致分散⼒⽆穷⼤!) 相反作⽤⼒的总体意思就是 当前个体位置-其他个体位置,即得到⼀个 其他个体位置 指向 当前个体位置的 三维向量 这个相加之后的⼒就是分散合⼒,⾄于我上⾯说的当距离越⼩,作⽤⼒越⼤是根据实际需求来决定的。 这样,我们得到了⼀个分散合⼒ 队列:在群体内,个体的朝向必须保持与队列朝向⼀致 实现原理: 队列朝向的获取:队列指的是 当前个体的周围的个体的平均朝向,那么我们⼀样根据上⾯的相同的原理获取到周围个体,然后周围个体的 朝向就是transform.forward 将这些周围个体朝向 全部相加起来,再求平均,即得到了队列朝向。 ⽽我们想要的⽬的是 将当前朝向 逐渐 朝向 队列朝向。那么我们需要加⼀个⼒,这个⼒叫队列趋向⼒ 我们将队列朝向向量-当前个体朝向向量,当前个体朝向向量是transform.forward,这样获取到⼀个队列趋向⼒,为什么会要这样算?因为 队列朝向向量-当前个体朝向向量是根据物理上的四边形原则,即合⼒、分⼒的⼀个原理来得出的。如果我们只是将队列朝向向量当做队列 趋向⼒的话,这样变化幅度很⼩,⽽我们直接计算出⼀个能够让当前个体受⼒变为队列朝向的分⼒,变化幅度就很⼤。若还是不理解的话, 可以看下物理的合⼒、分⼒的解释以及四边形原则,在这⾥ 队列朝向就是合⼒,当前个体原本的受⼒和队列趋向⼒都是分⼒,2个分⼒合起 来就是⼀个合⼒,即 当前个体原本的受⼒(当前朝向向量)+队列趋向⼒ = 队列朝向向量 ,这样我们也能推导出: 队列趋向⼒=队列朝向向 量-当前朝向向量。 这样,我们就得到了⼀个队列趋向⼒ 聚集:与分散相反原理。 我们在分散不发⽣的情况下才产⽣聚集,⼀样地获取周围个体,但是我们不是计算出聚集合⼒什么的,⽽是计算出周围个体的中⼼点,中⼼ 点计算⽅法:周围个体的位置之和/周围个体数量 然后,中⼼点-当前个体位置=当前个体位置 朝向 中⼼点 的向量,然后这个向量就是我们的聚集⼒ 如果对聚集⼒的⼤⼩有点不满意的话,推荐:将聚集⼒的⼤⼩设置为当前速度的⼤⼩ 经过以上说明,我们得到了三个⼒:分散合⼒、队列趋向⼒、聚集⼒,这三个⼒都可以分别设置⼀个权重,来调整它们的影响,加权重的意 思就是给⼒乘以⼀个值 然后,我们将这三个⼒相加得到了⼀个 最终合⼒,我们是⽤这个最终合⼒来影响当前物体的 根据⽜顿第⼆定律:F=ma , 故 a= F/m (F是最终合⼒,a是加速度(m/s^2) m是质量(kg) ) 所以,在Update⾥⾯我们可以通过这个公式计算出 最终合⼒对物体产⽣的加速度a 当前速度+=a*Time.deltaTime 这样处理加速度对当前速度的影响,当前速度是⼀个三维向量 我们通过使⽤Quaternion.Slerp(transform.rotation,⽬标四元数, 变化系数)来逐渐旋转当前个体的朝向,⾯向当前速度的朝向,⽬标四元数 ⽤Quaternion.LookRotation(当前速度)来获取。 移动的话可以⽤Translate 直接朝向当前个体的前⽅forward来移动,速度⼤⼩为当前速度⼤⼩ (2019/05/06更新)在以上基础上,新增2个⼒:移动恒⼒(可选)、⽬标趋向⼒(可选)。 移动恒⼒ = 当前速度.normalized * 初始速度⼤⼩(⽤户⾃定义速度⼤⼩) ⽬标趋向⼒ = (⾃⾝指向⽬标向量.normalized - 当前朝向(transform.forward)) * ⽬标趋向速度(⽤户⾃定义速度⼤⼩) 注意:当前速度是⼀个三维向量Vector3,当前朝向是transform.forward,请勿以为当前速度就是当前朝向,当前速度会影响当前朝向。 FSM有限状态机: 有限状态机: 个⼈理解:FSM就是⼀个控制状态切换的集中处理器,⽤⼀个FSMSystem管理着所有FSMState,每个FSMState(状态)都有它⾃⾝对应 的转换事件(
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值