Skip to content

第13回 いろいろな敵 ​

この回でつくるもの:波のように動く敵と、自機をねらって弾を撃つ敵を作ります。敵がランダムに、ときには編隊(へんたい=列をつくって飛ぶこと)で出てくるようにします。

この回で学ぶこと ​

  • Prefab Variant(プレハブ・バリアント)で「元のプレハブの色ちがい・機能ちがい」を作る
  • Mathf.Sin で波のような動きを作る
  • Mathf.Atan2 で「相手のいる方向」を角度にする
  • 配列(GameObject[])でたくさんの種類をまとめて持つ
  • 新しいタグ EnemyBullet を作って当たり判定に使う

前回までのふりかえり ​

前回は Title シーンを作り、START ボタンでゲームが始まり、ゲームオーバー画面からタイトルに戻れるようになりました。 今のところ敵は Enemy_Straight(まっすぐ左へ進むだけ)の1種類です。今回は敵の種類を増やして、ゲームを手ごわくしましょう。

1. Prefab Variant とは ​

1-1. Variant の考え方 ​

新しい敵を作るとき、毎回ゼロから「親+Model+Rigidbody+Collider+タグ+Enemy スクリプト」を作るのは大変です。 そこで Prefab Variant(プレハブの「バリエーション」)を使います。

Enemy_Straight(元のプレハブ = ベース)
 ├ Enemy_Wave    … 元をそのまま受けつぎ + EnemyWave を追加、色を緑に
 └ Enemy_Shooter … 元をそのまま受けつぎ + EnemyShooter を追加
ふつうのコピー(Ctrl+D)Prefab Variant
元を変えたときコピーは変わらないVariant にも反映される
変えたところ全部バラバラ変えたところだけ「上書き」として残る

たとえば、あとで Enemy_Straight の Collider の大きさを直すと、Variant の敵も全部いっしょに直ります。

たとえ話

Variant は「元の絵の上に、トレーシングペーパーを重ねて描き足す」ようなものです。下の絵(元のプレハブ)を描き直すと、重ねた紙ごしに見える絵も変わります。

2. 波のように動く敵 Enemy_Wave ​

2-1. Variant を作る ​

  1. Project ウィンドウで Assets/_Game/Prefabs を開きます
  2. Enemy_Straight を右クリックします
  3. Create > Prefab Variant を選びます
  4. Enemy_Straight Variant というプレハブができるので、名前を Enemy_Wave に変えます

先生へ

Variant のアイコンは、ふつうのプレハブの青い箱に矢印のような印が付いたものになります。生徒に見分け方を確認させてください。

2-2. 色を変える ​

  1. Assets/_Game/Materials を開き、右クリック Create > Material で新しいマテリアルを作り、名前を Enemy_Green にします
  2. Enemy_Green を選び、Inspector の Base Map の右の色をクリックして緑にします
  3. Project ウィンドウで Enemy_Wave をダブルクリックします(プレハブ編集モードになります)
  4. Hierarchy で Model(またはその子のメッシュ)を選びます
  5. Inspector の Mesh Renderer の Materials を開き、Element 0 の右の欄に Enemy_Green をドラッグします
  6. Hierarchy の左上の <(戻る矢印)を押して、プレハブ編集モードを終わります

ヒント

Kenney のモデルは、1つのモデルに Element 0、Element 1…と複数のマテリアルが付いていることがあります。全部変えなくても、見分けがつけば OK です。好きな Element を差し替えてみましょう。

2-3. Mathf.Sin を知ろう ​

Mathf.Sin(サイン)は、数を入れると -1 から 1 のあいだを行ったり来たりする数 を返す関数です。

 Mathf.Sin(x) の値

  1 |    ****              ****
    |  **    **          **    **
  0 |**--------**------**--------**---->  x
    |            **  **
 -1 |              **
x(入れる数)01.573.144.716.28
Mathf.Sin(x)01(いちばん上)0-1(いちばん下)0(もとに戻る)

x をどんどん変えていくと、値は「0 → 上 → 0 → 下 → 0 →…」とくり返します。これをそのまま 敵の高さ(y) に使えば、波のように上下する敵になります。

  • amplitude(振幅=しんぷく):波の高さ。Sin は -1〜1 なので、1.5 をかけると -1.5〜1.5 になる
  • frequency(周波数=しゅうはすう):波の細かさ。大きいほどギザギザ細かい波になる

