Skip to content

第17回 ボス戦 ​

この回でつくるもの:画面の右から大きなボスが登場し、上下に動きながら弾をばらまいてきます。画面の上には、ボスの体力バーが出ます。

この回で学ぶこと ​

  • 大きな敵(ボス)を「親+子」の形で作る
  • enum(名前のついた選択肢)と switch で「状態」を切り替える
  • コルーチンで攻撃パターンを順番にくり返す
  • UI の Pivot(回転・拡大の中心)を使って、体力バーを左から減らす

前回までのふりかえり ​

第16回では、自機のあとをついてくるオプションを作りました。 カプセルを取ってゲージで OPTION を選ぶと、オプションが最大2つまで増え、一緒に弾を撃ちます。 これでパワーアップはすべてそろいました。今回は、いよいよステージの最後に待つボスを作ります。

1. ボスを作る ​

1-1. ボスの親オブジェクトを作る ​

ボスも、自機や敵と同じく「親(空のオブジェクト)+子(見た目のモデル)」の2段で作ります。

  1. Game シーンが開いていることを確認します。
  2. Hierarchy の何もないところを右クリックし、Create Empty を選びます。
  3. 名前を Boss にします。
  4. Inspector の Transform で Position を (14, 0, 0) にします。
  5. Rotation が (0, 0, 0)、Scale が (1, 1, 1) であることを確認します。

なぜ x = 14?

画面の右はしは x = 約 8.9 です。x = 14 は画面の外なので、ボスは見えないところから入ってきます。

1-2. 見た目のモデルを付ける ​

  1. Project ウィンドウで Assets/ThirdParty/KenneySpaceKit を開きます。
  2. craft_miner を探して、Hierarchy の Boss の上にドラッグします。Boss の子になります。
  3. 子になったモデルの名前を Model にします。
  4. Model の Position を (0, 0, 0) にします。
  5. Model の Rotation の Y を 90 または -90 にして、機首が 左(-X) を向くようにします。
  6. Model の Scale を (3, 3, 3) くらいにします。Game 画面で見て、自機の2〜3倍くらいの大きさになればOKです。

モデルの名前がちがうとき

craft_miner が見つからないときは、プレビューを見て好きな大きめの機体を選んでOKです。

1-3. 弾が出る場所 Muzzle を作る ​

  1. Hierarchy で Boss を右クリックし、Create Empty を選びます。Boss の子に空のオブジェクトができます。
  2. 名前を Muzzle にします。
  3. Muzzle の Position を (-1.5, 0, 0) にします。ボスの機首の先あたりにくればOKです。ずれていたら X を調整します。

1-4. タグと当たり判定を付ける ​

  1. Hierarchy で Boss(親)を選びます。
  2. Inspector の一番上の Tag を Enemy にします。
  3. Add Component を押し、Rigidbody と入力して Rigidbody を追加します。
  4. Rigidbody の Use Gravity を OFF、Is Kinematic を ON にします。
  5. もう一度 Add Component を押し、Box Collider を追加します。
  6. Box Collider の Is Trigger を ON にします。
  7. Scene ビューの緑の線を見ながら、Size と Center を調整して、ボスの体がだいたい入るようにします(例: Size (4, 2, 2))。

コライダーは親に付ける

Collider と Rigidbody は Model ではなく、親の Boss に付けます。弾が当たったとき、親の Enemy スクリプトが見つかるようにするためです。

1-5. Enemy スクリプトを変える ​

ボスも「敵」の一種なので、Enemy スクリプトを使います。ただし、ボスだけの決まりを少し足します。

Assets/_Game/Scripts/Enemy.cs をダブルクリックして開き、次のように書きかえます。

csharp
using UnityEngine;

// 敵の基本(移動・HP・やられたとき)
public class Enemy : MonoBehaviour
{
    public float speed = 3f;              // 左へ進む速さ
    public int hp = 1;                    // 体力(第8回)
    public GameObject explosionPrefab;    // 爆発(第9回)
    public AudioClip explosionSound;      // 爆発音(第9回)
    public int score = 100;               // 倒したときの点数(第10回)
    public GameObject capsulePrefab;      // 落とすカプセル(第14回)
    public bool isBoss = false;           // ボスかどうか(第17回) // ★追加

