ダークモード
第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 を作る
- Project ウィンドウで
Assets/_Game/Prefabsを開きます Enemy_Straightを右クリックします- Create > Prefab Variant を選びます
Enemy_Straight Variantというプレハブができるので、名前をEnemy_Waveに変えます
先生へ
Variant のアイコンは、ふつうのプレハブの青い箱に矢印のような印が付いたものになります。生徒に見分け方を確認させてください。
2-2. 色を変える
Assets/_Game/Materialsを開き、右クリック Create > Material で新しいマテリアルを作り、名前をEnemy_GreenにしますEnemy_Greenを選び、Inspector の Base Map の右の色をクリックして緑にします- Project ウィンドウで
Enemy_Waveをダブルクリックします(プレハブ編集モードになります) - Hierarchy で
Model(またはその子のメッシュ)を選びます - Inspector の Mesh Renderer の Materials を開き、Element 0 の右の欄に
Enemy_Greenをドラッグします - 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(入れる数) | 0 | 1.57 | 3.14 | 4.71 | 6.28 |
|---|---|---|---|---|---|
| Mathf.Sin(x) | 0 | 1(いちばん上) | 0 | -1(いちばん下) | 0(もとに戻る) |
x をどんどん変えていくと、値は「0 → 上 → 0 → 下 → 0 →…」とくり返します。これをそのまま 敵の高さ(y) に使えば、波のように上下する敵になります。
amplitude(振幅=しんぷく):波の高さ。Sin は -1〜1 なので、1.5 をかけると -1.5〜1.5 になるfrequency(周波数=しゅうはすう):波の細かさ。大きいほどギザギザ細かい波になる
2-4. EnemyWave スクリプト
Assets/_Game/Scriptsで右クリック Create > Scripting > MonoBehaviour Script を選び、名前をEnemyWaveにします- ダブルクリックして開き、次のように書きます
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;
}
}- 保存します(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 に付ける
- Project ウィンドウで
Enemy_Waveをダブルクリックしてプレハブ編集モードにします - Hierarchy でいちばん上の
Enemy_Wave(親)を選びます - Inspector の Add Component を押し、
EnemyWaveと入力して選びます - < を押してプレハブ編集モードを終わります
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 を書きかえる
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); // ★追加
} // ★追加
}
}
}- 保存します
コードの解説
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 で敵を登録する
- Hierarchy で
EnemySpawnerを選びます - Inspector を見ると、Enemy Prefabs という新しい欄ができています(前の Enemy Prefab の設定は消えています)
- Enemy Prefabs の右下の + を2回押して、要素を2つにします
- Element 0 に
Enemy_Straight、Element 1 にEnemy_Waveをドラッグします - Formation Count を
3にします - シーンを保存します(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 を作る
自機が「これは敵の弾だ」とわかるように、新しいタグを作ります。
- Hierarchy で何かオブジェクト(例:
Player)を選びます - Inspector のいちばん上の Tag のプルダウンを開き、Add Tag... を選びます
- Tags の一覧の右下の + を押します
- 名前に
EnemyBulletと入力して Save を押します
4-2. 弾の見た目と当たり判定
- Hierarchy で右クリック 3D Object > Sphere を選び、名前を
EnemyBulletにします - Position を (0, 0, 0)、Scale を (0.3, 0.3, 0.3) にします
Assets/_Game/Materialsで右クリック Create > Material、名前をEnemyBullet_Redにします- Base Map を赤にし、Emission にチェックを入れて、Emission Map の色も赤にします
EnemyBullet_Redを Scene ビューの Sphere にドラッグします- Hierarchy で
EnemyBulletを選び、Inspector の Tag をEnemyBulletにします - Sphere Collider の Is Trigger にチェックを入れます
- Add Component で
Rigidbodyを追加し、Use Gravity をオフ、Is Kinematic をオンにします
よくあるまちがい
Tag を EnemyBullet にし忘れると、弾が自機に当たっても何も起きません。Add Tag... でタグを作っただけでは付いていないので、もう一度オブジェクトを選んで Tag を選びなおしましょう。
4-3. EnemyBullet スクリプト
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);
}
}- 保存します
- Hierarchy の
EnemyBulletに、Add Component でEnemyBulletを付けます EnemyBulletを Hierarchy からAssets/_Game/Prefabsへドラッグしてプレハブにします- Hierarchy の
EnemyBulletは消します(右クリック Delete) - シーンを保存します
コードの解説
- 自機の
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 スクリプト
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));
}
}- 保存します
コードの解説
| 行 | 意味 |
|---|---|
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 を作る
- Project ウィンドウで
Enemy_Straightを右クリックし、Create > Prefab Variant を選びます - 名前を
Enemy_Shooterにします Enemy_Shooterをダブルクリックしてプレハブ編集モードにします- 親の
Enemy_Shooterを選び、Add Component でEnemyShooterを付けます - Bullet Prefab の欄に、Project ウィンドウの
EnemyBulletプレハブをドラッグします - Enemy の Speed を
2にします(撃つ敵は少しゆっくり) - 見分けやすいように、2-2 と同じやり方で別の色のマテリアル(例:
Enemy_Purple)を作って付けてもOKです - < を押してプレハブ編集モードを終わります
ヒント
Variant で変えた項目は、Inspector で 太字 になり、左に青い線が付きます。「元から変えたところ」が一目でわかります。
5-4. スポナーに登録する
- Hierarchy で
EnemySpawnerを選びます - Enemy Prefabs の + を押して要素を3つにし、Element 2 に
Enemy_Shooterをドラッグします - シーンを保存します
5-5. 動作確認
▶ Play ボタンを押して、Enemy_Shooter が赤い弾を自機に向けて撃ってくればOK。自機を動かすと、弾の向きも変わります。 (まだ弾に当たってもやられません。次の章で直します)
6. 敵の弾に当たったらやられる
6-1. PlayerLife を書きかえる
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;
}
}- 保存します
コードの解説
||は「または」という意味です。「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で向き(矢印)を角度にすると、相手をねらって撃てる- 配列を使うと、たくさんの種類をまとめて持ち、ランダムに選べる