ダークモード
第15回 パワーアップの中身
この回でつくるもの:ゲージで決定すると、本当に自機が速くなったり、ミサイル・ダブル・レーザー・シールドが使えるようになります。やられたら元に戻ります。
この回で学ぶこと
enum(列挙型)で「弾の種類」に名前を付ける- 関数に 引数(ひきすう)をわたして、同じ処理をまとめる
boolの変数でレーザーの「つらぬく/つらぬかない」を切りかえる- URP Lit マテリアルの Surface Type を Transparent にして半透明にする
- ほかのスクリプトの変数を書きかえて、機能をオン・オフする
前回までのふりかえり
前回はパワーアップカプセルとゲージを作りました。カプセルを取るとゲージの光るマスが右へ進み、X キーで決定すると Console に SPEED UP や LASER と表示されます。 今回は、その Debug.Log を本物のパワーアップに置きかえます。
1. enum とは
1-1. 弾の種類に名前を付ける
自機の弾には「ふつう」「ダブル」「レーザー」の3種類があります。これを数字で 0, 1, 2 と覚えると、あとで「2 って何だっけ?」となってしまいます。 そこで enum(イーナム、列挙型=れっきょがた)を使います。enum は 選べるものに名前を付けたリスト です。
csharp
public enum ShotType
{
Normal, // ふつうの弾
Double, // 2方向
Laser // レーザー
}| 数字で書くと | enum で書くと |
|---|---|
shotType = 2; | shotType = ShotType.Laser; |
| 2 が何か、覚えていないとわからない | 読むだけでわかる |
| 3 や 99 も入ってしまう | 3つのどれかしか入らない |
たとえ話
enum は、自動販売機のボタンのようなものです。「コーラ」「お茶」「水」のボタンしかなく、それ以外は選べません。
public の enum 型の変数は、Inspector では プルダウン で選べるようになります。
2. Laser と Missile のプレハブ
2-1. Laser を作る
- Hierarchy で右クリック 3D Object > Cube を選び、名前を
Laserにします - Position を (0, 0, 0)、Scale を (2, 0.1, 0.1) にします
Assets/_Game/Materialsで右クリック Create > Material、名前をLaser_Cyanにします- Base Map を水色にし、Emission にチェックを入れて Emission Map の色も水色にします
Laser_Cyanを Scene ビューのLaserにドラッグしますLaserを選び、Box Collider の Is Trigger にチェックを入れます- Add Component で
Rigidbodyを追加し、Use Gravity をオフ、Is Kinematic をオンにします - Add Component で
Bulletを追加し、Speed を 25、Life Time を 1 にします
(Pierce の項目は、4章で Bullet を書きかえてから設定します)
2-2. Missile を作る
- Hierarchy で右クリック 3D Object > Sphere を選び、名前を
Missileにします - Position を (0, 0, 0)、Scale を (0.5, 0.2, 0.2) にします
- 好きな色のマテリアル(例: 黄色の
Missile_Yellow)を作ってドラッグします - Sphere Collider の Is Trigger にチェックを入れます
- Add Component で
Rigidbodyを追加し、Use Gravity をオフ、Is Kinematic をオンにします - Add Component で
Bulletを追加し、Speed を 8、Damage を 2 にします
2-3. プレハブにする
LaserとMissileを、それぞれ Hierarchy からAssets/_Game/Prefabsへドラッグします- Hierarchy の
LaserとMissileは消します - シーンを保存します(Ctrl+S、Macは Cmd+S)
3. PlayerShooter を Fire(position) に整理する
3-1. 関数の引数とは
第16回では、オプション(分身)からも同じ弾を撃ちます。つまり「この場所から、今の装備で弾を出す」という処理を、何か所からも使いたくなります。 そこで、その処理を Fire という関数にまとめ、どこから撃つか を 引数(ひきすう)としてわたします。
Fire(muzzle.position) … 自機の機首から撃つ
Fire(option.position) … オプションの場所から撃つ(第16回)
↑
引数:関数にわたす「材料」csharp
void Fire(Vector3 position) // position という名前で受け取る
{
Instantiate(bulletPrefab, position, Quaternion.identity);
}たとえ話
引数は、お店の注文のようなものです。「たこ焼き(関数)を、8個(引数)ください」と言えば、作り方は同じで、数だけ変えてもらえます。
3-2. PlayerShooter を書きかえる
PlayerShooterを開き、次のように書きかえます
csharp
using UnityEngine;
using UnityEngine.InputSystem;
// 弾の種類(第15回) // ★追加
public enum ShotType // ★追加
{ // ★追加
Normal, // ★追加
Double, // ★追加
Laser // ★追加
} // ★追加
// 弾を撃つスクリプト
public class PlayerShooter : MonoBehaviour
{
public GameObject bulletPrefab; // ふつうの弾
public Transform muzzle; // 弾が出る場所
public float fireInterval = 0.15f; // 連射の間隔(秒)
public AudioClip shotSound; // 発射音(第9回)
public GameObject laserPrefab; // レーザー(第15回) // ★追加
public GameObject missilePrefab; // ミサイル(第15回) // ★追加
public ShotType shotType = ShotType.Normal; // ★追加
public bool hasMissile = false; // ★追加
float timer;
InputAction attackAction;
AudioSource audioSource;
void Start()
{
attackAction = InputSystem.actions.FindAction("Attack");
audioSource = GetComponent<AudioSource>();
}
void Update()
{
timer -= Time.deltaTime;
if (attackAction.IsPressed() && timer <= 0f)
{
Shoot();
timer = fireInterval;
}
}
void Shoot()
{
Fire(muzzle.position); // ★変更
if (shotSound != null)
{
audioSource.PlayOneShot(shotSound);
}
}
// 指定した場所から、今の装備に合わせて弾を出す // ★追加
void Fire(Vector3 position) // ★追加
{ // ★追加
if (shotType == ShotType.Laser) // ★追加
{ // ★追加
Instantiate(laserPrefab, position, Quaternion.identity); // ★追加
} // ★追加
else // ★追加
{ // ★追加
Instantiate(bulletPrefab, position, Quaternion.identity); // ★追加
if (shotType == ShotType.Double) // ★追加
{ // ★追加
// 斜め上(45度)にも撃つ // ★追加
Instantiate(bulletPrefab, position, Quaternion.Euler(0f, 0f, 45f)); // ★追加
} // ★追加
} // ★追加
if (hasMissile) // ★追加
{ // ★追加
// 斜め下(-45度)にミサイル // ★追加
Instantiate(missilePrefab, position, Quaternion.Euler(0f, 0f, -45f)); // ★追加
} // ★追加
} // ★追加
}- 保存します
コードの解説
| 部分 | 意味 |
|---|---|
public enum ShotType {...} | クラスの外に書くと、ほかのスクリプト(PowerUpManager)からも ShotType が使える |
Fire(muzzle.position) | 機首の位置を引数にして Fire を呼ぶ。もとの Instantiate は Fire の中へ引っこし |
shotType == ShotType.Laser | レーザーのときはレーザーだけ出す |
Quaternion.Euler(0f, 0f, 45f) | 45度回した弾。Bullet は自分の「右」へ進むので、斜め上に飛ぶ |
hasMissile | ミサイルを持っていれば、どの弾のときでも斜め下にミサイルも出す |
DOUBLE のとき MISSILE を持っているとき
/ 45度
自機 ──── 0度 自機 ────
\ -45度(ミサイル)3-3. Inspector でつなぐ
- Hierarchy で
Playerを選びます - PlayerShooter の Laser Prefab に
Laser、Missile Prefab にMissileのプレハブをドラッグします - シーンを保存します
3-4. 動作確認
- ▶ Play ボタンを押します
- Play 中に、Inspector の Shot Type を Double に変えて撃つと、2方向に弾が出ればOK
- Laser に変えるとレーザーが出ればOK
- Has Missile にチェックを入れると、斜め下にミサイルが出ればOK
- Play を止めます(変えた値は元に戻ります)
4. Bullet に pierce(つらぬく)を足す
4-1. Bullet を書きかえる
今のままだと、レーザーも敵に当たると消えてしまいます。レーザーは 敵をつらぬいて 進むようにしましょう。
Bulletを開き、次のように書きかえます
csharp
using UnityEngine;
// 自機の弾(ふつうの弾・レーザー・ミサイル共通)
public class Bullet : MonoBehaviour
{
public float speed = 15f; // 速さ
public float lifeTime = 2f; // 何秒で消えるか
public int damage = 1; // 敵に与えるダメージ(第8回)
public bool pierce = false; // 敵をつらぬくか(第15回 レーザー用) // ★追加
void Start()
{
Destroy(gameObject, lifeTime);
}
void Update()
{
// 自分の向いている「右」へ進む(回転させると斜めに飛ぶ)
transform.Translate(Vector3.right * speed * Time.deltaTime);
}
// 何かにぶつかったとき(第8回)
void OnTriggerEnter(Collider other)
{
Enemy enemy = other.GetComponent<Enemy>();
if (enemy != null)
{
enemy.TakeDamage(damage);
if (!pierce) // ★追加
{ // ★追加
Destroy(gameObject);
} // ★追加
}
}
}- 保存します
- Project ウィンドウで
Laserプレハブを選び、Bullet の Pierce にチェックを入れます
コードの解説
!は「〜ではない」という意味です。!pierceは「つらぬかないなら」pierceがfalse(ふつうの弾・ミサイル)なら、当たったら消えますpierceがtrue(レーザー)なら、消えずに次の敵へ進みます
5. PowerUpManager の Activate を実装する
5-1. PowerUpManager を書きかえる
PowerUpManagerを開き、次のように書きかえます
csharp
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
// グラディウス風パワーアップゲージ
public class PowerUpManager : MonoBehaviour
{
// ゲージのマス(0:SPEED 1:MISSILE 2:DOUBLE 3:LASER 4:OPTION 5:?)
public Image[] gaugeSlots;
public Color normalColor = new Color(0.2f, 0.2f, 0.5f);
public Color selectedColor = Color.yellow;
// パワーアップの中身(第15回) // ★追加
public PlayerController controller; // ★追加
public PlayerShooter shooter; // ★追加
public float speedUpAmount = 1.5f; // ★追加
public float maxSpeed = 12f; // ★追加
public GameObject shield; // ★追加
int cursor = -1; // 光っているマス(-1 はどこも光っていない)
float startSpeed; // ★追加
InputAction powerUpAction;
void Start()
{
powerUpAction = InputSystem.actions.FindAction("PowerUp");
startSpeed = controller.speed; // ★追加
UpdateGauge();
}
void Update()
{
if (powerUpAction.WasPressedThisFrame() && cursor >= 0)
{
Activate(cursor);
cursor = -1;
UpdateGauge();
}
}
// カプセルを取ったとき:光るマスを1つ右へ
public void AddCapsule()
{
cursor++;
if (cursor >= gaugeSlots.Length)
{
cursor = 0;
}
UpdateGauge();
}
void Activate(int index)
{
switch (index)
{
case 0: // SPEED UP
controller.speed = Mathf.Min(controller.speed + speedUpAmount, maxSpeed); // ★変更
break;
case 1: // MISSILE
shooter.hasMissile = true; // ★変更
break;
case 2: // DOUBLE
shooter.shotType = ShotType.Double; // ★変更
break;
case 3: // LASER
shooter.shotType = ShotType.Laser; // ★変更
break;
case 4: // OPTION
Debug.Log("OPTION");
break;
case 5: // ?(シールド)
shield.SetActive(true); // ★変更
break;
}
}
// シールドがあれば使う。使えたら true を返す // ★追加
public bool UseShield() // ★追加
{ // ★追加
if (shield.activeSelf) // ★追加
{ // ★追加
shield.SetActive(false); // ★追加
return true; // ★追加
} // ★追加
return false; // ★追加
} // ★追加
// やられたときに全部元に戻す // ★追加
public void ResetAll() // ★追加
{ // ★追加
controller.speed = startSpeed; // ★追加
shooter.shotType = ShotType.Normal; // ★追加
shooter.hasMissile = false; // ★追加
shield.SetActive(false); // ★追加
cursor = -1; // ★追加
UpdateGauge(); // ★追加
} // ★追加
void UpdateGauge()
{
for (int i = 0; i < gaugeSlots.Length; i++)
{
if (i == cursor)
{
gaugeSlots[i].color = selectedColor;
}
else
{
gaugeSlots[i].color = normalColor;
}
}
}
}- 保存します
コードの解説
| case | やっていること |
|---|---|
| 0 SPEED | Mathf.Min(A, B) は小さい方を返す。速くしすぎないよう、maxSpeed で止める |
| 1 MISSILE | PlayerShooter の hasMissile を true に |
| 2 DOUBLE | PlayerShooter の shotType を Double に(レーザーは外れる) |
| 3 LASER | shotType を Laser に(ダブルは外れる) |
| 4 OPTION | まだ Debug.Log のまま(第16回で作る) |
| 5 ? | シールドのオブジェクトを表示する |
startSpeed = controller.speed;… ゲーム開始のときの速さを覚えておき、やられたときにこの速さに戻しますUseShield()…bool(true か false)を 返す 関数です。シールドがあれば消して「使えたよ(true)」、なければ「使えなかった(false)」と答えます
| SPEED を何回取ったか | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| 速さ(speed) | 6 | 7.5 | 9 | 10.5 | 12 | 12(これ以上ふえない) |
6. シールドを作る
6-1. 半透明のマテリアル
Assets/_Game/Materialsで右クリック Create > Material、名前をShieldにします- Inspector の Surface Options の Surface Type を Opaque から Transparent に変えます
- Base Map の右の色をクリックし、色を水色にして、A(アルファ=不透明度)を 60 くらいに下げます
- お好みで Emission にチェックを入れ、うすい水色にしてもOKです
アルファとは
アルファ(A)は「どれくらい不透明か」です。255(または1)で完全に見える、0 で完全に透明です。Surface Type が Opaque(不透明)のままだと、アルファを下げても透けません。
6-2. Player の子に Shield を置く
- Hierarchy で
Playerを右クリックし、3D Object > Sphere を選びます - 名前を
Shieldにします - Position を (0, 0, 0)、Scale を (2, 2, 2) にします(自機がすっぽり入る大きさに調整)
- Sphere Collider の右の ⋮(3つの点)を押し、Remove Component を選んで消します
ShieldマテリアルをShieldオブジェクトにドラッグします- Inspector のいちばん上、名前の左のチェックを外して 非アクティブ にします
よくあるまちがい
Shield の Sphere Collider を消し忘れる と、Shield が自機の当たり判定を大きくしたり、カプセルや敵とおかしな当たり方をします。必ず消しましょう。
6-3. Inspector でつなぐ
- Hierarchy で
Playerを選びます - PowerUpManager の Controller の欄に、Hierarchy の
Playerをドラッグします(Player に付いている PlayerController が入ります) - Shooter の欄にも
Playerをドラッグします(PlayerShooter が入ります) - Shield の欄に、
Playerの子のShieldをドラッグします - シーンを保存します
7. やられたら元に戻る ResetAll
7-1. PlayerLife を書きかえる
PlayerLifeを開き、次のように書きかえます
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);
}
// シールドがあれば、シールドが代わりに消える(第15回) // ★追加
PowerUpManager powerUp = GetComponent<PowerUpManager>(); // ★追加
if (powerUp != null && powerUp.UseShield()) // ★追加
{ // ★追加
return; // ★追加
} // ★追加
Hit();
}
}
void Hit()
{
if (explosionPrefab != null)
{
Instantiate(explosionPrefab, transform.position, Quaternion.identity);
}
if (explosionSound != null)
{
AudioSource.PlayClipAtPoint(explosionSound, Camera.main.transform.position);
}
// パワーアップを元に戻す(第15回) // ★追加
PowerUpManager powerUp = GetComponent<PowerUpManager>(); // ★追加
if (powerUp != null) // ★追加
{ // ★追加
powerUp.ResetAll(); // ★追加
} // ★追加
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;
}
}- 保存します
コードの解説
powerUp.UseShield()が true(シールドが代わりに消えた)なら、returnで Hit() をしない で終わります&&は「かつ」です。powerUp != nullを先に調べているので、PowerUpManager が付いていなくてもエラーになりませんHit()の中でResetAll()を呼ぶので、やられるとスピード・弾・ミサイル・シールド・ゲージが全部元に戻ります
敵や敵の弾に当たった
│
├─ 無敵中? ── はい → 何もしない
│
├─ シールドある? ── はい → シールドが消えるだけ(セーフ!)
│
└─ Hit() → 爆発 → ResetAll() → 残機 -1 → 復活7-2. 動作確認
▶ Play ボタンを押して、次のようになればOK。
- SPEED を決定すると、自機が速くなる
- MISSILE を決定すると、斜め下にミサイルが出る
- DOUBLE を決定すると、2方向に撃てる。そのあと LASER を決定すると、レーザーに変わる
- レーザーが敵を何機もつらぬく
- ? を決定すると、自機のまわりに半透明のシールドが出る。敵の弾に1回当たるとシールドだけ消える
- シールドがないときにやられると、全部ふつうに戻る
テストのコツ
? までカプセルを6個集めるのは大変です。テストのときは、Play 中に Hierarchy で Player の子の Shield を選び、Inspector の名前の左のチェックを入れて手でアクティブにしても確かめられます。
チャレンジ課題
- Speed Up Amount や Max Speed を変えて、ちょうどよいスピードを見つけよう
- レーザーの長さ(Laser プレハブの Scale X)や速さを変えてみよう
- シールドを「3回まで耐える」ようにしてみよう(ヒント:
int shieldCountを作り、UseShieldで1ずつ減らす) - 同じマスは2回選べないように、決定したマスの文字を消してみよう(ヒント: 本物のグラディウスでは、もう選べないマスは空白になります)
つまずきポイント
| こんなときは | ここを確認 |
|---|---|
Play した瞬間に NullReferenceException(PowerUpManager の Start) | Controller と Shooter に Player を入れたか |
| ? を決定したりやられたりするとエラー | Shield に Shield オブジェクトを入れたか |
| シールドが透けない | マテリアルの Surface Type が Transparent か。Base Map の A を下げたか |
| レーザーが1機で消える | Laser プレハブの Pierce にチェックが入っているか |
| レーザーやミサイルを選ぶとエラー | PlayerShooter の Laser Prefab / Missile Prefab にプレハブが入っているか |
ShotType が見つからないというエラー | enum ShotType を PlayerShooter.cs の クラスの外(上)に書いたか。つづり |
まとめ
enumで選べるものに名前を付けると、コードが読みやすくまちがいにくくなる- 関数に引数をわたすと、「場所だけちがう同じ処理」を1つにまとめられる
- PowerUpManager が、PlayerController や PlayerShooter の変数を書きかえてパワーアップさせている
- やられたら
ResetAll()で全部元に戻る