アイテムを設置しよう のバックアップソース(No.5)

#contents
*はじめに [#v3a58097]
ステージにアイテム・ギミック・ゴールを設置していきます。
今回は
 ・拾うと移動スピードが変化するりんご
 ・エレベーター
 ・ゴール
の3つを実装します。
#br
*りんごの実装 [#m089a5bb]
まずアイテム用の画像を Assets に追加します。
#br
#download(apple_red.png)
#download(apple_blue.png)
#br
追加できたら Assets > それぞれの画像を選択し、inspector から Pixels Per Unit を 32 にそれぞれ変更に変更しておきます。
#br
実装したいリンゴの仕様
 ・ステージの各所に同じ機能のりんごを複数設置する
 ・赤いリンゴを拾うと一定時間速度が上がる
 ・青いリンゴを拾うと一定時間速度が落ちる
 ・速度変化の効果は重ね掛けできる
 ・それぞれの色の速度変化と継続時間は inspector から調整できるようにする
#br
「拾う」という動作は、ひつじに当たったときに効果を発動してりんごを消す
という処理で表現しようと思います。
**プレハブについて [#m7a4dc60]
調べてみるとUnityで同じものを複製して使用するときはプレハブ(Prefab)が便利なようです。
プレハブはゲームオブジェクトから生成するので、まずはりんごのゲームオブジェクトを完成させます。
Assets から apple_red.png を Scene にドラッグ・ドロップし、ゲームオブジェクトを作ります。
#br
**コンポーネントの追加 [#tc80cf62]
作ったゲームオブジェクトにコンポーネントを追加します。
必要になりそうなコンポーネントは
・当たり判定用のCollider
です。
りんごの画像がほぼ円に近いので、今回は Circle Collider 2D でよさそうです。
inspector > Add Component から追加します。
#br
追加できたらinspectorからコンポーネントの設定を少し変更します。
まず、りんごにひつじが反発するなどの物理演算は不要なので、isTrigger にチェックを入れて物理演算を無効にします。
次に、Radius の数値を調整し、当たり判定の大きさをりんごの画像に合わせます。0.4 くらいがちょうどよさそうです。
#br
**スクリプトを追加 [#v952b5b2]
Assets > Add Component > New Script から新しいスクリプトをりんごに追加します。
スクリプト名は Apple としておきます。
毎フレーム行いたい処理はないので Update メソッドは削除して大丈夫です。
まずは必要な変数を作っておきます。
速度の変化率 を float型で rate
継続時間を float型で duration としておきます。
#geshi(csharp,number){{
using System.Collections;
using UnityEngine;

public class Apple : MonoBehaviour
{
    public float duration;
    public float rate;
}
}}
rate と duration は public にしておけば inspector から編集できます。
テスト用に rate は 1.5, duration は 3.0 にしておきます。
#br
***速度変化の実装 [#wff4b8b3]
次に速度変化を作ります。
当たり判定のイベントを受け取り、当たったオブジェクトがひつじだった場合にひつじの速度を変化させる処理で表現できそうです。
調べてみると isTrriger が入った Collider の当たり判定を受け取るイベントには OnTriggerEnter, OnTriggerStay, OnTriggerExit などがありましたが、今回は当たった時に一度だけ呼ばれればよいので OnTriggerEnter を選択しました。
#geshi(csharp,number){{
using System.Collections;
using UnityEngine;

public class Apple : MonoBehaviour
{
    // 略

    void OnTriggerEnter2D(Collider2D col)
    {
        if (col.name == "hitsuji_dot") {
            MoveSheep moveSheep = col.GetComponent<MoveSheep>();
            moveSheep.speed *= rate;
        }
    }
}
}}
#br
OnTriggerEnterの引数 Collider2D 型の中にはnameという名前で当たったオブジェクトの名前が入っているので、それを使って当たったオブジェクトがひつじかどうかを判定しています。
当たったオブジェクトがひつじだった場合には、そのオブジェクトから GetComponent で MoveSheepコンポーネント を取得し、
speed にりんごの速度変化率 rate でかけて代入します。
#br
***継続時間の実装 [#v028d6a9]
次に、継続時間を実装します。
ひつじがりんごに触れて速度を変化させた後、一定時間後に元の速度に戻すことで表現できそうです。
調べてみると、Unity で一連の流れの中で数秒後の処理を行いたいときには コルーチン (coroutine)が便利なようです。
コルーチンはIEnumerator 型を戻り値とする関数として書き、StartCoroutine関数でその関数を呼び出すことができます。
以下はコルーチンの基本的な書き方です。
#geshi(csharp,number){{
public class Apple : MonoBehaviour {
    void Start () {
        // コルーチンを呼び出し  
        StartCoroutine ("Test");
    }

    // コルーチン  
    private IEnumerator Test() {
        // メッセージをconsoleに出力
        Debug.Log("コルーチンの処理が始まりました");
        // 一秒待つ
        yield return new WaitForSeconds (1.0f);
        // メッセージをconsoleに出力
        Debug.Log("コルーチンの処理が終わりました");
    }
}}
このように書くと、Unityの console では
'''コルーチンの処理が始まりました'''
と出力された1秒後に
'''コルーチンの処理が終わりました'''
と出力されます。
#br
これを使って、
ひつじがリンゴに触れたらコルーチンを呼び、
コルーチンのなかで速度を変化させた後、一定時間後に元の速度に戻す
ようにスクリプトを改良します。
まずは、コルーチンを作ります。コルーチン名は ChangeSpeed としておきます。
コルーチンのなかでひつじの速度を変えたいので、MoveSheep を引数で受け取れるようにしておきます。
#geshi(csharp,number){{
public class Apple : MonoBehaviour {

    //略

    // コルーチン  
    private IEnumerator ChangeSpeed(MoveSheep moveSheep) {
        moveSheep.speed *= rate;
        yield return new WaitForSeconds(duration);
        moveSheep.speed /= rate;
    }
}
}}
'''*=''' で変化させたので、 '''/='''すればもとの速度に戻ります。 
元の速度を変数で保管しておく方法もありそうですが、りんごが複数あった時にスピード変化の効果を重ね掛けできるようにしたいのでこのような処理を考えました。
#br
コルーチンができたら、ひつじと接触したときに子のコルーチンを呼び出すようにします。
コルーチンに引数を渡したいときは
#geshi(csharp,number){{
StartCoroutine(コルーチン(引数));
}}
のように書くことができます。

