unityの2D UFO tutorialメモ

unityの公式チュートリアルである2D UFO tutorialをやったのでメモをまとめておきます。自分が戸惑ったところについて出来るだけ詳しく説明しているためかなり冗長です。コードも分かりやすさ優先で全部貼ってますのでめちゃめちゃ冗長です。

元のチュートリアルはこちら
https://unity3d.com/jp/learn/tutorials/s/2d-ufo-tutorial

## プロジェクトを作る

unityを起動するとプロジェクトを新規に開始するか、以前のプロジェクトから再開するかを選ぶメニューが出ます。今回は新規プロジェクトですのでNewから2D Templateを選択し、プロジェクト名を適当に付けたらCreate Projectです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/08cc37ea-a603-276b-5409-1a55b343300c.png)

## unityの基礎知識

unityでは場面毎にsceneをつくり、その中にゲーム進行上必要なすべてを入れておきます。今回のように場面切り替えのないゲームではsceneは1つだけです(つまりこのチュートリアルでは場面切り替えのやり方は分かりません)。

sceneはカメラとオブジェクトから成ります。多分はじめてunityを触る人はカメラの扱いに戸惑うと思います。その点、2Dゲームはカメラの機能が限られているので入りやすいかも知れません。sceneの中身はすべて画像左上のHierarchyに表示されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/6bb26f32-39c4-ca4e-c3c6-724ae85cc666.png)

カメラおよびオブジェクトの挙動は画像右上のInspectorから操作するか、C#またはJavaのプログラムに記載するかします。大枠をInspectorで決めて、細かくはプログラムを書くイメージです。このチュートリアルでも何度かプログラムを書きます。

プログラムはそれぞれのオブジェクトに貼り付ける事になります。バラバラのプログラムの連携はコード内に別のオブジェクトを定義しておいて、Inspectorから相手オブジェクトを指定することで可能です(今回のチュートリアルでも3回ほどやります)。また、時間や衝突などのイベントは共有されているのでそれらを使って実行順などをあまり気にせずに書くことができます(めちゃめちゃ楽です)。

画像左下にあるAssetは過去に自分で作った素材やAsset Storeで提供されている素材の置き場です。中央のSceneとGameはどちらもゲーム画面を表示するパネルですが、Game実際にカメラに写る画面を表示するのに対し、Sceneは作業用のパネルになっていて拡大縮小などができます。

どのパネルも場所を移動できるようになっていますのでやりやすいように移動して使いましょう。

## チュートリアルに使うAssetをゲットする

WindowからAsset StoreをクリックしてAsset Storeを開きます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/b6620e34-89f7-bfd0-b5c2-0279d9f594fe.png)
2D UFOで検索するとほしいAssetがすぐ見つかりますのでImportボタンをクリックします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/9038d045-af34-14f3-7785-37a8e9f5b057.png)
「上書きするかも」と言われますが気にしなくて良いと思います
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/c2bc3372-2610-22f3-7a97-5b4f28cfa19f.png)
全部Importするんじゃなく一部だけImportすることも出来ます。今回は全部ImportしたいのでAllをクリックしてからImportボタンを押します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/e28ad57e-789b-d638-8ca0-ebd0e101a171.png)
Importが完了するとAssetパネルにダウンロードしてきたAssetが表示されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/c7431830-db60-2c29-185a-73328c0f34cd.png)
できました

## 背景を配置する
AssetのSpritesからBackgroundをdragし、Hierarchyにdropすると、Gameパネル、Sceneパネルともタイル地の背景画像が表示されます。Spritesは画像のAssetのことで、オブジェクトとしての情報を画像ファイルに追加したものです。自前の画像ファイルを使うときもSpriteに必ず貼って使います(貼らないと使えません)。Sceneパネルはマウスのスクロールホイールで拡大縮小ができますので適当な大きさになるように調節しておきましょう。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/64c0bcaa-cd71-acc4-1c63-661f9ca35153.png)

## UFOを配置して表示順を変更
同様にAssetのSpritesからUFOをdrag & dropしてもUFOが表示されません。これは表示順が背景の方が上になってしまっているからで、隠れてUFOが見えなくなっています。これではゲームになりませんので表示順を変更します。

