请选择 进入手机版 | 继续访问电脑版

查看: 385|回复: 0

[技术] 使用Unity训练AI玩《Flappy Bird》

[复制链接]

1193

主题

1894

帖子

2万

贡献

管理员

Rank: 9Rank: 9Rank: 9

积分
24860
QQ
发表于 2018-11-5 04:17:19 | 显示全部楼层 |阅读模式
《flappy bird》是一款由来自越南的独立游戏开发者Dong Nguyen所开发的作品,这款游戏最火热的时候,吸引了大量玩家沉迷其中。游戏中玩家必须控制一只小鸟,跨越由各种不同长度水管所组成的障碍。

随着人工智能时代的到来,我们可以将这项任务交给人工智能来完成。本文将介绍如何使用Unity ML-Agents机器学习代理工具训练AI玩《Flappy Bird》。

下图为训练后的AI达到的游戏水平。

01.gif


构建《Flappy Bird》游戏
首先,我们需要制作简化版的《Flappy Bird》游戏,只需少量代码便可完成。制作该游戏有很多种方法,本文选择的方法是为了让强化学习的过程尽可能清晰,而不是注重编程的最佳实践。

为了构建游戏,我们使用了FlapPyBird项目中的精灵,使用Unity就可以制作出该游戏。你可以根据本文内容从头构建游戏,或者也可以在本项目的GitHub库获取游戏成品。

下载FlapPyBird项目:
https://github.com/sourabhv/FlapPyBird

下载Flappy Agents项目:
https://github.com/xstreck1/Flappy-Agents

1、场景
首先我们需要创建一个新场景,下图为Unity中的Flappy Agents场景。背景的网格每隔1米有一个分隔线,使用准确的单位对训练过程很重要。

02.png


Main Camera:Main Camera用的是正交摄像机,大小设为2.56。我们将使用9:16的宽高比来模拟手机屏幕。
Unit:整个项目包含在Unit对象中,中心位置为(0,0)。这样做方便之后的并行训练。
Background:背景是个静态图片,位于Background排序图层。背景在整个游戏过程中不会移动。
Bird:Bird是将要训练的代理,位于sprite 图层。
Colliders:该对象包含二个Box Collider,负责控制屏幕的顶部和底部边缘。
Bottom:该对象包含二个底部精灵,用作视觉效果。这些精灵位于Sprite图层,展示在管道前面。
PipeSet:PipeSet对象包含三组Pipes对象。用于查找当前位于小鸟附近并需要通过的障碍。
Pipes:Pipes是一对底部和顶部管道,二个管道上下对称。该对象在游戏期间会调整管道的位置,管道位于Tiles图层。

现在,我们需要一些简单的脚本来让游戏运行。

