遗传算法案例一:基于感官的移动
2018-06-07 本文已影响0人
veinsvx
首先,看一下我们最终训练出来的结果是这样的(这个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
第十步:因为篇幅所限,我没有教大家如何调试及优化,直接把项目最终形式的代码都写好了。其实做项目不要想着一上来就做的完美,要先完成一个小功能,在一点点的进行调试完善。
最后也希望大家水平越来越高。