2-4. EnemyWave スクリプト ​

  1. Assets/_Game/Scripts で右クリック Create > Scripting > MonoBehaviour Script を選び、名前を EnemyWave にします
  2. ダブルクリックして開き、次のように書きます
csharp
using UnityEngine;

// 波のように上下しながら進む(Enemy と一緒に付ける)
public class EnemyWave : MonoBehaviour
{
    public float amplitude = 1.5f;   // 波の高さ
    public float frequency = 1.5f;   // 波の細かさ

    float startY;

    void Start()
    {
        startY = transform.position.y;
    }

    void Update()
    {
        // x の位置で高さを決めるので、つなげて出すと「ヘビ」のような編隊になる
        Vector3 pos = transform.position;
        pos.y = startY + Mathf.Sin(pos.x * frequency) * amplitude;
        transform.position = pos;
    }
}
  1. 保存します(Ctrl+S、Macは Cmd+S)

コードの解説

行意味
startY = transform.position.y;生まれたときの高さを覚えておく。波の「まん中」になる
Mathf.Sin(pos.x * frequency)今の x の位置から、-1〜1 の値を作る
* amplitude波の高さをかける
startY + ...まん中の高さから、上下にずらす

左へ進む動きは、もともと付いている Enemy がやってくれます。EnemyWave は 高さだけ を決めています。

なぜ「時間」ではなく「x の位置」を使うの?

x の位置で高さを決めると、同じ x に来た敵はみんな同じ高さ になります。だから、少しずつ遅れて出した敵は、前の敵とまったく同じ道を通ります。これが「ヘビ」のような編隊に見えるひみつです。

2-5. Enemy_Wave に付ける ​

  1. Project ウィンドウで Enemy_Wave をダブルクリックしてプレハブ編集モードにします
  2. Hierarchy でいちばん上の Enemy_Wave(親)を選びます
  3. Inspector の Add Component を押し、EnemyWave と入力して選びます
  4. < を押してプレハブ編集モードを終わります

3. EnemySpawner を配列+編隊にする ​

3-1. 配列とは ​

今の EnemySpawner は enemyPrefab を1つしか持てません。これを 配列(はいれつ)にします。配列は「同じ種類のものを、番号つきの箱にならべたもの」です。

enemyPrefabs(GameObject[] 型)
 [0] Enemy_Straight
 [1] Enemy_Wave
 [2] Enemy_Shooter   ← あとで入れる
  • enemyPrefabs.Length … 箱の数(上の例なら 3)
  • enemyPrefabs[1] … 1番の箱の中身(番号は 0から 数える)
  • Random.Range(0, enemyPrefabs.Length) … 0, 1, 2 のどれかがランダムに出る(int の Random.Range は最後の数をふくまない)

3-2. EnemySpawner を書きかえる ​

  1. EnemySpawner をダブルクリックして開き、次のように書きかえます
csharp
using System.Collections;
using UnityEngine;

// 敵を一定時間ごとに出す
public class EnemySpawner : MonoBehaviour
{
    public GameObject[] enemyPrefabs;   // 出す敵(第13回で配列に) // ★変更
    public float interval = 1.5f;       // 出す間隔(秒)
    public float minY = -4f;            // 出す高さの範囲
    public float maxY = 4f;
    public int formationCount = 1;      // 一度に何機つなげて出すか(第13回) // ★追加
    public float formationGap = 0.3f;   // つなげて出すときの間隔 // ★追加

    void Start()
    {
        StartCoroutine(SpawnLoop());
    }

    IEnumerator SpawnLoop()
    {
        while (true)
        {
            yield return new WaitForSeconds(interval);

            // ゲームが終わっていたら出さない(第11回)
            if (!GameManager.instance.isPlaying) continue;

            // ランダムに敵の種類と高さを決める
            GameObject prefab = enemyPrefabs[Random.Range(0, enemyPrefabs.Length)]; // ★追加
            float y = Random.Range(minY, maxY);

            for (int i = 0; i < formationCount; i++) // ★追加
            { // ★追加
                Vector3 pos = new Vector3(transform.position.x, y, 0f); // ★変更
                Instantiate(prefab, pos, Quaternion.identity); // ★変更
                yield return new WaitForSeconds(formationGap); // ★追加
            } // ★追加
        }
    }
}
  1. 保存します