表示順はInspectorのSorting Layerから選択できます。下にいくほど後から描画されますので一番下は常に表示されます。UFOをPlayerに、BackgroundをBackgroundに変更すると無事UFOが表示されました。UFOのオブジェクト名をLayer名と対応させる為にPlayerに変更しておきます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/fc6cf7a4-0bc5-4afe-e203-2b9bce7c1302.png)
また、UFOがタイル目よりちょっと大きいようなので、Scaleを(0.75, 0.75, 0)に変更してタイルと合うようにしておきます。
## カメラの縮尺を調整する

Main CameraをハイライトするとInspectorからカメラの設定を変更することが出来ます。カメラ自体は2Dと3Dで共通のようですが、ProjectionをOrthographicに設定すると奥行きが無視されるようになるので2D表示になる、という事のようです。

まず、カメラの表示範囲を示すInspectorのSizeを16.5に変更します。Gameパネルで背景全体がちょうど画面に収まるようになりました。続いてオブジェクトのない部分の背景色をBackgroudをクリックして変更します。チュートリアルではRGB = (32,32,32)の暗い灰色に指定しています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/0bc30876-346e-688a-2c68-3655e86df770.png)

## UFOの動きを設定する

unityは物理演算用のテンプレートをたくさん用意しています。このチュートリアルではその中でも一番基本的なRigid Body 2Dを使ってUFOの動きを設定していきます。rigid bodyは剛体ですから、柔らかく曲がったり凹んだりしない、変形のない物体という事でしょうね。

PlayerをハイライトしてInspectorの一番下にあるAdd Componentをクリック、2D PhysicsからRigid Body 2Dを選択して追加します。追加できたらunityの画面上側にある右向きの黒い三角形をクリックしてゲームをテストプレイしてみましょう。

UFOが一直線に下の方に落っこちていったと思います。これはデフォルトで重力が下向き掛かる設定になってるからですが、真上から見ているのに重力で横に動いて行ったら変ですので、InspectorのGravity Scaleを0に変更しておきます。テストプレイすると動かなくなっている筈です。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/1e4a0c32-4800-0bd7-f258-eaa630c75b00.png)

続いて、カーソル操作でUFOが動くようにしていきます。ここからはいよいよC# Scriptを書いていきます。PlayerをハイライトしてInspectorの一番下にあるAdd Componentをクリック、New Scriptを選択して新しいScriptを追加します。Script名はPlayerControllerとしておきます。Scriptは自動的にAssetパネルの末尾に追加されますが、後で分からなくならないようフォルダにまとめておきます。

AssetパネルにあるPlayerControllerをダブルクリックするとC#を開けるEDITERが開きます。windows環境だとunityインストール時にVisual Studioをインストールしているんじゃないかと思います。開いてすぐは以下のような感じになっている筈です。

```
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
```
Start()はゲーム開始時に1度だけ実行される関数ですので初期化に使います。Update()はゲーム動作の最小単位である1フレームに1度実行されます。ちなみに1フレームが何秒なのかは負荷次第で変わっちゃうようです。書式はC#ですが、オブジェクトの多くがunity独自のものになりますので、慣れるまではunityのdocumentとにらめっこしながらの作業になるのではないでしょうか。
https://docs.unity3d.com/ja/2018.1/Manual/class-Rigidbody2D.html

とりあえず動かしてみます。剛体がどう動くかはRigidbody2Dがやってくれますので、カーソルの入力をInput.GetAxis()で取得し、2次元ベクトルとして定義したmovementに格納してrb2dに渡します。AddForceは加速度運動の外力を入力としているのでUFOがふわふわと慣性運動してくれる筈です。

```
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    private Rigidbody2D rb2d;

    void Start()
    {
        rb2d = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        float moveHorizontal = Input.GetAxis("Horizontal");
        float moveVertical = Input.GetAxis("Vertical");
        Vector2 movement = new Vector2(moveHorizontal, moveVertical);
        rb2d.AddForce(movement);
    }
}
```
上手く動いたでしょうか。動かない場合、コードが間違っていてcompileが通っていない可能性があります。compileエラーの場合、ScriptのInspectorにその旨が表示されます。

また、これでは動きが遅すぎてイマイチです。ですので速度をちょっと上げてみます。ここではpublic float speed;を定義しておいてrb2d.AddForce()に放り込むのをmovement * speedに変更します。speedを100にしたら100倍早く動く筈です。