    [HideInInspector] public int maxHp;   // 最初の体力(第17回) // ★追加

    bool isDead = false;

    void Awake()                          // ★追加
    {                                     // ★追加
        maxHp = hp;                       // ★追加
    }                                     // ★追加

    void Update()
    {
        transform.Translate(Vector3.left * speed * Time.deltaTime, Space.World);

        // 画面の左に出たら消す
        if (transform.position.x < -12f)
        {
            Destroy(gameObject);
        }
    }

    // ダメージを受ける(第8回)
    public void TakeDamage(int damage)
    {
        if (isDead) return;

        hp -= damage;
        if (hp <= 0)
        {
            Die();
        }
    }

    void Die()
    {
        isDead = true;

        GameManager.instance.AddScore(score);

        if (explosionPrefab != null)
        {
            Instantiate(explosionPrefab, transform.position, Quaternion.identity);
        }
        if (explosionSound != null)
        {
            AudioSource.PlayClipAtPoint(explosionSound, Camera.main.transform.position);
        }
        if (capsulePrefab != null)
        {
            Instantiate(capsulePrefab, transform.position, Quaternion.identity);
        }
        if (isBoss)                       // ★追加
        {                                 // ★追加
            Debug.Log("BOSS DEFEATED");   // ★追加
        }                                 // ★追加

        Destroy(gameObject);
    }
}

保存します(Ctrl+S、Macは Cmd+S)。

コードの解説

行意味
public bool isBoss = false;ボスかどうかのしるし。ボスだけ Inspector でチェックを入れます
[HideInInspector] public int maxHp;最初の体力を覚えておく変数。[HideInInspector] を付けると、public でも Inspector に出ません(体力バーから読むために public にしています)
void Awake()Start よりも先に、一番最初に1回だけ呼ばれる関数です
maxHp = hp;Inspector で決めた体力(例: 60)を、ダメージを受ける前に覚えておきます
if (isBoss) { ... }ボスがやられたときだけ Console に「BOSS DEFEATED」と出します。第18回で、ここを「ステージクリア」に変えます

1-6. Enemy の値を入れる ​

  1. Hierarchy で Boss を選びます。
  2. Add Component を押し、Enemy と入力して Enemy を追加します。
  3. 次のように値を入れます。
項目値
Speed0
Hp60
Explosion PrefabExplosion プレハブ
Explosion Sound爆発音(第9回で使ったもの)
Score10000
Capsule Prefab空のまま(None)
Is BossON(チェックを入れる)

Speed を 0 にする理由

Enemy の Speed が 0 より大きいと、ボスはそのまま左へ進んで画面の外へ消えてしまいます。ボスの動きは、次に作る Boss スクリプトにまかせます。

2. Boss スクリプト(登場して、戦う) ​

2-1. 「状態」という考え方 ​

ボスは、次の2つの「状態」を持ちます。

状態やること
Enter(登場中)画面の右から左へ、ゆっくり入ってくる
Fight(戦闘中)決まった場所で上下に動きながら、弾を撃つ

「今どの状態か」を覚えておき、状態によってやることを変えます。 このやり方は、ゲームのキャラクターを作るときによく使います。

2-2. enum と switch ​

enum(イーナム)は、「名前のついた選択肢」を自分で作るしくみです。第15回の ShotType(Normal / Double / Laser)でも使いましたね。

csharp
enum State
{
    Enter,   // 登場中
    Fight    // 戦闘中
}

switch(スイッチ)は、「値によって、やることを分ける」ための文です。

csharp
switch (state)
{
    case State.Enter:
        // 登場中にやること
        break;
    case State.Fight:
        // 戦闘中にやること
        break;
}
  • case ○○: から break; までが、その状態のときに動く部分です
  • break; を忘れるとエラーになるので注意しましょう

if でも書けるけど…

if (state == State.Enter) { } else if (state == State.Fight) { } と書いても同じです。状態が増えてきたとき、switch のほうが見やすくなります。

2-3. Boss スクリプトを作る ​

  1. Project ウィンドウで Assets/_Game/Scripts を開きます。
  2. 何もないところを右クリックし、Create > Scripting > MonoBehaviour Script を選びます。
  3. 名前を Boss にします(ファイル名とクラス名は同じにします)。
  4. ダブルクリックして開き、中身を全部消して、次のコードを書きます。