#geshi(csharp,number){{
public class Apple : MonoBehaviour {

    // 略

    void OnTriggerEnter2D(Collider2D col)
    {
        if (col.name == "hitsuji_dot") {
            MoveSheep moveSheep =  col.GetComponent<MoveSheep>();
            StartCoroutine(ChangeSpeed(moveSheep));
        }
    }

    //略

}
}}
#br
***ひつじを消す [#y1139f88]
最後にひつじを消す処理を作ります。
オブジェクトの削除は Destory メソッドで実装できます。
#geshi(csharp,number){{
Destroy(消したいゲームオブジェクト, 消すまでの時間)
}}
消すまでの時間は省略できます。
#geshi(csharp,number){{
public class Apple : MonoBehaviour {

    // 略

    void OnTriggerEnter2D(Collider2D col)
    {
        if (col.name == "hitsuji_dot") {
            MoveSheep moveSheep =  col.GetComponent<MoveSheep>();
            StartCoroutine(ChangeSpeed(moveSheep));
            Destroy(this.gameObject);
        }
    }

    //略

}
}}
これで動きそうですが、これを実行してみるとひつじの速度が変わらなくなります。
調べてみると、Destroyは実行中のCoroutineもオブジェクトと一緒に消してしまうようです。
そこで、オブジェクト自体は消さずに、画像を見えなくして当たり判定を無効にする処理にかえようとおもいます。
オブジェクトの画像をUnityで表示している SpriteRenderer と CircleCollider2D をそれぞれ無効にすれば再現できそうです。
#geshi(csharp,number){{
public class Apple : MonoBehaviour {

    // 略

    void OnTriggerEnter2D(Collider2D col)
    {
        if (col.name == "hitsuji_dot") {
            MoveSheep moveSheep =  col.GetComponent<MoveSheep>();
            spriteRenderer = this.GetComponent<SpriteRenderer>();
            circleCollider2D = this.GetComponent<CircleCollider2D>();

            StartCoroutine(ChangeSpeed(moveSheep));
            spriteRenderer.enabled = false;
            circleCollider2D.enabled = false;
        }
    }

