こんにちは!今回は前からやりたかったレースゲーム作成の第1回目です。
前回の強化学習の勉強でなんとなくUnityでの強化学習の方法は学びました。
今回はこれを活かして2Dのレースゲームを制作する方法について勉強しましたので紹介したいと思います。
ぼくがレースするよ!
準備(コースとキャラクターの作成)
プロジェクトの作成
今回はUnity2Dで制作しました。2DはUnityの新規作成から選択できます。
初期の画面はこんな感じです。
3Dとくらべて平面になっていますね!
2Dを使う場合は2D用のコンポーネントがあり、3Dのときと微妙に使い勝手違うものがあるので注意が必要です。
その他スクリプトなどのこまかい使い方などは公式HPで調べられます。
コースを作る
レースゲームにはコースが必要ですので、まずは適当にコースを作りました。
できたコースはこんな感じ、
高さ20×幅40のロの字の枠を4つの長方形(wall(1), wall(2), wall(3), wall(4))で作成し、真ん中に仕切り(wall)を追加しました。
動かすキャラクターはあしぺん君にお願いしました。
イメージは今いる位置がスタート地点で反時計回りに回る至ってシンプルなコースです。
絵の表示方法
上記の画像ではもう絵が追加されていますが、絵の追加の方法を説明します。
まずはHierarchyのCreateから2DObjectの中のSpriteを選択してオブジェクトを追加します。
すると何もないオブジェクトが作成されます。
また、これとは別に絵を準備します。今回はあしぺんを準備しました。
これは私がペイントで作成していたものです。これをAssetにSpritesフォルダを作成してその中に追加します。
そしてこのSpriteを先に追加していたNew Spriteに追加するだけです。
そうするとオブジェクトに絵が追加されて画面に表示されます。
簡単ですね! 壁も同じようにして水色にしました。
水色はどうなんですかね~
もし絵を追加してもシーンには表示されるけど下のゲーム画面には表示されない!という場合はTransformのPositionのZの位置が-10などになっている可能性が考えられます。PositionのZを0にすれば表示される場合がありますので、試してみてください。私はこれに数十分悩みました。
物理判定の追加
コースに使ったwallにはそれぞれキャラクターがぶつかったときに判定を追加するために「Box Colider 2D」のコンポーネントを追加しました。
これであしぺん君が壁にぶつかったときに弾かれます。
同様にあしぺん君にも「Box Colider 2D」を追加します。またあしぺん君は動きますので、物理演算を行います。そこで「Rigidbody 2D」を追加します。今回は上から見たレースゲームですので、Gravity Scaleは0にしておきます。
キーでキャラクターを操作しよう
これで見た目はできました!
あとはキャラクターを操作する仕組みを作ります。今回はキー入力で動かします。つまり「キーボードの上を押すと画面上方に移動する」ということです。
移動のさせかたは色々あるみたいです。こちらのサイトを参考にしました。
今回はレースゲームですのでより自然な方法で動かしたいです。そこで、キー入力中は加速度を増加させる方法にしました。
作成したスクリプトは以下のとおりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# Unity Script C# using UnityEngine; using System.Collections; public class playermove : MonoBehaviour { private bool isRight = true; // Update is called once per frame void Update() { } void FixedUpdate() { //左右キーの入力 float h = Input.GetAxis("Horizontal"); float y = Input.GetAxis("Vertical"); GetComponent<Rigidbody2D>().AddForce(Vector3.right * h * 10f); GetComponent<Rigidbody2D>().AddForce(Vector3.up * y * 10f); } } |
Input.GetAxisは入力された軸に補正をかけて-1から1までの数値で返すものですので、これで入力の値を取得します。ここで、Horizontal と Verticalは w、a、s、d と矢印キーを表します。
その後入力した値を好みの値に補正してオブジェクトの物理演算に使っています。
AddForceはベクトルの方向に継続的に加速度を加えられるものです。
できたものがこちらです。
おおおおおおおおおおおおおー!
徐々に加速されて、壁にぶつかると跳ね返っていることがわかります。
なんとなくそれっぽいものができました。
AIを作ってみよう
さてここからが本番です。上記のままですとただのソロプレイであまり面白みがありません。
せっかくなのでAIを追加してみたいと思います。
作り方は前回勉強したML-Agentsを使い強化学習で作成することにしました。
基本的な使い方は前回と同じです。
なので、今回は前回のものを流用してコースを一周するAIに挑戦してみました。
Tergetを用意して移動させる
まずは前回と同様にTergetを準備してそこにたどり着くと報酬を与えるといった方法で強化学習をしてみたいと思います。
約200万回学習した結果がこちら
コースの上側にある青い四角が今回のTergetです。
・・・・・?全くですね。
ちなみに下の壁に近づくと強制的にリセットされるようにしているので、スタート地点に戻されます。
くそ~これだと一緒にプレイしても面白くない!
ブロックに分けて報酬を与える
報酬の与え方を見直す必要がありそうです。そこで、下図のようにコースを6つに分割しました。
そしてそれぞれに番号を与えてキャラクターが存在する場所に応じて領域の番号を付与します。
そして図の矢印の方向に領域を移動するときに報酬を与えることにしました。
Tergetは無視することにしました。
また、番号5,0,1にいるときは右側に移動しているときに継続的に報酬を与え、逆に番号2,3,4にいるときには左側に移動しているときに継続的に報酬を与えました。
速度はx軸方向の速度は「rBody.velocity.x」で取得できます。
これでうまく反時計回りに回ってくれるはず!
結果がこちら
あ?あああああああああああああああ”あ”あ”ーー😫😫
もうわけわからん。
ちなみに一定時間毎にマイナス報酬を与えており、ある時間がたつと自動的にリセットさせるようにしています。
ここでBrainに与える要素の存在に気づきました。
対象物までの距離をBrainに与える
これまではキャラクター自身の位置と速度とTergetまでの距離をBrainに与えていました。Tergetの距離はもう使っていないので、別の要素として、対象物までの距離をBrainに与えることにしました。
こちらを参考にしました。
距離を測るためには「RaycastHit2D」という関数を使います。これでキャラクターから特定の方向にレーザーを発射してそれがぶつかったときにぶつかったものの情報を得ることができます。
以下はキャラクターから上下左右にレーザーを発射して距離を取得する部分とそれを可視化するための部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# Unity Script C# void Update() { Vector2 StartPosition = transform.position; RaycastHit2D HitObject1 = Physics2D.Raycast(StartPosition, Vector2.down);#StartPositionはキャラクターの位置, Vector2.downは画面下方向 dis1 = HitObject1.distance; RaycastHit2D HitObject2 = Physics2D.Raycast(StartPosition, Vector2.up);#Vector2.downは画面上方向 dis2 = HitObject2.distance; RaycastHit2D HitObject3 = Physics2D.Raycast(StartPosition, Vector2.right);#Vector2.rightは画面右方向 dis3 = HitObject3.distance; RaycastHit2D HitObject4 = Physics2D.Raycast(StartPosition, Vector2.left);#Vector2.leftは画面左方向 dis4 = HitObject4.distance; Debug.DrawRay(StartPosition, Vector2.down* dis1, Color.green, 0.05f, false);#Vector2.down*dis1#は下方向にdis1の距離だけレーザーを可視化する,Color.greenはレーザーの色,0.05fは表示時間 Debug.DrawRay(StartPosition, Vector2.up * dis2, Color.green, 0.05f, false); Debug.DrawRay(StartPosition, Vector2.right * dis3, Color.green, 0.05f, false); Debug.DrawRay(StartPosition, Vector2.left * dis4, Color.green, 0.05f, false); } |
シーンではこのようになります。
注意するのはキャラクター自身を発射したレーザーの対象物から除外することです。
Brainに与える要素の数が変わりますので、BrainのパラメータのVector ObservationのSpace Sizeを調整します。今回は上下左右の距離4つとキャラクターの位置3つ(x, y, z)及びキャラクターの速度2つ(x, y)の9つです(今思えば位置のzはいらなかったですね)。
学習した結果が以下です。
おおおおおおおおおおおおおー!
一応反時計回りに回っています。
より最適にするほうほうについては現在検討中です。
いざ勝負!
自分で作ったAIと自分自身で勝負してみます。
水色のあしぺん君が私です。
一周目はギリギリ勝ちました!
陸地ではしるのは苦手です・・・。
ただし二周目のはじめのコーナーでAIにぶつかると、AIの軌道が変わってしまって逆走していることがわかります。
このAIはイレギュラーが苦手なのかもしれません。
なにはともあれ、ぼっちの私でもソロプレイを脱却できました😢
作成したTempAgent.csを載せておきます。アドバイスお待ちしております。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# Unity Script C# using System.Collections.Generic; using UnityEngine; using MLAgents; using UnityEngine.UI; // 追加しましょう public class TempAgent : Agent { private float seconds; Rigidbody2D rBody; public float dis1; public float dis2; public float dis3; public float dis4; void Start() { rBody = GetComponent<Rigidbody2D>(); } void Update() { seconds += Time.deltaTime; Vector2 StartPosition = transform.position; RaycastHit2D HitObject1 = Physics2D.Raycast(StartPosition, Vector2.down); dis1 = HitObject1.distance; RaycastHit2D HitObject2 = Physics2D.Raycast(StartPosition, Vector2.up); dis2 = HitObject2.distance; RaycastHit2D HitObject3 = Physics2D.Raycast(StartPosition, Vector2.right); dis3 = HitObject3.distance; RaycastHit2D HitObject4 = Physics2D.Raycast(StartPosition, Vector2.left); dis4 = HitObject4.distance; Debug.DrawRay(StartPosition, Vector2.down* dis1, Color.green, 0.05f, false); Debug.DrawRay(StartPosition, Vector2.up * dis2, Color.green, 0.05f, false); Debug.DrawRay(StartPosition, Vector2.right * dis3, Color.green, 0.05f, false); Debug.DrawRay(StartPosition, Vector2.left * dis4, Color.green, 0.05f, false); } public Transform Target; public override void AgentReset() { // if (this.transform.position.y < 0) // { // If the Agent fell, zero its momentum // this.rBody.angularVelocity = Vector3.zero; this.rBody.velocity = Vector3.zero; this.transform.position = new Vector3(0.19f, -6.78f, 0); numb = 0; Random.value * 8 - 4); } public override void CollectObservations() { AddVectorObs(this.transform.position); AddVectorObs(rBody.velocity.x); AddVectorObs(rBody.velocity.y); AddVectorObs(dis1); AddVectorObs(dis2); AddVectorObs(dis3); AddVectorObs(dis4); } public float numb = 0; public float speed = 10; public override void AgentAction(float[] vectorAction, string textAction) { // Actions, size = 2 Vector3 controlSignal = Vector3.zero; controlSignal.x = vectorAction[0]; controlSignal.y = vectorAction[1]; rBody.AddForce(controlSignal * speed); // Rewards float distanceToTarget = Vector3.Distance(this.transform.position, Target.position); if (this.transform.position.y < 0) { if (rBody.velocity.x > 0) { AddReward(0.001f); } if (this.transform.position.x < 10f && this.transform.position.x > -10f) { if (numb == 5) { AddReward(1.0f); Done(); } numb = 0; // SetReward(+0.00005f); } else if (this.transform.position.x >= 10f) { if (numb == 0) { AddReward(1.0f); } numb = 1; } else { if (numb == 4) { AddReward(1.0f); } numb = 5; } } else { if (rBody.velocity.x < 0) { AddReward(0.001f); } if (this.transform.position.x < 10f && this.transform.position.x > -10f) { if (numb == 2) { AddReward(1.0f); } numb = 3; } else if (this.transform.position.x >= 10f) { if (numb == 1) { AddReward(1.0f); } numb = 2; } else { if (numb == 3) { AddReward(1.0f); } numb = 4; } } AddReward(-0.00005f); if (seconds >= 150f) { Done(); seconds = 0.0f; } } } |
まとめ
今回はレースゲームの第1回目ということで、コースとキャラクターを作成したのと、強化学習でAIを作成しました。学んだ点は以下のとおりです。
- Unity2Dのベクトルの考え方を理解する。
- 強化学習は報酬の与え方が重要
- Raycastで対象物までの距離を得ることが可能
まだまだ、最適な解ではないと想っておりますので、これから先もガンガン進化させていきたいと思いますので、よろしくお願いいたします!!
コメント