首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

使用Unity训练AI玩《Flappy Bird》

《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

{

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

{

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

().InitialPosition();

}

} public Transform GetNextPipe()

{

float leftMost = float.MaxValue;

Transform leftChild = null;

foreach (Transform child in transform)

{

if (child.localPosition.x

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();

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();

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();

}

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会在评选中有额外的加分。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181113B1LZYF00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券