csharp
using System.Collections;
using UnityEngine;

// ボスの動きと攻撃(Enemy と一緒に付ける。Enemy の speed は 0 にする)
public class Boss : MonoBehaviour
{
    public float enterX = 6f;          // ここまで入ってきたら戦闘開始
    public float enterSpeed = 2f;
    public float moveRange = 3f;       // 上下に動く幅
    public float moveSpeed = 1f;
    public GameObject bulletPrefab;    // EnemyBullet プレハブ
    public Transform muzzle;           // 弾が出る場所

    enum State
    {
        Enter,   // 登場中
        Fight    // 戦闘中
    }

    State state = State.Enter;
    float fightTime = 0f;

    void Update()
    {
        switch (state)
        {
            case State.Enter:
                transform.Translate(Vector3.left * enterSpeed * Time.deltaTime, Space.World);
                if (transform.position.x <= enterX)
                {
                    state = State.Fight;
                    StartCoroutine(AttackLoop());
                }
                break;

            case State.Fight:
                fightTime += Time.deltaTime;
                Vector3 pos = transform.position;
                pos.y = Mathf.Sin(fightTime * moveSpeed) * moveRange;
                transform.position = pos;
                break;
        }
    }

    // 攻撃パターンをくり返す
    IEnumerator AttackLoop()
    {
        while (true)
        {
            // パターン1:扇形に5発 × 3回
            for (int i = 0; i < 3; i++)
            {
                FireSpread();
                yield return new WaitForSeconds(0.6f);
            }
            yield return new WaitForSeconds(1f);

            // パターン2:自機ねらいの連射
            for (int i = 0; i < 8; i++)
            {
                FireAimed();
                yield return new WaitForSeconds(0.15f);
            }
            yield return new WaitForSeconds(1f);
        }
    }

    void FireSpread()
    {
        for (int angle = -30; angle <= 30; angle += 15)
        {
            // 180度 = 左向き
            Instantiate(bulletPrefab, muzzle.position, Quaternion.Euler(0f, 0f, 180f + angle));
        }
    }

    void FireAimed()
    {
        GameObject player = GameObject.FindWithTag("Player");
        if (player == null) return;

        Vector3 dir = player.transform.position - muzzle.position;
        float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
        Instantiate(bulletPrefab, muzzle.position, Quaternion.Euler(0f, 0f, angle));
    }
}

保存します。

コードの解説(動きの部分)

行意味
State state = State.Enter;最初は「登場中」から始めます
case State.Enter:登場中は、左へ進みます
if (transform.position.x <= enterX)x が 6 まで来たら…
state = State.Fight;状態を「戦闘中」に切り替えます。次のフレームからは case State.Fight: のほうが動きます
StartCoroutine(AttackLoop());攻撃を始めます。状態が切り替わる瞬間に1回だけ呼ばれます
pos.y = Mathf.Sin(fightTime * moveSpeed) * moveRange;第13回の波の敵と同じ Mathf.Sin で、上下にゆったり動きます。y は -3〜3 の間を行ったり来たりします

2-4. Boss の値を入れる ​

  1. Hierarchy で Boss を選びます。
  2. Add Component で Boss を追加します。
  3. Bullet Prefab に、Project の Assets/_Game/Prefabs/EnemyBullet をドラッグします。
  4. Muzzle に、Hierarchy の Boss の子の Muzzle をドラッグします。
  5. Enter X などはそのまま(6、2、3、1)でOKです。
  6. シーンを保存します(Ctrl+S、Macは Cmd+S)。

3. 攻撃パターン ​

3-1. コルーチンで順番に撃つ ​

AttackLoop は、第7回の EnemySpawner と同じ コルーチン(とちゅうで待てる関数)です。 上から順に、次のように動きます。

  1. 扇形に5発撃つ → 0.6秒待つ。これを3回
  2. 1秒休む
  3. 自機をねらって1発撃つ → 0.15秒待つ。これを8回
  4. 1秒休む
  5. 1にもどる(while (true) でずっとくり返す)

休みを入れる理由

ずっと撃ちっぱなしだと、よけるすきがありません。「休み」を入れると、プレイヤーが攻撃するチャンスが生まれます。