コードの解説

  • GameObject prefab = enemyPrefabs[...] … 配列の中から1つ、ランダムに選びます
  • for (int i = 0; i < formationCount; i++) … formationCount 回くり返します。3 なら 3機
  • yield return new WaitForSeconds(formationGap); … 1機出すごとに少し待つので、敵が 少しずつ遅れて 一列にならびます
  • 同じ y から出るので、Enemy_Wave なら同じ波の道をたどり、ヘビのような編隊になります

3-3. Inspector で敵を登録する ​

  1. Hierarchy で EnemySpawner を選びます
  2. Inspector を見ると、Enemy Prefabs という新しい欄ができています(前の Enemy Prefab の設定は消えています)
  3. Enemy Prefabs の右下の + を2回押して、要素を2つにします
  4. Element 0 に Enemy_Straight、Element 1 に Enemy_Wave をドラッグします
  5. Formation Count を 3 にします
  6. シーンを保存します(Ctrl+S、Macは Cmd+S)

注意

変数の名前を enemyPrefab から enemyPrefabs に変えたので、前に入れておいた敵は 外れてしまいます。必ず入れなおしてください。入れ忘れると Play したときに Console に赤いエラーが出ます。

3-4. 動作確認 ​

▶ Play ボタンを押して、まっすぐの敵と緑の敵が3機ずつつながって出てきて、緑の敵がヘビのように波打って飛べばOK。

ヒント

Play 中に Enemy_Wave の Amplitude や Frequency を変えると、波の形が変わるのがわかります。(Project ウィンドウでプレハブを選んで変えます。Play を止めると戻るものもあるので、気に入った値はメモしておきましょう)

4. 敵の弾 EnemyBullet ​

4-1. タグ EnemyBullet を作る ​

自機が「これは敵の弾だ」とわかるように、新しいタグを作ります。

  1. Hierarchy で何かオブジェクト(例: Player)を選びます
  2. Inspector のいちばん上の Tag のプルダウンを開き、Add Tag... を選びます
  3. Tags の一覧の右下の + を押します
  4. 名前に EnemyBullet と入力して Save を押します

4-2. 弾の見た目と当たり判定 ​

  1. Hierarchy で右クリック 3D Object > Sphere を選び、名前を EnemyBullet にします
  2. Position を (0, 0, 0)、Scale を (0.3, 0.3, 0.3) にします
  3. Assets/_Game/Materials で右クリック Create > Material、名前を EnemyBullet_Red にします
  4. Base Map を赤にし、Emission にチェックを入れて、Emission Map の色も赤にします
  5. EnemyBullet_Red を Scene ビューの Sphere にドラッグします
  6. Hierarchy で EnemyBullet を選び、Inspector の Tag を EnemyBullet にします
  7. Sphere Collider の Is Trigger にチェックを入れます
  8. Add Component で Rigidbody を追加し、Use Gravity をオフ、Is Kinematic をオンにします

よくあるまちがい

Tag を EnemyBullet にし忘れると、弾が自機に当たっても何も起きません。Add Tag... でタグを作っただけでは付いていないので、もう一度オブジェクトを選んで Tag を選びなおしましょう。

4-3. EnemyBullet スクリプト ​

  1. Assets/_Game/Scripts に Create > Scripting > MonoBehaviour Script で EnemyBullet を作り、次のように書きます
csharp
using UnityEngine;

// 敵の弾(向いている方向へまっすぐ進む)
public class EnemyBullet : MonoBehaviour
{
    public float speed = 5f;
    public float lifeTime = 5f;

    void Start()
    {
        Destroy(gameObject, lifeTime);
    }

    void Update()
    {
        transform.Translate(Vector3.right * speed * Time.deltaTime);
    }
}
  1. 保存します
  2. Hierarchy の EnemyBullet に、Add Component で EnemyBullet を付けます
  3. EnemyBullet を Hierarchy から Assets/_Game/Prefabs へドラッグしてプレハブにします
  4. Hierarchy の EnemyBullet は消します(右クリック Delete)
  5. シーンを保存します

