ダークモード
第17回 ボス戦
この回でつくるもの:画面の右から大きなボスが登場し、上下に動きながら弾をばらまいてきます。画面の上には、ボスの体力バーが出ます。
この回で学ぶこと
- 大きな敵(ボス)を「親+子」の形で作る
enum(名前のついた選択肢)とswitchで「状態」を切り替える- コルーチンで攻撃パターンを順番にくり返す
- UI の Pivot(回転・拡大の中心)を使って、体力バーを左から減らす
前回までのふりかえり
第16回では、自機のあとをついてくるオプションを作りました。 カプセルを取ってゲージで OPTION を選ぶと、オプションが最大2つまで増え、一緒に弾を撃ちます。 これでパワーアップはすべてそろいました。今回は、いよいよステージの最後に待つボスを作ります。
1. ボスを作る
1-1. ボスの親オブジェクトを作る
ボスも、自機や敵と同じく「親(空のオブジェクト)+子(見た目のモデル)」の2段で作ります。
- Game シーンが開いていることを確認します。
- Hierarchy の何もないところを右クリックし、Create Empty を選びます。
- 名前を
Bossにします。 - Inspector の Transform で Position を (14, 0, 0) にします。
- Rotation が (0, 0, 0)、Scale が (1, 1, 1) であることを確認します。
なぜ x = 14?
画面の右はしは x = 約 8.9 です。x = 14 は画面の外なので、ボスは見えないところから入ってきます。
1-2. 見た目のモデルを付ける
- Project ウィンドウで
Assets/ThirdParty/KenneySpaceKitを開きます。 craft_minerを探して、Hierarchy のBossの上にドラッグします。Bossの子になります。- 子になったモデルの名前を
Modelにします。 Modelの Position を (0, 0, 0) にします。Modelの Rotation の Y を 90 または -90 にして、機首が 左(-X) を向くようにします。Modelの Scale を (3, 3, 3) くらいにします。Game 画面で見て、自機の2〜3倍くらいの大きさになればOKです。
モデルの名前がちがうとき
craft_miner が見つからないときは、プレビューを見て好きな大きめの機体を選んでOKです。
1-3. 弾が出る場所 Muzzle を作る
- Hierarchy で
Bossを右クリックし、Create Empty を選びます。Bossの子に空のオブジェクトができます。 - 名前を
Muzzleにします。 Muzzleの Position を (-1.5, 0, 0) にします。ボスの機首の先あたりにくればOKです。ずれていたら X を調整します。
1-4. タグと当たり判定を付ける
- Hierarchy で
Boss(親)を選びます。 - Inspector の一番上の Tag を Enemy にします。
- Add Component を押し、
Rigidbodyと入力して Rigidbody を追加します。 - Rigidbody の Use Gravity を OFF、Is Kinematic を ON にします。
- もう一度 Add Component を押し、Box Collider を追加します。
- Box Collider の Is Trigger を ON にします。
- 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 の値を入れる
- Hierarchy で
Bossを選びます。 - Add Component を押し、
Enemyと入力して Enemy を追加します。 - 次のように値を入れます。
| 項目 | 値 |
|---|---|
| Speed | 0 |
| Hp | 60 |
| Explosion Prefab | Explosion プレハブ |
| Explosion Sound | 爆発音(第9回で使ったもの) |
| Score | 10000 |
| Capsule Prefab | 空のまま(None) |
| Is Boss | ON(チェックを入れる) |
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 スクリプトを作る
- Project ウィンドウで
Assets/_Game/Scriptsを開きます。 - 何もないところを右クリックし、Create > Scripting > MonoBehaviour Script を選びます。
- 名前を
Bossにします(ファイル名とクラス名は同じにします)。 - ダブルクリックして開き、中身を全部消して、次のコードを書きます。
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 の値を入れる
- Hierarchy で
Bossを選びます。 - Add Component で Boss を追加します。
- Bullet Prefab に、Project の
Assets/_Game/Prefabs/EnemyBulletをドラッグします。 - Muzzle に、Hierarchy の
Bossの子のMuzzleをドラッグします。 - Enter X などはそのまま(6、2、3、1)でOKです。
- シーンを保存します(Ctrl+S、Macは Cmd+S)。
3. 攻撃パターン
3-1. コルーチンで順番に撃つ
AttackLoop は、第7回の EnemySpawner と同じ コルーチン(とちゅうで待てる関数)です。 上から順に、次のように動きます。
- 扇形に5発撃つ → 0.6秒待つ。これを3回
- 1秒休む
- 自機をねらって1発撃つ → 0.15秒待つ。これを8回
- 1秒休む
- 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. 動作確認
- ▶ を押します。
- 右からボスが入ってきて、x = 6 で止まり、上下に動き始めればOKです。
- 扇形の弾と、自機ねらいの連射が交互に来ればOKです。
- もう一度 ▶ を押して止めます。
4. ボスの体力バー
4-1. 体力バーの UI を作る
体力バーは「暗い背景」の上に「赤いバー(Fill)」を重ねて作ります。Fill の横の大きさを、体力に合わせて小さくします。
- Hierarchy の
Canvasを右クリックし、Create Empty を選びます。 - 名前を
BossHPBarにします。 - Inspector の Rect Transform の左上にある四角(Anchor Presets)をクリックし、top-center(上・真ん中)を選びます。
- Pos X を 0、Pos Y を -80、Width を 800、Height を 30 にします。
BossHPBarを右クリックし、UI > Image を選びます。名前をBackgroundにします。Backgroundの Anchor Presets を開き、Alt(Macは Option)を押しながら右下の stretch-stretch(全体に広げる)をクリックします。親と同じ大きさになります。Backgroundの Image の Color を暗い灰色(例: R 40, G 40, B 40)にします。BossHPBarを右クリックし、もう一度 UI > Image を選びます。名前をFillにします。Fillも、Alt(Macは Option)を押しながら stretch-stretch をクリックします。Fillの Image の Color を赤にします。Fillの Rect Transform の Pivot の X を 0 にします(Y は 0.5 のまま)。- 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 スクリプト
Assets/_Game/Scriptsで右クリックし、Create > Scripting > MonoBehaviour Script を選びます。- 名前を
BossHPBarにします。 - ダブルクリックして開き、中身を全部消して、次のコードを書きます。
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 をつなぐ
- Hierarchy で
BossHPBarを選びます。 - Add Component で Boss HP Bar を追加します。
- Boss に、Hierarchy の
Bossをドラッグします。 - Fill に、Hierarchy の
Fillをドラッグします。 - シーンを保存します(Ctrl+S、Macは Cmd+S)。
5. テストのやり方
5-1. ふつうの敵を一時的に止める
ボスだけをじっくりテストしたいので、ふつうの敵が出ないようにします。
- Hierarchy で
EnemySpawnerを選びます。 - Inspector の一番上、名前の左にあるチェックボックスを OFF にします。
EnemySpawnerが非アクティブ(はたらかない状態)になります。
ボスをすぐに出したいとき
Boss の Position の X は 14 にしてあります。もっと早く戦いたいときは、X を 9 くらいにすると、すぐに画面に入ってきます。テストが終わったら 14 に戻しましょう。
5-2. 動作確認
- ▶ を押します。
- ボスが入ってきて、上の体力バーが満タンで表示されていればOKです。
- ボスに弾を当てると、体力バーが右から減っていけばOKです。
- 体力が0になると、ボスが爆発して消え、体力バーも消えればOKです。
- Console ウィンドウに
BOSS DEFEATEDと出ていればOKです。 - 左上のスコアが 10000 増えていればOKです。
- ▶ を押して止めます。
倒すのがたいへんなとき
テストのときだけ、Boss の Hp を 10 くらいにすると早く確認できます。終わったら 60 に戻しましょう。パワーアップしてから戦うのもおすすめです。
5-3. 元に戻す
EnemySpawnerのチェックボックスを ON に戻します。Bossの Position の X が 14、Hp が 60 になっているか確認します。- シーンを保存します(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 の横の大きさを変えて表しました