3-2. 扇形の弾 FireSpread ​

  • for (int angle = -30; angle <= 30; angle += 15) で、angle は -30, -15, 0, 15, 30 の5つになります
  • 敵の弾(EnemyBullet)は「自分の右」へ進みます。Z を 180度 回すと左向きになります
  • 180f + angle で、左向きを中心に少しずつ角度をずらすので、扇のように広がります

3-3. 自機ねらいの弾 FireAimed ​

第13回の EnemyShooter と同じです。

  • player.transform.position - muzzle.position で、ボスから自機への向き(矢印)を作ります
  • Mathf.Atan2(dir.y, dir.x) で、その矢印の角度を求めます
  • * Mathf.Rad2Deg で、「ラジアン」という単位から「度」に直します

3-4. 動作確認 ​

  1. ▶ を押します。
  2. 右からボスが入ってきて、x = 6 で止まり、上下に動き始めればOKです。
  3. 扇形の弾と、自機ねらいの連射が交互に来ればOKです。
  4. もう一度 ▶ を押して止めます。

4. ボスの体力バー ​

4-1. 体力バーの UI を作る ​

体力バーは「暗い背景」の上に「赤いバー(Fill)」を重ねて作ります。Fill の横の大きさを、体力に合わせて小さくします。

  1. Hierarchy の Canvas を右クリックし、Create Empty を選びます。
  2. 名前を BossHPBar にします。
  3. Inspector の Rect Transform の左上にある四角(Anchor Presets)をクリックし、top-center(上・真ん中)を選びます。
  4. Pos X を 0、Pos Y を -80、Width を 800、Height を 30 にします。
  5. BossHPBar を右クリックし、UI > Image を選びます。名前を Background にします。
  6. Background の Anchor Presets を開き、Alt(Macは Option)を押しながら右下の stretch-stretch(全体に広げる)をクリックします。親と同じ大きさになります。
  7. Background の Image の Color を暗い灰色(例: R 40, G 40, B 40)にします。
  8. BossHPBar を右クリックし、もう一度 UI > Image を選びます。名前を Fill にします。
  9. Fill も、Alt(Macは Option)を押しながら stretch-stretch をクリックします。
  10. Fill の Image の Color を赤にします。
  11. Fill の Rect Transform の Pivot の X を 0 にします(Y は 0.5 のまま)。
  12. Hierarchy で、Fill が Background より 下 にあることを確認します。

Hierarchy の順番と重なり

UI は、Hierarchy で下にあるものほど手前に表示されます。Fill が上にあると、背景にかくれて見えません。

4-2. Pivot を左にする理由 ​

Pivot(ピボット)は、大きさを変えたり回したりするときの「中心の点」です。

  • Pivot X が 0.5(真ん中)だと、横の大きさを小さくしたとき、両はしから縮みます
  • Pivot X が 0(左はし)だと、左はしを止めたまま、右から縮みます

ゲームの体力バーは、右から減っていくのがふつうなので、Pivot X を 0 にします。

ためしてみよう

Fill を選んで、Scale の X を 0.5 にしてみましょう。バーが左半分だけになれば、Pivot の設定は正しいです。確認したら 1 に戻します。

4-3. BossHPBar スクリプト ​

  1. Assets/_Game/Scripts で右クリックし、Create > Scripting > MonoBehaviour Script を選びます。
  2. 名前を BossHPBar にします。
  3. ダブルクリックして開き、中身を全部消して、次のコードを書きます。
csharp
using UnityEngine;

// ボスの体力バー(Fill の横の大きさを変える)
public class BossHPBar : MonoBehaviour
{
    public Enemy boss;
    public RectTransform fill;   // Pivot X を 0(左はし)にしておく

    void Update()
    {
        if (boss == null)
        {
            // ボスが倒されたらバーを消す
            gameObject.SetActive(false);
            return;
        }

        float rate = (float)boss.hp / boss.maxHp;
        fill.localScale = new Vector3(rate, 1f, 1f);
    }
}

保存します。

コードの解説

行意味
public Enemy boss;ボスの Enemy スクリプト。ここから hp と maxHp を読みます
public RectTransform fill;UI の位置や大きさを持つ部品。ふつうの Transform の UI 版です
if (boss == null)ボスが倒されて消えると null(何もない)になります。そのときはバーも消します
(float)boss.hp / boss.maxHp残りの体力の「割合」(1 なら満タン、0.5 なら半分)を求めます
fill.localScale = new Vector3(rate, 1f, 1f);横の大きさだけを割合に合わせます