コードの解説

  • 自機の Bullet とほとんど同じです。Vector3.right(自分の右)へ進みます
  • Translate は、何も書かないと 自分の向き を基準に動きます。なので、弾を回転させて出せば、その向きへ飛びます。次の章で、この回転を使って自機をねらいます

5. 弾を撃つ敵 Enemy_Shooter ​

5-1. Mathf.Atan2 で「自機の方向」を角度にする ​

敵から自機への 向き(矢印)は、引き算で求められます。

 dir = 自機の位置 - 敵の位置

        y
        ^
        |        ● 敵 (4, 2)
        |      ↙
        |    ↙   dir = (-6, -1) … 左に6、下に1
        |  ↙
  ● 自機 (-2, 1)
 -------+---------------> x

でも、弾を回転させるには「何度回すか」という 角度 が必要です。矢印から角度を求めるのが Mathf.Atan2(y, x)(アークタンジェント・ツー)です。

向きdir の例角度
右 →(1, 0)0度
上 ↑(0, 1)90度
左 ←(-1, 0)180度
下 ↓(0, -1)-90度
  • Mathf.Atan2(dir.y, dir.x) … y が先、x があと の順番に注意!
  • 答えは「ラジアン」という単位で出てきます(180度 = 約3.14)
  • * Mathf.Rad2Deg をかけると、ふだん使う「度」に直せます

たとえ話

時計の針で考えてみましょう。右(3時の方向)を 0度 として、反時計回りに何度回したら自機の方を指すか。それを計算してくれるのが Atan2 です。

5-2. EnemyShooter スクリプト ​

  1. Assets/_Game/Scripts に EnemyShooter を作り、次のように書きます
csharp
using UnityEngine;

// 自機をねらって弾を撃つ(Enemy と一緒に付ける)
public class EnemyShooter : MonoBehaviour
{
    public GameObject bulletPrefab;    // EnemyBullet プレハブ
    public float interval = 2f;        // 撃つ間隔(秒)

    float timer;

    void Start()
    {
        timer = interval;
    }

    void Update()
    {
        timer -= Time.deltaTime;
        if (timer <= 0f)
        {
            Shoot();
            timer = interval;
        }
    }

    void Shoot()
    {
        GameObject player = GameObject.FindWithTag("Player");
        if (player == null) return;   // 自機がいなければ撃たない

        // 自機への向きを角度にする
        Vector3 dir = player.transform.position - transform.position;
        float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;

        Instantiate(bulletPrefab, transform.position, Quaternion.Euler(0f, 0f, angle));
    }
}
  1. 保存します

コードの解説

行意味
timer = interval;出てきてすぐには撃たず、少し待ってから撃つ
GameObject.FindWithTag("Player")タグが Player のオブジェクトをさがす
if (player == null) return;ゲームオーバーで自機が消えていたら撃たない
Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg自機への向きを「度」にする
Quaternion.Euler(0f, 0f, angle)Z軸のまわりに angle 度回した向きで弾を出す

5-3. Enemy_Shooter を作る ​

  1. Project ウィンドウで Enemy_Straight を右クリックし、Create > Prefab Variant を選びます
  2. 名前を Enemy_Shooter にします
  3. Enemy_Shooter をダブルクリックしてプレハブ編集モードにします
  4. 親の Enemy_Shooter を選び、Add Component で EnemyShooter を付けます
  5. Bullet Prefab の欄に、Project ウィンドウの EnemyBullet プレハブをドラッグします
  6. Enemy の Speed を 2 にします(撃つ敵は少しゆっくり)
  7. 見分けやすいように、2-2 と同じやり方で別の色のマテリアル(例: Enemy_Purple)を作って付けてもOKです
  8. < を押してプレハブ編集モードを終わります

ヒント

Variant で変えた項目は、Inspector で 太字 になり、左に青い線が付きます。「元から変えたところ」が一目でわかります。

5-4. スポナーに登録する ​

  1. Hierarchy で EnemySpawner を選びます
  2. Enemy Prefabs の + を押して要素を3つにし、Element 2 に Enemy_Shooter をドラッグします
  3. シーンを保存します

5-5. 動作確認 ​

▶ Play ボタンを押して、Enemy_Shooter が赤い弾を自機に向けて撃ってくればOK。自機を動かすと、弾の向きも変わります。 (まだ弾に当たってもやられません。次の章で直します)

6. 敵の弾に当たったらやられる ​