2、底部
游戏最好在固定位置进行,这样能避免运行更多实例产生的问题。小鸟会待在原有位置,世界会进行移动。为此,我们会将底部部分向左移动,这些部分离开屏幕画面后会转移到右边。
[C#] 纯文本查看 复制代码
// Bottom.cs
using UnityEngine;
public class Bottom : MonoBehaviour
{
    public float tileSize = 3.36f;
    void LateUpdate()
    {
        transform.Translate(Vector3.left * Time.deltaTime);
        if (transform.localPosition.x < -tileSize)
        {
            transform.Translate(Vector3.right * tileSize);
        }
    }
}


请注意:我们使用的是本地位置,这样所有坐标都会相对于Unit 对象的位置。

3、管道
接下来需要移动的对象是管道。管道的行为和底部行为几乎一致,不同之处在于我们需要通过pipeVariancevalue随机设置Y轴位置,如果我们重新启动游戏,我们必须移动Pipes 对象到它们的初始位置。
[C#] 纯文本查看 复制代码
// Pipes.cs
using UnityEngine;public class Pipes : MonoBehaviour
{
   const float spacing = 2f; // 管道间的水平距离
   const int totalPipes = 3;
   private Vector3 startPos;
   public float pipeVariance = .5f;     private void Awake () {
       startPos = transform.localPosition;
       RandomizeY();
   }    private void LateUpdate()
   {
       transform.Translate(Vector3.left * Time.deltaTime);
       if (transform.localPosition.x < -spacing)
       {
           transform.Translate(Vector3.right * 
               spacing * totalPipes);
       }
   }    public void InitialPosition()
   {
       transform.localPosition = startPos;
       RandomizeY();
   }    private void RandomizeY()
   {
       transform.Translate(Vector3.up 
           * Random.Range(-pipeVariance, pipeVariance));
   }
}


现在整个环境都会移动了。在进入游戏过程前,我们需要确保可以在游戏结束时重置整个环境,这部分将通过PipeSet对象实现。

4、PipeSet
在训练阶段,我们会使用还要使用一个函数,用来提供下一个需要通过的管道位置。

由于管道宽度为0.5m,而小鸟宽度为0.1m,我们可以确定当管道距离小鸟左侧(0.5+0.1)/2=0.3m时,它们不会互相碰撞,后续障碍是下一个管道,此时该管道距离小鸟右侧1.7m。这意味着该管道的最左侧坐标是1.7-(0.5/2) = 1.45。

屏幕宽度是2.88m,因此最右边的可见坐标为1.44,因此我们的解决方案能在下一管道进入视图时注意到该管道的位置。
[C#] 纯文本查看 复制代码
// PipeSet.cs
using UnityEngine;public class PipeSet : MonoBehaviour
{
   public void ResetPos()
   {
       foreach (Transform child in transform)
       {
           child.GetComponent<Pipes>().InitialPosition();
       }
   }    public Transform GetNextPipe()
   {
       float leftMost = float.MaxValue;
       Transform leftChild = null;
       foreach (Transform child in transform)
       {
           if (child.localPosition.x < leftMost &&
               child.localPosition.x > -.3f)
           {
               leftChild = child;
               leftMost = child.localPosition.x;
           }
       }
       return leftChild;
   }    
}


5、BirdBasic
现在我们要处理Bird对象。基本上我们只需要检查碰撞,并确定在碰撞后是否重置位置。

鼠标单击左键时,会添加上升动力。我们也会Counter 变量中计算距离。由于场景每秒移动1m,我们只需要计算时间就能测量距离。
[C#] 纯文本查看 复制代码
// BirdBasic.cs
using UnityEngine;public class BirdBasic : MonoBehaviour
{
   private Rigidbody2D myBody;
   private Vector3 startPos;
   private bool dead = false;    public PipeSet pipes;
   public float counter = 0f;    private void Start()
   {
       myBody = GetComponent<Rigidbody2D>();
       startPos = transform.localPosition;
   }    private void Update()
   {
       if (!dead)
       {
           counter += Time.deltaTime;
           if (Input.GetMouseButtonDown(0))
           {
               Push();
           }
       } 
       else
       {
           ResetPos();
       }
   }    private void OnTriggerEnter2D(Collider2D collision2d)
   {
       dead = true;
   }    public void Push()
   {
       myBody.AddForce(Vector2.up, ForceMode2D.Impulse);
   }    public void ResetPos()
   {
       myBody.velocity = Vector3.zero;
       transform.localPosition = startPos;
       dead = false;
       pipes.ResetPos();
       counter = 0;
   }
}


注意事项:
我们会在OnTrigger上检测,因为游戏不需要实际碰撞物理。因此,场景中的所有碰撞体都需要设为触发器。
PipeSet引用需要在编辑器中指定。
我们使用RigidBody2D 实现物理效果,需要将该组件附加到游戏对象上。然后游戏过程会由该刚体控制,重量越小,上升动力越大,重力比例越小,小鸟下落速度越慢。本示例中,我们将重量和重力设为0.3。

现在我们得到了可以运行的《Flappy Bird》游戏,现在我们可以自己玩玩这个游戏,接下来我们将让机器接管游戏。

开发代理
我们将通过使用强化学习,训练小鸟自动飞过障碍。我们需要安装Unity ML-Agents,Python,TensorFlow和TensorFlowSharp。

下面是安装和配置参考:
Mac下配置Unity机器学习代理工具
配置Unity机器学习代理工具和TensorFlow环境(Windows 10)

1、学院脚本
第一步要创建新的学院(Academy)脚本。

在本项目中,我们可以使用预制BasicAcademy组件。BasicAcademy组件组件用于配置训练过程,应将该组件指定到一个位于场景根目录的空白对象上。

指定好组件后,我们将在检视窗口看到多个配置选项,展开Training Configuration部分,并将Time Scale设为10,这样会让训练过程的速度是正常游戏的10倍。

2、大脑组件
学院必须带有接收大脑(Brain)组件信息的子对象。Brain组件会在Unity中控制训练过程和游戏过程。创建代理后,我们会配置大脑。将该游戏对象命名为FlappyBrain,以便之后使用。我们要将Bird对象转换为代理。

代理是ML-Agents训练过程的基本单元,它是个能观察游戏世界、训练和做决策的组件。为此,我们需要使Bird脚本继承自Agent 而不是MonoBehaviour。接下来是新Bird对象的三个重要区别。

3、动作
逻辑不再发生在Update函数中,而是发生在AgentAction函数。
[C#] 纯文本查看 复制代码
private bool screenPressed = false;
public override void AgentAction(
   float[] vectorAction, 
   string textAction)
{
   if (dead)
   {
       SetReward(-1f);
       Done();
   }
   else
   {
       SetReward(0.01f);        int tap = Mathf.FloorToInt(vectorAction[0]);
       if (tap == 0)
       {
           screenPressed = false;
       }
       if (tap == 1 && !screenPressed)
       {
           screenPressed = true;
           Push();
       }
   }
}


这部分是代理行为的核心内容,代理将在此做决策。每个代理步骤都会从神经网络接收一个动作向量,并由代理处理该向量。如果小鸟拍打翅膀的动作,我们会获取 vectorAction[0]的小数部分,如果该值为1,就让小鸟拍打翅膀。

由于鼠标按下事件不会被处理,我们需要强制释放按键。为此,我们使用ScreenPressed字段,它会在没有拍打翅膀动作时重置。

最后是最重要的奖励过程。如果Bird对象与管道碰撞,我们将奖励设为-1。否则我们会在训练的每个步骤设置0.01的奖励。

在强化学习过程中,代理的目标是最大化奖励,即做出赢得更高奖励的行为,而不是得到较低奖励的行为。奖励的距离数值需要由开发者选择,这些值被称为超参数(hyperparameters),选择合适的超参数是强化学习过程的核心要素。

4、重置脚本
当Bird对象发生碰撞时,我们会调用Done() 函数,该函数会重置环境。该调用由AgentReset()函数接收,它会替换ResetPos()函数。
[C#] 纯文本查看 复制代码
public override void AgentReset()
{
    myBody.velocity = Vector3.zero;
    transform.localPosition = startPos;
    dead = false;
    pipes.ResetPos();
    counter = 0f;
}


5、观测值
最后需要描述环境的当前状态,我们会提供下面信息:
Bird对象的Y轴位置
Bird对象的Y轴速度
当前上管道的底部位置
当前下管道的顶部位置
小鸟最后动作是否是拍打翅膀

[C#] 纯文本查看 复制代码
const float height = 2f; //从中心到顶部或底部的距离
const float pipeSpace = .6f; // 管道在Y轴被偏移0.6m

public override void CollectObservations()
{
   AddVectorObs(gameObject.transform.localPosition.y / height);
   AddVectorObs(Mathf.Clamp(myBody.velocity.y, -height, height) 
       / height);
   Vector3 pipePos = pipes.GetNextPipe().localPosition;
   AddVectorObs((pipePos.y - pipeSpace) / height);
   AddVectorObs((pipePos.y + pipeSpace) / height);
   AddVectorObs(screenPressed ? 1f : -1f);
}


我们通过用距离除以高度将所有数值限制在-1到1的范围。该过程称为归一化,这将有助于提升算法的性能。

这便是我们需要的观测值。以下是Bird.cs脚本的完整代码,请将该脚本添加到Bird游戏对象上而不是BirdBasic组件上。
[C#] 纯文本查看 复制代码
// Bird.cs
using MLAgents;
using UnityEngine;public class Bird : Agent
{
   private Rigidbody2D myBody;
   private Vector3 startPos;
   private bool dead = false;    private bool screenPressed = false;
   const float height = 2f;
   const float pipeSpace = .6f;    public PipeSet pipes;
   public float counter = 0f;    private void Update()
   {
       counter += Time.deltaTime;
   }    private void Start()
   {
       myBody = GetComponent<Rigidbody2D>();
       startPos = transform.localPosition;
   }    private void Push()
   {
       myBody.AddForce(Vector2.up, ForceMode2D.Impulse);
   }    public override void CollectObservations()
   {
       AddVectorObs(gameObject.transform.localPosition.y / height);
       AddVectorObs(Mathf.Clamp(myBody.velocity.y, -height, height)
          / height);
       Vector3 pipePos = pipes.GetNextPipe().localPosition;
       AddVectorObs((pipePos.y - pipeSpace) / height);
       AddVectorObs((pipePos.y + pipeSpace) / height);
       AddVectorObs(screenPressed ? 1f : -1f);
   }    public override void AgentAction(
       float[] vectorAction, 
       string textAction)
   {
       if (dead)
       {
           SetReward(-1f);
           Done();
       }
       else
       {
           SetReward(0.01f);            int tap = Mathf.FloorToInt(vectorAction[0]);
           if (tap == 0)
           {
               screenPressed = false;
           }
           if (tap == 1 && !screenPressed)
           {
               screenPressed = true;
               Push();
           }
       }
   }    public override void AgentReset()
   {
       myBody.velocity = Vector3.zero;
       transform.localPosition = startPos;
       dead = false;
       pipes.ResetPos();
       counter = 0f;
   }    private void OnTriggerEnter2D(Collider2D collision2d)
   {
       dead = true;
   }
}


完成训练
开始训练前,我们设置了多个游戏副本,这些副本将并行训练,从而加速训练过程并实现多样性。我们使用15个Unit对象的副本来创建学院。

下图中为15个并行游戏,每个游戏在X轴偏移20m,在Y轴偏移8m。由于小鸟不会在X轴上移动,我们可以使场景视图一直关注整个学院。

03.png


我们现在设置并启动训练过程。首先需要使用Brain对象来描述配置,配置如下:
将Space Size值设为5,对应在CollectObservations()函数中收集的5个观测值。
将Space Type改为Discrete,将Branch Size设为2。对应带有二个选项的flap动作:拍打翅膀或不拍打翅膀。

1、玩家大脑
现在该系统能正常运行。我们可以通过将Brain Type设为玩家(Player)来进行测试。

为了让游戏对鼠标点击做出反应,并创建离散玩家行为,将Key设为Mouse 0,Branch Index设为0,Value设为1。通过结合上文中的代码,该大脑创建了游戏的可玩版本。

04.png


2、外部大脑
训练过程通过使用外部(External)大脑类型(Brain Type)来完成。

首先需要在ML-Agents项目的根文件夹启动命令行。
mlagents-learn config\trainer_config.yaml --train --run-id=Flappy0

如果已经正确安装环境,应该会看到Unity的Logo在几秒内弹出。在Unity中运行项目会开始学习过程,我们可以在终端看到各个奖励的生成和进展。

05.png


与此同时,我们也可以在Unity场景视图中看到所有游戏在同时进行。

3、配置
虽然系统能够很好地学习行为,但适当提高神经网络的复杂度会更好。我们在Trainer_config.yaml文件的结尾插入下面的内容:
FlappyBrain:
    hidden_units: 256
    num_layers: 3

这样可以加倍每个图层的神经元数量,并添加一个图层,从而使系统学会更复杂的功能。我们在配置中用到了大脑游戏对象的名称,即FlappyBrain,使其匹配我们的项目。

我们保存改动,然后再次运行训练。

4、内部大脑
当训练完成时,大脑数据会创建在文件夹中,目录如下:
models/Flappy0-0/editor_FlappyAcademy_Flappy0-0.bytes

该文件包含实际训练的神经网络。我们将该文件复制到Unity项目文件夹,把大脑类型切换为内部(Internal),在Graph Model进行指定,然后运行游戏。

现在,我们将得到自己训练出的AI玩《Flappy Bird》。

5、得分
本项目中最后一项内容是计数器。如果我们想知道AI控制小鸟飞多远,可以添加画布,上面带有Text字段和以下组件:
[C#] 纯文本查看 复制代码
// Counter.cs
using UnityEngine;
using UnityEngine.UI;

public class Counter : MonoBehaviour {
    public Bird bird;
    Text scoreText;

    void Start () {
        scoreText = GetComponent<Text>();
    }
 
    void Update () {
        scoreText.text = Mathf.Floor(bird.counter / 2f).ToString();
    }
}


在编辑器中从第一个单元指定小鸟,显示小鸟飞行的距离。我们也可以使用类似《Flappy Bird》的字体并添加Outline 组件,使游戏画面更像原版游戏。

小结
使用Unity ML-Agents机器学习代理工具训练AI玩《Flappy Bird》就介绍到这里,希望大家能学以致用,在更多的游戏创作中使用到Unity机器学习代理工具。更多Unity技术内容尽在Unity官方中文论坛(UnityChina.cn) !
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表