```
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float speed;
    private Rigidbody2D rb2d;

    void Start()
    {
        rb2d = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        float moveHorizontal = Input.GetAxis("Horizontal");
        float moveVertical = Input.GetAxis("Vertical");
        Vector2 movement = new Vector2(moveHorizontal, moveVertical);
        rb2d.AddForce(movement * speed);
    }
}
```
ところであれれ、speedの値を入力しないまま使ってますね。普通のC#コードだと実行時にエラーになる筈です。でも大丈夫。unityはScriptのpublic変数をInspectorから指定できるようになってるので、そっちで指定するつもりでこういう書きかたをしているようです。100にして速すぎるのを見てひと笑いした後、10ぐらいにしておきます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/2685ccb8-541e-7166-07c2-203844bedb13.png)

## 壁を設定する

さて、UFOはそれっぽく動くようになりました。ですが壁がないとどこまでも飛んでいってしまってゲーム的にイマイチですから、色の違う背景のところでぶつかって止まるようにしましょう。

衝突のためのPhysics EngineとしてCollider Componentが用意されています。衝突判定を付けたいオブジェクトの双方(今回はUFOと壁)にColliderを付けて大きさを指定しておくとぶつかったら止まるようになります(簡単！)。

Colliderは形がいくつかありますが、UFOは丸いのでCircle Colliderを使います。PlayerのInspectorからAdd ComponentでPhysics 2DのCircle Collider 2Dを追加しましょう。sceneパネルに緑色の線で衝突判定の範囲が表示されてますが、例によってUFOのサイズとはズレてますのでRadiusを2.15に変更してUFOの外径とぴったり合わせます。

続いて四角い壁をBox Collider 2Dで作ります。BackgroundをハイライトしてInspectorからAdd ComponentでPhysics 2DのBox Collider 2Dを追加します。Sizeを(3.3, 31.64)、Offsetを(14.3, 0)とするとちょうど右側の壁が作れます。作ったBox ColliderのInspector右上にある歯車マークをクリックしてCopy Componentを選択してから、Paste Component as Newを3回繰り返して合計4つのColliderを作り
Size(3.3, 31.64), Offset(-14.3, 0)
Size(31.64, 3.3), Offset(0, 14.3)
Size(31.64, 3.3), Offset(0, -14.3)
の3つを設定すると4方の壁が完成です。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/8f8f6150-305a-6525-ef36-c98c9dc92773.png)
ゴロゴロ～

## カメラがUFOを追うようにする

一番簡単な方法はMain CameraをPlayerの子オブジェクトにすることだそうです。確かに簡単なんですが、今回はPlayerが回転するのでカメラが一緒に回ってしまってイマイチです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/a08083f1-62ed-559e-0cff-790c6a3e0bd3.png)
なので今回はコードを書きます。Main CameraのInspectorのAdd ComponentからNew Scriptを追加、エディターで開きます。public GameObject player;というのはUFOのことです。後で紐付ける操作をすると別のオブジェクトの情報を拾えるようになります。ここではUFOの位置とカメラの位置(つまり自分の位置)の差を初期値として、UFOが動いた分カメラを移動するようにしています。Start()内で初期値を指定しているのはInspectorで調整した位置を初期とできるようにして、いちいちコードを直さなくて良いようにしているんだと思います。

```
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour
{
    public GameObject player;
    private Vector3 offset;

    void Start()
    {
        offset = transform.position - player.transform.position;
    }

    void LateUpdate()
    {
        transform.position = player.transform.position + offset;
    }
}
```
最後に、Main Cameraをハイライトした状態でHierarchyのPlayerをdragし、InspectorのScriptにあるPlayerのところにdropして紐づけを行います。これをやらないとScriptがpublic GameObject playerが誰のことなのか分からなくてエラーになっちゃうので忘れずにやっておきます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/e2455de4-7dcd-38e8-f40b-93d23c6d8f8a.png)
実行するとカメラがUFOに追従するようになってる筈です。

## 金塊のGameObjectを作る

ProjectのSpritesからPickupをdragして、Hierarchyにドロップします。HierarchyのPickupをハイライトしてInspectorのSprite RendererのSorting LayerをPickupに変更して背景より手前、UFOより奥に見えるようにします。通常だとUFOと重なっていて見えないと思いますのでPositionを適当に変更して見える場所まで動かします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/bef27afe-f90b-e7a4-c3ac-606f3932ac46.png)

## 金塊を回転させる

金塊画像を貼り付けたgameObjectであるPickupを回転させてゲーム画面に動きを出します。Pickupをハイライトして、InspectorのAdd ComponentからScriptを追加して以下のように記入します。updateが掛かる度に回転のz軸を45×Time.deltaTime回すという書き方です。