6-1. PlayerLife を書きかえる ​

  1. PlayerLife を開き、OnTriggerEnter を次のように書きかえます
csharp
using System.Collections;
using UnityEngine;

// 自機がやられる・復活する
public class PlayerLife : MonoBehaviour
{
    public GameObject explosionPrefab;    // 爆発(第9回)
    public AudioClip explosionSound;      // 爆発音(第9回)
    public GameObject model;              // 点滅させる見た目(第11回)
    public float invincibleTime = 2f;     // 無敵時間(第11回)

    bool isInvincible = false;

    void OnTriggerEnter(Collider other)
    {
        if (isInvincible) return;

        if (other.CompareTag("Enemy") || other.CompareTag("EnemyBullet")) // ★変更
        {
            if (other.CompareTag("EnemyBullet")) // ★追加
            { // ★追加
                Destroy(other.gameObject); // ★追加
            } // ★追加

            Hit();
        }
    }

    void Hit()
    {
        if (explosionPrefab != null)
        {
            Instantiate(explosionPrefab, transform.position, Quaternion.identity);
        }
        if (explosionSound != null)
        {
            AudioSource.PlayClipAtPoint(explosionSound, Camera.main.transform.position);
        }

        GameManager.instance.LoseLife();

        if (GameManager.instance.lives > 0)
        {
            StartCoroutine(Respawn());
        }
        else
        {
            gameObject.SetActive(false);
        }
    }

    // 少しのあいだ点滅しながら無敵になる
    IEnumerator Respawn()
    {
        isInvincible = true;
        transform.position = new Vector3(-6f, 0f, 0f);

        float time = 0f;
        while (time < invincibleTime)
        {
            model.SetActive(!model.activeSelf);
            yield return new WaitForSeconds(0.1f);
            time += 0.1f;
        }

        model.SetActive(true);
        isInvincible = false;
    }
}
  1. 保存します

コードの解説

  • || は「または」という意味です。「Enemy タグ または EnemyBullet タグ」なら、やられます
  • 敵の弾に当たったときは、Destroy(other.gameObject); でその弾を消します。消さないと、弾が自機をすりぬけて飛んでいってしまいます
  • 無敵中(isInvincible が true)は、いちばん上の return で何もしません

6-2. 動作確認 ​

▶ Play ボタンを押して、次のようになればOK。

  • 赤い弾に当たると自機が爆発して、残機が1つ減る
  • 当たった弾は消える
  • 復活して点滅しているあいだは、弾に当たってもやられない

チャレンジ課題 ​

  • Enemy_Wave の Variant をもう1つ作り、Amplitude を 3、Frequency を 0.8 にして「大きくゆったり波打つ敵」を作ってみよう
  • EnemyShooter の Interval を小さくして、連射してくる強い敵を作ってみよう
  • 敵の弾を3方向に撃つようにしてみよう(ヒント: angle + 15 と angle - 15 の向きにも Instantiate する)
  • 配列に同じプレハブを2回入れると、その敵が出る確率が上がります。ためしてみよう

つまずきポイント ​

こんなときはここを確認
Play すると IndexOutOfRangeException や UnassignedReferenceException が出るEnemySpawner の Enemy Prefabs が空っぽになっていないか。全部の Element にプレハブが入っているか
緑の敵が波打たないEnemyWave を Enemy_Wave の 親 に付けたか。Amplitude が 0 になっていないか
敵が弾を撃たないEnemy_Shooter の Bullet Prefab に EnemyBullet が入っているか。自機のタグが Player か
弾が自機をすりぬけるEnemyBullet プレハブの Tag が EnemyBullet か。Is Trigger と Rigidbody の Is Kinematic がオンか
弾が自機と反対の方へ飛ぶMathf.Atan2(dir.y, dir.x) の y と x の順番、dir の引き算の順番(自機 − 敵)を確認

まとめ ​

  • Prefab Variant を使うと、元のプレハブを受けついだ「色ちがい・機能ちがい」の敵がかんたんに作れる
  • Mathf.Sin は -1〜1 を行ったり来たりするので、波の動きに使える
  • Mathf.Atan2 で向き(矢印)を角度にすると、相手をねらって撃てる
  • 配列を使うと、たくさんの種類をまとめて持ち、ランダムに選べる