よくあるまちがい:(float) を消してしまう

C# では、整数 ÷ 整数 の答えは整数になります(小数点以下は切り捨て)。たとえば 30 / 60 は 0.5 ではなく 0 です。 (float) を付けて小数にしてから割ると、正しく 0.5 になります。

4-4. BossHPBar をつなぐ ​

  1. Hierarchy で BossHPBar を選びます。
  2. Add Component で Boss HP Bar を追加します。
  3. Boss に、Hierarchy の Boss をドラッグします。
  4. Fill に、Hierarchy の Fill をドラッグします。
  5. シーンを保存します(Ctrl+S、Macは Cmd+S)。

5. テストのやり方 ​

5-1. ふつうの敵を一時的に止める ​

ボスだけをじっくりテストしたいので、ふつうの敵が出ないようにします。

  1. Hierarchy で EnemySpawner を選びます。
  2. Inspector の一番上、名前の左にあるチェックボックスを OFF にします。EnemySpawner が非アクティブ(はたらかない状態)になります。

ボスをすぐに出したいとき

Boss の Position の X は 14 にしてあります。もっと早く戦いたいときは、X を 9 くらいにすると、すぐに画面に入ってきます。テストが終わったら 14 に戻しましょう。

5-2. 動作確認 ​

  1. ▶ を押します。
  2. ボスが入ってきて、上の体力バーが満タンで表示されていればOKです。
  3. ボスに弾を当てると、体力バーが右から減っていけばOKです。
  4. 体力が0になると、ボスが爆発して消え、体力バーも消えればOKです。
  5. Console ウィンドウに BOSS DEFEATED と出ていればOKです。
  6. 左上のスコアが 10000 増えていればOKです。
  7. ▶ を押して止めます。

倒すのがたいへんなとき

テストのときだけ、Boss の Hp を 10 くらいにすると早く確認できます。終わったら 60 に戻しましょう。パワーアップしてから戦うのもおすすめです。

5-3. 元に戻す ​

  1. EnemySpawner のチェックボックスを ON に戻します。
  2. Boss の Position の X が 14、Hp が 60 になっているか確認します。
  3. シーンを保存します(Ctrl+S、Macは Cmd+S)。

先生へ

この時点では、ゲームを始めるとすぐにボスが登場します。次の第18回で「決まった時間に敵が出て、最後にボスが出る」流れに作り直すので、ここではこれで問題ありません。

チャレンジ課題 ​

  • FireSpread の角度を -45〜45 に広げたり、angle += 10 にして弾を増やしたりしてみましょう
  • 攻撃パターン3を作ってみましょう(ヒント:AttackLoop の while の中に、新しい for を足します。たとえば上下にまっすぐ撃つ、など)
  • ボスの体力が半分以下になったら、moveSpeed を速くしてみましょう(ヒント:GetComponent<Enemy>().hp で体力を読めます)
  • 体力バーの色を、体力が少なくなったら黄色に変えてみましょう(ヒント:fill.GetComponent<Image>().color。using UnityEngine.UI; が必要です)

つまずきポイント ​

こんなときはここを確認
ボスが入ってこずに、左へ通り過ぎてしまうEnemy の Speed が 0 になっているか
ボスが入ってこないBoss コンポーネントが付いているか。Position の X が 14 くらいか
弾を当ててもボスがへらない親の Boss に Rigidbody と Box Collider(Is Trigger ON)があるか。Collider の大きさがボスに合っているか
ボスが弾を撃たない/エラーが出るBullet Prefab と Muzzle がつながっているか
体力バーが減らない/一気に消える(float) を書いたか。Fill に Fill をつないだか
体力バーが両はしから縮むFill の Pivot X が 0 になっているか

まとめ ​

  • ボスは Enemy(体力・爆発・点数)と Boss(動き・攻撃)の2つのスクリプトを組み合わせて作りました
  • enum で状態に名前を付け、switch で状態ごとにやることを分けました
  • コルーチンで「撃つ → 待つ」を順番に書くと、攻撃パターンが作れます
  • 体力バーは、Pivot を左にした Fill の横の大きさを変えて表しました