```
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Rotator : MonoBehaviour
{
    void Update()
    {
        transform.Rotate(new Vector3(0, 0, 45) * Time.deltaTime);
    }
}
```
これでゲームを実行すると金塊がグルグルまわるようになります。

## 作った金塊をprefabに登録してから複製する

Hierarchyに作ってあるPickupをdrag & dropでProjectビューに持っていくとprefabになります。この時点で作ったprefabのPickupとHierarchyのPickupは紐付いているので、この後でHierarchyのPickupを複製したらすべてprefabのPickupの変更が反映されるようになります(便利)。

あとはHierarchyのPickupをハイライトした状態にして、ctrl-dで複製し、位置を移動して金塊を配置していきます。たくさんあるのでフォルダを追加して入れておくと良いでしょう。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/0c3977b9-24d2-e588-7297-51a4d0d3bfac.png)

## 金塊を回収する

UFOが金塊に接触したら金塊を回収するようにします。衝突の検出にはCollider 2Dを使うようです。衝突を検出したいオブジェクトの両方にColliderが必要ですが、PlayerはすでにPhysics、Collider 2Dを貼り付け済みですので、今回はPickupに付けるだけです。

PrefabのPickupをダブルクリックしてInspectorを開き、Add ComponentでPhysics Circle Collider 2Dを追加します。金塊はUFOより小さいので、Radiusは0.94にします。

これで衝突が検出されるようになったので、続いて衝突したら金塊が消えるようにします。Playerに貼ってあるPlayerControllerスクリプトにOnTriggerEnter2D()イベントを利用して衝突対象のタグがPickupであるときに相手のgameObjectを消す一節を追記します。

```
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float speed;
    private Rigidbody2D rb2d;

    void Start()
    {
        rb2d = GetComponent<Rigidbody2D> ();
    }

    void FixedUpdate()
    {
        float moveHorizontal = Input.GetAxis("Horizontal");
        float moveVertical = Input.GetAxis("Vertical");
        Vector2 movement = new Vector2(moveHorizontal, moveVertical);
        rb2d.AddForce(movement * speed);
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.gameObject.CompareTag("PickUp"))
        {
            other.gameObject.SetActive(false);
        }
    }
}
```
これでいけるかと思ったらまだのようで、ゲームをプレイすると金塊にぶつかると壁にぶつかったようにUFOが止まってしまいます。これはColliderが衝突したものとして扱ってしまう為であるようです。PickupのCircle Collider 2DのIs Triggerのチェックを入れる(trueにする)と衝突フラグがトリガーとして使われるようになって、ようやく期待通り金塊が消えてくれるようになります。

## 動かない金塊をStaticにする

unityはColliderのあるオブジェクトにrigidbodyが付いてない場合はdynamic rigidbodyとして取り扱ってしまうそうです。dynamicだと毎フレーム物理演算をやってしまう為、動かない金塊をdynamic扱いしていると無駄な負荷が掛かります。そこで金塊のPrefabにRigidbody 2DをAdd Componentして、gravityをゼロにしてから、Rigidbody 2DのBody TypeをKinematicに変更しておきます。動作は変わりませんが処理が軽くなった筈。

## 金塊をゲットした数を表示する

まず、PlayerControllerに金塊をいくつゲットしたか取得する変数を作ります。private int count;を定義しておいて、void Start()でゲーム開始時にcount = 0;して、金塊に接触して消すときについでにcount ++;するように変更します。

```
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float speed;
    private Rigidbody2D rb2d;
    private int count;

    void Start()
    {
        rb2d = GetComponent<Rigidbody2D> ();
        count = 0;
    }

    void FixedUpdate()
    {
        float moveHorizontal = Input.GetAxis("Horizontal");
        float moveVertical = Input.GetAxis("Vertical");
        Vector2 movement = new Vector2(moveHorizontal, moveVertical);
        rb2d.AddForce(movement * speed);
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.gameObject.CompareTag("PickUp"))
        {
            other.gameObject.SetActive(false);
            count++;
        }
    }
}
```
続いて表示するためのTextを作ります。HierarchyのCreateボタンからUI、Textと選択していくとTextオブジェクトが追加されます。
<img width="1209" alt="無題.png" src="https://qiita-image-store.s3.amazonaws.com/0/277233/a2ef9b49-6291-9c2b-e4f8-7f4b35997c99.png">