    //略

}
}}
enebled を false にすることでコンポーネントを簡単に無効化できます。
これでりんごが消えたように見えます。
#br
**りんごのプレハブ化 [#ef9f3281]
完成したリンゴをプレハブにします。
その前に、プレハブ保管用のフォルダを Assets のなかに作っておきます。
フォルダ名は Prefabs としておきます。
フォルダを作ったら Hierarchy から先ほど作ったりんごをフォルダにドラッグ・ドロップします。
これだけでプレハブの完成です。
プレハブは Hierarchy 上で水色のキューブとして表示されるようになります。
#br
**りんごをステージの各所に設置 [#n7fd35e9]
作ったプレハブを Scene にドラッグ・ドロップすると簡単にプレハブのインスタンスを生成できます。
ソフトウェアの分野でインスタンスとは「実体」を指し、設計図をもとにして実際に作った物などとよく表現されています。
ここではプレハブが「設計図」になっているわけです。純粋な「コピー」とは少し異なる概念です。
ステージの適当な位置にりんごのインスタンスを作って設置しておきます。
#br
**青りんごの実装 [#b35ea27a]
青リンゴも赤リンゴと同じように作ります。スクリプトは Apple.cs を流用し、rate を1/1.5にするだけです。
長くなるので省略します。
#br
#br
*エレベーターの実装 [#p15c1530]
エレベーターの画像を Assets に追加します。
#download(elevator.png);
例によって画像の Pixels Per Unit は 32 に変更します。
#br
エレベーターの仕様
 ・一定時間おきに定位置から出現する
 ・一定距離登ったら消滅する
 ・地面のように上でジャンプできる
 ・上昇距離、上昇スピード、エレベーターの生成間隔は inspector から調整できるようにする
#br
地面のようにエレベーター上でジャンプできるようにするには、エレベーターにgroundタグをつければ良いかもしれません。
調べてみると、プレハブのインスタンス生成はプログラムから行うことができるようです。
一定時間おきに定位置からインスタンスを生成すれば簡単に実装できそうです。
Assets から elevator.png を Scene にドラッグ・ドロップし、プレハブになるゲームオブジェクトを先に完成させます。
#br
**コンポーネントの追加 [#hef01867]
作ったゲームオブジェクトにコンポーネントを追加します。
調べてみると、物理演算が有効な当たり判定を持ったオブジェクトが移動する場合、Colliderに加えRigidbodyが必要になるようです
必要になりそうなコンポーネントは
 ・物理演算用のRigidBody
 ・当たり判定用のCollider
になります。
#br
Rigidbody は2Dゲーム用の RIgidbody 2D を使います。
Collider はエレベーターの画像に合った形にしたいので、今回は Polygon Collider 2D を使います。
Polygon Collider 2Dは画像に合った形のコライダーを自動で作ってくれます。
inspector > Add Component から追加します。
#br
追加できたらinspectorからコンポーネントの設定を少し変更します。
今回は物理演算を使ってひつじと反発してほしいので Collider の isTrigger はいじりません。
Rigidbody はBody TypeをKinematicに変更しておきます。
RigidBody の Kinematicモード は物理演算で摩擦、重力などを無視したい場合に使います。
物理法則に反する動きを作りたいときに便利です。また、Kinematicモード では transform による移動ができます。
#br
**スクリプトを追加 [#g80ac75f]
Assets > Add Component > New Script から新しいスクリプトをりんごに追加します。
スクリプト名は Elevator としておきます。
先に必要な変数を作っておきます。
上昇速度 を float型で speed
上昇距離 を float型で height としておきます。
#geshi(csharp,number){{
using UnityEngine;

public class Elevator : MonoBehaviour
{
    public float speed;
    public float height;

    void Start()
    {
    }

    void Update()
    {
    }
}
}}
#br
inspector から speed を 2, height を 6 に設定しておきます。
#br
***上昇の実装 [#zd0eed21]
Updateを使って毎フレーム上に移動させます。
[[ひつじを動かしてみる]] と同じやり方でできます。
#geshi(csharp,number){{
using UnityEngine;

public class Elevator : MonoBehaviour
{

    // 略

    void Update()
    {
        Vector3 pos = this.transform.position;
        pos.y += speed * Time.deltaTime;
        this.transform.position = pos;
    }
}
}}
#br
***一定距離上昇して削除する処理の実装 [#n2f74772]
上昇開始位置を変数で保存し、その現在位置 - 位置 が height を超えたときに Destroy すれば再現できそうです。
private な 変数 initPosition を追加し, Start で開始位置を記録するようにします。
#geshi(csharp,number){{
public class Elevator : MonoBehaviour
{
    //略

    private Vector3 initPosition;

    void Start()
    {
        initPosition = this.transform.position;
    }

    void Update()
    {
        Vector3 pos = this.transform.position;
        pos.y += speed * Time.deltaTime;
        this.transform.position = pos;
        if (this.transform.position.y - initPosition.y > height)
        {
            Destroy(this.gameObject);
        }
    }
}
}}
#br
**エレベーターのプレハブ化 [#xa53860d]
Hierarchy から完成したエレベーターを Prefabsフォルダにドラッグ・ドロップします。
Hierarchy に残っているインスタンスは右クリック > Delete で削除します。
#br
**インスタンスの自動生成 [#k88c1e21]
りんごはあらかじめステージに設置しましたが、エレベーターはスクリプトで一定時間おきに同じ場所から出現するようにします。
***ElevatorGeneratorの作成 [#v3c09cd5]
まずは、エレベーターを生成するオブジェクトを新しく作ります。
Hierarchy > Create Empty
で空オブジェクトを作り、名前を ElevatorGenerator としておきます。
***スクリプトの追加 [#c04b7115]
作成したオブジェクトにスクリプトを追加します。
スクリプト名は ElevatorGenerator としておきます。
プレハブの生成は Instantiate メソッドで書くことができます。
#geshi(csharp,number){{
Instantiate(プレハブ);
}}
Instantiate は引数で生成する場所や向きを指定できます。
自分と同じ位置に生成したい場合は、Prefabの座標を(0,0)にして
#geshi(csharp,number){{
Instantiate(プレハブ, this.transform);
}}
のようにするだけでOKです
以下細かいこと
 Instantiateの引数の挙動に注意
 ・引数が1つの場合、プレハブのoriginalのtransformで生成
 ・引数が2つの場合、第二引数で参照するtransformを指定する
 ・引数が3つの場合は、引数の型に応じて
  第二:参照するtransform(transform)、第三:ワールド座標(false) or 第二引数のtransform基準のローカル座標(true)の切り替え 指定しない場合(引数2つの場合)true
  第二:参照するposition(Vector3)  、第三:参照するrotation(Quaternion)
  を切り替える
 よってオブジェクトの位置を初期化する場合、positionに加えrotationまで指定する必要がある
 親オブジェクトと同じ位置に生成したい場合、Instantiate(elevator, this.transform.position, this.transform.rotation)
 とする方法もあるが, あらかじめPrefabのoriginalの座標を0,0に設定しておくことで、
 Instantiate(elevator, this.transform) のように書いて実現できる
 そうです笑。
#br
プレハブは Assets 内から検索するのが難しそうなので、Publicで代入用の変数を作り、inspector から設定することにします。
#geshi(csharp,number){{
using UnityEngine;

public class ElevatorGenerator : MonoBehaviour
{
    public Elevator elevator;

    void Start()
    {
        Instantiate(elevator, this.transform);
    }
}
}}
Assets/Prefabs/Elevatorを選択し、transform Position のx ,y をそれぞれ0, 0にし、
ElevatorGenerator を inspector で選択し、Elevator 変数に Assets/Prefabs/Elevator をドラッグ・ドロップしておきます。
この状態でゲームを起動すると、ElevatorGeneratorの場所にElevatorが生成されます。
#br
次に、エレベーターを一定時間おきに生成するようにスクリプトを改良します。
調べてみると、一定時間おきに同じ処理を行う場合には InvokeRepeating を使うと簡単なようです。
#geshi(csharp,number){{
InvokeRepeating("関数名", 開始時間, 繰り返し間隔);
}}
これを使ってElevatorGeneratorを改良します。
InvokeRepeatingに渡す関数にはそのままでは引数を指定できないので、Instantiateをそのまま呼ぶのはあきらめ、Instantiateに引数入れて呼ぶ関数を別で作成します。
関数名は GenerateElevator としておきます。
生成間隔はpublicで変数を作り、inspector から調整できるようにしておきます。
#geshi(csharp,number){{
public class ElevatorGenerator : MonoBehaviour
{
    public Elevator elevator;
    public float repeatRate;

