查看原文
其他

使用Unity训练AI玩《Flappy Bird》

Unity Unity官方平台 2019-05-07

《Flappy Bird》是一款由来自越南的独立游戏开发者Dong Nguyen所开发的作品,这款游戏最火热的时候,吸引了大量玩家沉迷其中。游戏中玩家必须控制一只小鸟,跨越由各种不同长度管道所组成的障碍。

 

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

 

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


构建《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米有一个分隔线,使用准确的单位对训练过程很重要。

 

 

  • 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

底部

游戏最好在固定位置进行,这样能避免运行更多实例产生的问题。小鸟会待在原有位置,世界会进行移动。为此,我们会将底部部分向左移动,这些部分离开屏幕画面后会转移到右边。

// 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对象到它们的初始位置。

// 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,因此我们的解决方案能在下一管道进入视图时注意到该管道的位置。

// 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,我们只需要计算时间就能测量距离。

// 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。


下面是安装和配置参考:


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函数。

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()函数。

public override void AgentReset()

{

    myBody.velocity = Vector3.zero;

    transform.localPosition = startPos;

    dead = false;

    pipes.ResetPos();

    counter = 0f;

}


5

观测值

 最后需要描述环境的当前状态,我们会提供下面信息:

  • Bird对象的Y轴位置

  • Bird对象的Y轴速度

  • 当前上管道的底部位置

  • 当前下管道的顶部位置

  • 小鸟最后动作是否是拍打翅膀

 

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组件上。

// 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轴上移动,我们可以使场景视图一直关注整个学院。


 

我们现在设置并启动训练过程。首先需要使用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。通过结合上文中的代码,该大脑创建了游戏的可玩版本。



2

外部大脑

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


首先需要在ML-Agents项目的根文件夹启动命令行。

mlagents-learn config\trainer_config.yaml --train --run-id=Flappy0

 

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

 

 

与此同时,我们也可以在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字段和以下组件:

// 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) !


小提示:Unity全球学生开发挑战赛目前正在举行中,如果在创作的参赛项目中有使用到Unity机器学习代理工具ML-Agents会在评选中有额外的加分。


推荐阅读

官方活动

双十一提前到 | Unity超值订阅,更有限时活动送惊喜 

现在访问Unity在线商店(store.unity.com)参加双十一狂欢,享受订阅优惠的同时,可获赠Unite China 2019技术门票以及限量Unity礼品![了解详情...]


Unity全球学生开发挑战赛

Unity面向全球的学生推出-Unity全球学生开发挑战赛,寻找全世界最具创意,展现自我的学生开发者团队。[了解详情...

活动地址:https://connect.unity.com/challenges/gsc2018


点击“阅读原文”访问Unity官方中文论坛

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存