美文网首页Unity-机器学习
遗传算法案例一:基于感官的移动

遗传算法案例一:基于感官的移动

作者: veinsvx | 来源:发表于2018-06-07 21:15 被阅读0次

    首先,看一下我们最终训练出来的结果是这样的(这个gif图片我截得非常不满意,后面有一张比较完整的训练动图,大家可以先拉到后方看一下,这篇文章可是非常长的。。。)


    遗传算法结果.gif

    第一步:在场景里新建一个Cube(黑色)和一个Plane(蓝色),并改变改变他们的缩放和位置,实现如下图所示的场景;然后将Cube命名为Dead,Plane命名为Ground。


    基本场景搭建.png
    第二步:新建一个胶囊体和一个立方体,将立方体设为胶囊体的子物体,然后将胶囊体命名为People,立方体命名为Eye,并且调整他们的位置和旋转角度成如下图所示:(请务必保持Cube的轴向如图所示(60,0,0),因为到时我们会通过Cube的蓝色轴方向发射射线,来检测地面)
    物体的轴向.png
    物体的层级关系.png

    第三步:
    1.选中People,给他添加Rigibody组件,并冻结刚体xyz轴的旋转。
    2.修改People的层,将layer改为bot(需要自己先创建)。


    物体需要更改的属性参数.png

    第四步:
    1.新建一个空物体,并拖拽到蓝色地面的正上方,以后他将会作为我们People的出生点。
    2.将People变成预制体,并删掉场景里的People。

    第五步:新建3个脚本,分别命名为MyDNA,MyBrain,MyPopulationManager

    MyDNA脚本编写以下代码

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class MyDNA
    {
        //这个列表里存储的数值,是为了影响物体的行动
        List<int> genes = new List<int>();
        int dnaLength = 0;
        int maxValues = 0;
            
        /// <summary>
        /// 自己编写构造函数来实现这个类实例化时,进行一些初始的赋值
        /// </summary>
        /// <param name="l">基因列表的长度</param>
        /// <param name="v">基因列表中影响物体行动的随机整数的最大值</param>
        public MyDNA (int l,int v)
        {
            dnaLength = l;
            maxValues = v;
            SetRandom();
        }
            
        /// <summary>
        /// 对基因列表进行随机数赋值(因为我们的目标是实现优胜劣汰,所以要有随机数产生各种
        /// 各样的“性状”,来进行训练)
        /// </summary>
        private void SetRandom()
        {
            genes.Clear();
            for (int i = 0; i < dnaLength; i++)
            {
                genes.Add(Random.Range(0, maxValues));
            }
        }
            
            
        /// <summary>
        /// 这个函数会将传进来的父亲和母亲的DNA序列进行重组(各取一半,取d1的基因列表
        /// 前半部分,取d2的基因列表后半部分)
        /// </summary>
        /// <param name="d1">父亲的DNA</param>
        /// <param name="d2">母亲的DNA</param>
        public void Combine(MyDNA d1,MyDNA d2)
        {
            for (int i = 0; i < dnaLength; i++)
            {
                if (i < dnaLength / 2.0)
                {
                    int c = d1.genes[i];
                    genes[i] = c;
                }
                else
                {
                    int c = d2.genes[i];
                    genes[i] = c;
                }
            }
        }
        
        /// <summary>
        /// 对基因列表的某个随机的元素进行赋一个随机的"行为"值(因为列表里的元素的值
        /// 是可以影响物体的运动行为)
        /// </summary>
        public void Mutate()
        {
            genes[Random.Range(0, dnaLength)] = Random.Range(0, maxValues);
        }
        
        /// <summary>
        /// 返回基因列表里的某个元素的值
        /// </summary>
        /// <param name="pos">元素的下标位置</param>
        /// <returns></returns>
        public int GetGene(int pos)
        {
                return genes[pos];
        }
    }
    

    MyBrain脚本编写如下代码

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class MyBrain : MonoBehaviour
    {
        int DNALength = 2;
        //用来记录存活时间,
        public float timeAlive;
        public float timeWalking;
        public MyDNA dna;
        public GameObject eyes;
        bool alive = true;
        bool seeGround = true;
            
        
        /// <summary>
        /// 这个是unity的回调函数,当两个碰撞体/刚体相撞时,unity会自动调用此函数
        /// </summary>
        /// <param name="collision">碰撞到的物体</param>
        private void OnCollisionEnter(Collision collision)
        {
            if (collision.gameObject.name == "Dead")
            {
                alive = false;
                timeWalking = 0;
                timeAlive = 0;
            }
        }
    
        /// <summary>
        /// Brain进行初始化赋值的函数,每个挂在这个脚本的物体,都会进行初始化赋值
        /// </summary>
        public void Init()
        {
            //列表长度为2,每个元素存在3种动作状态,实例化后将随机赋予一种
            //实例化后,每个数字的意义:0 向前走 1左转 2右转
            //dna.genes[0]看到地面,dna.genes[1]看不到地面
            dna = new MyDNA(DNALength, 3);
            timeAlive = 0;
            alive = true;
        }
        
        void Update()
        {
            if (!alive) return;
            Debug.DrawRay(eyes.transform.position, eyes.transform.forward * 10, Color.red, 10);
            seeGround = false;
            RaycastHit hit;
            if (Physics.Raycast(eyes.transform.position, eyes.transform.forward * 10, out hit))
            {
                if (hit.collider.gameObject.name == "Ground")
                {
                    seeGround = true;
                }
            }
            //不能让他们一直无限制的运行下去,即使是最优秀的基因,他们最多存活时间也不可能超过极限。
            timeAlive = MyPopulationManager.elapsed;
    
            float turn = 0;
            float move = 0;
            if (seeGround)
            {
                if (dna.GetGene(0) == 0) { move = 1; timeWalking += 1; }
                else if (dna.GetGene(0) == 1) turn = -90;
                else if (dna.GetGene(0) == 2) turn = 90;
            }
            else
            {
                if (dna.GetGene(1) == 0) { move = 1; timeWalking += 1; }
                else if (dna.GetGene(1) == 1) turn = -90;
                else if (dna.GetGene(1) == 2) turn = 90;
            }
    
            this.transform.Translate(0, 0, move * 0.1f);
            this.transform.Rotate(0, turn, 0);
        }
    }
    

    MyPopulationManager编写如下代码

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using System.Linq;
        
    public class MyPopulationManager : MonoBehaviour
    {
        public GameObject botPrefab;
        public int populationSize = 50;
        //用来储存实例化出来的"人"
        List<GameObject> population = new List<GameObject>();
        //可以看做是人类寿命。
        public static float elapsed = 0;
        public float trailTime = 5;
        int generation = 1;
    
        GUIStyle guiStyle = new GUIStyle();      
        /// <summary>
        /// 通过UI的方式向屏幕输出一些信息,方便查看当前进化状态
        /// </summary>
        private void OnGUI()
        {
            guiStyle.fontSize = 25;
            guiStyle.normal.textColor = Color.white;
            GUI.BeginGroup(new Rect(10, 10, 250, 150));
            GUI.Box(new Rect(0, 0, 140, 140), "Stats", guiStyle);
            GUI.Label(new Rect(10, 25, 200, 30), "Gen:" + generation, guiStyle);
            GUI.Label(new Rect(10, 50, 200, 30), string.Format("Time:{0:0.00}", elapsed), guiStyle);
            GUI.Label(new Rect(10, 75, 200, 30), "Population:" + population.Count, guiStyle);
            GUI.EndGroup();
        }
        
    
        void Start()
        {  
            for (int i = 0; i < populationSize; i++)
            {
                Vector3 startingPos = new Vector3(this.transform.position.x + Random.Range(-2, 2),
                this.transform.position.y,
                this.transform.position.z + Random.Range(-2, 2));
                GameObject b = Instantiate(botPrefab, startingPos, this.transform.rotation);
                b.GetComponent<MyBrain>().Init();
                population.Add(b);
            }
        }
        
        /// <summary>
        /// 这个函数的核心就是调用了MyDNA脚本里的Combine函数,实现父母基因的重组功能。
        /// 此外还有百分之一的概率会调用MyDNA脚本里的Mutate函数,实现自然界中的基因突变功能。
        /// </summary>
        /// <param name="parent1">父亲</param>
        /// <param name="parent2">母亲</param>
        /// <returns></returns>
        GameObject Breed(GameObject parent1, GameObject parent2)
        {
            Vector3 startingPos = new Vector3(this.transform.position.x + Random.Range(-2, 2),this.transform.position.y,this.transform.position.z + Random.Range(-2, 2));
            GameObject offspring = Instantiate(botPrefab, startingPos, this.transform.rotation);
            MyBrain b = offspring.GetComponent<MyBrain>();
            //模仿自然界中的变异,有百分之一的概率,在DNA序列中的随机位置产生随机的数字。
            //否则的话,会将父亲的基因和母亲的基因进行分离重组。
            //在MyDNA脚本里有注释解释了Combine函数的作用。
            if (Random.Range(0, 100) == 1)
            {
                b.Init();
                b.dna.Mutate();
            }
            else
            {
                b.Init();
                b.dna.Combine(parent1.GetComponent<MyBrain>().dna,parent2.GetComponent<MyBrain>().dna);
            }
            return offspring;
        }
        
        /// <summary>
        /// 这个脚本的核心
        /// </summary>
        void BreedNewPopulation()
        {
            //这条语句的作用就是实现:根据物体的存活时间和行走时间的长短,来对元素重新排序(升序)
            //因为我们认为行走比存活更重要一些,所以对行走进行加权。
            //查询表达式 
            //List<GameObject> sortedList = (from o in population
            //                               orderby o.GetComponent<MyBrain>().timeAlive+
            //                                       o.GetComponent<MyBrain>().timeWalking*5
            //                               select o).ToList();
                
            //用Lambda表达式
            List<GameObject> sortedList = population.OrderBy(o => (o.GetComponent<MyBrain>().timeWalking * 5 +o.GetComponent<MyBrain>().timeAlive)).ToList();
        
            //把上一代的物体清除掉,用来储存新的一代的物体。
            population.Clear();
            for (int i = (int)(sortedList.Count / 2.0f) - 1; i < sortedList.Count - 1; i++)
            {
                //假设有两个基因良好的物体1,2。
                //首先让1当父亲,2当母亲来组成一个新的基因
                //然后让2当父亲,1当母亲来组成一个新的基因
                //目的1.是加快产生优良基因的速度2.是为了能让这些存活下来排名较高的基因能填满人口列表
                population.Add(Breed(sortedList[i], sortedList[i + 1]));
                population.Add(Breed(sortedList[i + 1], sortedList[i]));
            }
            //销毁掉存放在列表的排序好的所有的上一代物体。
            for (int i = 0; i < sortedList.Count; i++)
            {
                Destroy(sortedList[i]);
            }
            generation++;
        }
        
        
        void Update()
        {
            elapsed += Time.deltaTime;
            //当场景里的物体存活时间超过极限时间,强制结束这一代,重新开始产生新人
            if (elapsed >= trailTime)
            {
                BreedNewPopulation();
                elapsed = 0;
            }
        }
    }
    

    第六步:将MyBrain脚本添加到People预制体上,并将Eye拖到空开的字段上。


    场景环境配置1.png

    第七步:将MyPopulationManager脚本添加到PopulationManager物体上并将People预制体拖动到空开的字段上。


    场景环境配置2.png

    第八步:点击菜单栏Edit—>ProjectSettings—>Physics,关闭bot层碰撞。(因为People上面有刚体,我们的People相互碰撞,会影响我们的一些数据,所以我们要关闭掉它)


    场景环境配置3.png

    第九步:运行,等待结果。


    遗传算法训练过程和结果.gif

    第十步:因为篇幅所限,我没有教大家如何调试及优化,直接把项目最终形式的代码都写好了。其实做项目不要想着一上来就做的完美,要先完成一个小功能,在一点点的进行调试完善。

    最后也希望大家水平越来越高。

    相关文章

      网友评论

        本文标题:遗传算法案例一:基于感官的移动

        本文链接:https://www.haomeiwen.com/subject/miqssftx.html