    void Start()
    {
        InvokeRepeating("GenerateElevator", 3f, repeatRate);
    }

    void GenerateElevator()
    {
        Instantiate(elevator, this.transform);
    }
}
}}
これでスクリプトが完成です。
repeatRate は 1 に設定しておきます。
ElevatorGenerator を Scene 上で移動すれば、好きな位置からエレベーターを生やすことができます。
**エレベーターを地面判定にする [#h6cbd989]
地面の判定は ground タグで行ったので、エレベーターに groundタグをつければエレベーターを地面扱いにできそうです。
Assets/Prefabs/Elevator を選択し、inspector から Tag を Ground に設定します。
これでひつじがエレベーター上でもジャンプできるようになります。

*ゴールの実装 [#x539a60a]
最後にゴールを実装します。
ゴールの仕様は「接触したらクリア(スコア)画面に飛ぶ」というものになります。
よって、実装に必要な機能は
 1.ゴールとの当たり判定を見る
 2.当たった対象がプレイヤーかどうか判定を取る
 3.クリア(スコア)画面に飛ぶ
 といった風になります。
**ゲームオブジェクトの作成 [#o8356dcc]
まず、ゴールの画像を挿入しましょう。
#download(goal.png)
これまでと同様に、上の画像からダウンロードしてアセッツのImagesにいれて
画像のPixePerUnitを32にします。
そうしたら、ヒエラルキーにドラッグ&ドロップし、名前を「Goal」とします。
これで、「Goal」という名前のオブジェクトとして生成されます。
#br
次に、ゴールの設置したいところに作った「Goal」オブジェクトを移動させましょう。
(シーンからオブジェクトをドラッグして移動させたり、「Goal」オブジェクトのインスペクターからTransform>
positionを変更させたり、お好みの方法で移動・設置してください)
#br
**新しいシーンの追加 [#rd0e1d6b]
次は「ゴールについたらクリア(スコア)画面に飛ぶ」という機能を追加します。
そのためにはまず、新しいシーンを作らなければなりません。
新しいシーンは、アセッツを右クリックし、Create>Sceneで作ることができます。
とりあえず、名前は「Score」とでもしましょう。
これで、「Score」という名前の新しいシーンができました。
そうしたら次は、「ゴールについたら」つまり「「Goal」オブジェクトに触れたら」今のシーンから「Score」シーンに移動する、
という機能を作ります。
#br
**当たり判定の追加 [#h6bd4345]
前準備として、「Goal」オブジェクトに当たり判定を作ります。
「Goal」オブジェクトのインスペクター下部のAddComportnemtから、
Physics2D>PolygonCollider2Dをおしてコンポーネントを追加してください。
「Goal」オブジェクトの当たり判定は円や四角形で表現するのは難しいため、
今回は「PolygonCollider2D」という多角形の当たり判定を作れるものを利用しました。
インスペクターのPolygonCollider2D>EditColliderから画像の縁に合うように当たり判定の線を調節してください。
あと、「isTrigger」にチェックを入れるのもお忘れなく。
#br
**スクリプトの追加 [#k681a75c]
当たり判定ができたら、C#スクリプトを作っていきます。
これまでと同じ要領で、「Goal」オブジェクトのインスペクター下部のAddComportnemtから、
NewScriptをおして、名前をGoalとして新しいスクリプトを「Goal」オブジェクトのコンポーネントととして追加してあげます。
次に、テキストエディターを開いて「Goal」スクリプトを編集していきます。
#br
#geshi(csharp,number){{
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement; //<=新たに追加

public class Goal : MonoBehaviour
{
    //略

        //当たり判定を貫通したときの命令を記述する
    void OnTriggerEnter2D(Collider2D col)
    {
        //当たった対象の名前が"hitsuji_dot"だったら、「Score」シーンに移動する
        if (col.name == "hitsuji_dot")
        {
            SceneManager.LoadScene("Score");
        }
    }
}
}}
#br
ここで初めて、これまでさんざん無視してきた"using ○○;"に触れようと思います。
ここでの○○は「名前空間(ネームスペース)」と呼ばれるもので、○○の中で定義されるものを、このスクリプトでも扱えるようにしてくれます。
C#では定義されたオブジェクトしか使うことができません。
例えば、"using UnityEngine"がないと、これまで何気なく使ってきた"GameObject"クラスや"Transform"クラス、今回も使っている"Collider2D"クラスなどが使えないわけです。
逆を言えば、"using UnityEngine"をいれることで、"UnityEngine"の名前空間で定義される様々なクラスやメゾットを自分で定義せずに使うことができます。
今回も、シーンに関する設定・動きなどを扱っている"UnityEngine.SceneManagment"という名前空間を使うことで、
"SceneManager"クラスおよび"LoadScene"メゾット(引数に指定した名前のシーンに移動する関数)を自作せずに利用できます。
#br
さて、これでスクリプトは完成しました。
動作確認をしてみましょう。
ゴールに触れて真っ青な画面に飛んだら成功です。