追加されたらまず文字色を黄色に変更します。InspectorのText内にあるColorをクリックし、表示されたカラーウィンドウのRGBにそれぞれ255, 255, 0を入力すると文字色が黄色に変更されます。

続いて、位置を画面左上に文字の位置を移動します。Anchor Presetの左上をctrl + altを押しながらクリックすると一度に左上に移動させることができます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/3def44d7-b839-bb76-34b5-db36ddd0382a.png)

このTextオブジェクトにPlayerControllerで取得したcountを表示するようにします。using UnityEngine.UI;でUI用のライブラリを読み込み、public Text countText;を定義してTextオブジェクトを受け取れるように宣言し、countが更新される度にcountTextに金塊の数を文字列で書き込むように追記します。

```
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class PlayerController : MonoBehaviour
{
    public float speed;
    public Text CountText;

    private Rigidbody2D rb2d;
    private int count;

    void Start()
    {
        rb2d = GetComponent<Rigidbody2D>();
        count = 0;
        SetCountText();
    }

    void FixedUpdate()
    {
        float moveHorizontal = Input.GetAxis("Horizontal");
        float moveVertical = Input.GetAxis("Vertical");
        Vector2 movement = new Vector2(moveHorizontal, moveVertical);
        rb2d.AddForce(movement * speed);
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.gameObject.CompareTag("PickUp"))
        {
            other.gameObject.SetActive(false);
            count++;
            SetCountText();
        }
    }

    void SetCountText()
    {
        CountText.text = "Count: " + count.ToString();
    }
}
```
最後にHierarchyのPlayerをハイライトした状態でInspectorのPlayer Controller (Script)に表示されているCount Textの右の枠にHierarchyのCount Textをdrag & dropすると、先程public Text CountText;で宣言したCountTextがいまdrag & dropしたCount Textであるという事になります。

これで金塊の数が表示されるようになりました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/9c3c73b3-30f5-9e79-250a-af0eb50917e2.png)

## 勝利メッセージを表示する

先程と同様にUI、Textを作成して、WinTextとrenameします。PlayerControllerにcountが金塊の数と同じになったらWinTextを「You Win!」と表示するように変更します。以下のコードでは金塊10個として書いています。

```
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class PlayerController : MonoBehaviour
{
    public float speed;
    public Text CountText;
    public Text winText;

    private Rigidbody2D rb2d;
    private int count;

    void Start()
    {
        rb2d = GetComponent<Rigidbody2D>();
        count = 0;
        SetCountText();
        winText.text = "";
    }

    void FixedUpdate()
    {
        float moveHorizontal = Input.GetAxis("Horizontal");
        float moveVertical = Input.GetAxis("Vertical");
        Vector2 movement = new Vector2(moveHorizontal, moveVertical);
        rb2d.AddForce(movement * speed);
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.gameObject.CompareTag("PickUp"))
        {
            other.gameObject.SetActive(false);
            count++;
            SetCountText();
        }
    }

    void SetCountText()
    {
        CountText.text = "Count: " + count.ToString();
        if (count >= 10)
        {
            winText.text = "You Win!";
        }
    }
}
```
先程と同様にHierarchyのPlayerをハイライトした状態でInspectorのPlayer Controller (Script)に表示されているWin Textの右の枠にHierarchyのCount Textをdrag & dropすると、先程public Text winText;で宣言したwinTextがいまdrag & dropしたWin Textであるという事になります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/221c337b-e283-82a4-b349-081e2bdf2525.png)

## Windowsアプリをビルドする

FileからBuild Settingsを開き、PC, Mac & Linux Standaloneを選択してから、Add Open Scenesをクリックして現在のsceneを取り込みます。Buildボタンをクリックしてフォルダを指定するとBuild結果がフォルダ内に出力されて終了です。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/40788278-2ef5-e5cd-d812-90e7f0260e17.png)
フォルダに生成されたWindows用exeファイルを実行してみます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/9440b72d-4992-87fb-8a31-4881143a27eb.png)

ウィンドウサイズを選択してPlayボタンを押すとゲームがはじまります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/77620b6d-0e94-6ca1-7e31-244e4da76251.png)
やったね
![image.png](https://qiita-image-store.s3.amazonaws.com/0/277233/bd1c9c7c-c01f-bb81-7879-3adab5f50d3c.png)

レッツエンジョイunity！！