ダークモード
第11回 残機とゲームオーバー
この回でつくるもの:自機が3機になり、やられても点滅しながら復活します。全部なくなると「GAME OVER」が出て、RETRY ボタンでやり直せます。
この回で学ぶこと
- 残機(のこりの自機の数)を GameManager で管理する
- コルーチンで自機を点滅させ、無敵時間を作る
SetActiveでオブジェクトを表示したり消したりする- UI の Panel と Button を作り、On Click () で関数を呼ぶ
SceneManager.LoadSceneでシーンを読みこみ直す
前回までのふりかえり
第10回で、画面の左上にスコアを表示しました。敵を倒すたびに GameManager の AddScore が呼ばれて点数がふえます。 でも今は、自機が1回ぶつかると消えてしまい、そこでおしまいです。今回は「残機」と「ゲームオーバー」を作ります。
1. 残機の文字を出す
1-1. LivesText を作る
スコアの文字をコピーして使います。
- Hierarchy で
Canvasの子のScoreTextを選ぶ - Ctrl+D(Macは Cmd+D)で複製する
- 複製したものの名前を
LivesTextに変える - Inspector の Rect Transform で、Pos Y を
-110にする(スコアの下になる) - Text Input を
LIFE 3に書きかえる - Ctrl+S(Macは Cmd+S)でシーンを保存する
2. GameManager に残機とゲームオーバーを追加する
2-1. GameManager を書きかえる
GameManager を開いて、次のように書きかえます。
csharp
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement; // ★追加
// スコア・残機・ゲームの状態を管理する
public class GameManager : MonoBehaviour
{
public static GameManager instance; // どこからでも使えるようにする
public int score = 0;
public TextMeshProUGUI scoreText;
public int lives = 3; // 残機(第11回) // ★追加
public TextMeshProUGUI livesText; // ★追加
public GameObject gameOverPanel; // ★追加
public bool isPlaying = true; // ゲーム中かどうか // ★追加
void Awake()
{
instance = this;
}
void Start()
{
UpdateUI();
}
public void AddScore(int amount)
{
score += amount;
UpdateUI();
}
public void LoseLife() // ★追加
{ // ★追加
lives--; // ★追加
UpdateUI(); // ★追加
// ★追加
if (lives <= 0) // ★追加
{ // ★追加
GameOver(); // ★追加
} // ★追加
} // ★追加
void GameOver() // ★追加
{ // ★追加
isPlaying = false; // ★追加
gameOverPanel.SetActive(true); // ★追加
} // ★追加
void UpdateUI()
{
scoreText.text = "SCORE " + score.ToString("D6");
livesText.text = "LIFE " + lives; // ★追加
}
// ボタンから呼ぶ
public void Retry() // ★追加
{ // ★追加
SceneManager.LoadScene(SceneManager.GetActiveScene().name); // ★追加
} // ★追加
}保存します。
コードの解説
| 行 | 意味 |
|---|---|
using UnityEngine.SceneManagement; | シーンを切りかえる SceneManager を使うための1行 |
public int lives = 3; | 残機。最初は3 |
public GameObject gameOverPanel; | ゲームオーバーのときに出す画面(この後で作る) |
public bool isPlaying = true; | ゲーム中なら true、ゲームオーバーになったら false。ほかのスクリプトが「まだ遊んでいる?」を確かめるのに使う |
lives--; | lives = lives - 1; と同じ。1へらす |
if (lives <= 0) | 残機が0以下になったらゲームオーバー |
gameOverPanel.SetActive(true); | かくしておいたゲームオーバー画面を表示する(次の tip) |
"LIFE " + lives | 文字と数字を + でつなぐと、数字も文字になってつながる |
SceneManager.GetActiveScene().name | 今のシーンの名前(Game) |
SceneManager.LoadScene(...) | その名前のシーンを最初から読みこみ直す。スコアも残機も敵も、全部最初の状態に戻る |
SetActive とは
SetActive(true) はオブジェクトを表示して動かす、SetActive(false) はかくして止める命令です。 Inspector の名前の左にあるチェックボックスを、スクリプトからオン・オフするのと同じです。 Destroy とちがって、消したわけではないので、あとでまた SetActive(true) で出せます。
3. PlayerLife を作り直す
3-1. どう変えるか
今の PlayerLife は、敵にぶつかると Destroy(gameObject) で自機を消しています。消してしまうと復活できません。 そこで次のように作り直します。
- ぶつかったら爆発して、GameManager に「残機をへらして」とお願いする(
Hit) - まだ残機があれば、スタート位置に戻して、2秒間点滅しながら無敵にする(
Respawn) - 残機がなければ、自機を
SetActive(false)でかくす
3-2. PlayerLife を書きかえる
PlayerLife を開いて、次のように書きかえます。Die は Hit に名前が変わり、中身もかなり変わります。
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"))
{
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; // ★追加
} // ★追加
}保存します。
コードの解説
| 行 | 意味 |
|---|---|
using System.Collections; | コルーチン(IEnumerator)を使うための1行。第7回の EnemySpawner でも書いた |
public GameObject model; | 点滅させる見た目(自機のモデル) |
bool isInvincible = false; | 無敵中かどうか |
if (isInvincible) return; | 無敵中なら、ここで関数を終わりにする(ぶつかっても何も起きない) |
GameManager.instance.LoseLife(); | GameManager に残機をへらしてもらう |
if (GameManager.instance.lives > 0) | へらしたあと、まだ残機があれば復活する |
StartCoroutine(Respawn()); | Respawn というコルーチンを始める |
gameObject.SetActive(false); | 残機がないときは自機をかくす。Destroy ではなくかくすだけにしている |
transform.position = new Vector3(-6f, 0f, 0f); | スタート位置(画面の左のほう)に戻す |
model.SetActive(!model.activeSelf); | 点滅のしかけ(下の説明) |
3-3. コルーチンで点滅させるしくみ
第7回で、コルーチンは「とちゅうで待つことができる関数」だと学びました。yield return new WaitForSeconds(0.1f); のところで 0.1 秒待ってから、続きを実行します。
点滅は、この3行のくり返しです。
csharp
model.SetActive(!model.activeSelf); // 見えていたら消す、消えていたら見せる
yield return new WaitForSeconds(0.1f); // 0.1秒待つ
time += 0.1f; // 待った時間を足していくmodel.activeSelfは「今、表示されているか」(trueかfalse)!は「反対にする」記号。!trueはfalse、!falseはtrue- つまり
SetActive(!model.activeSelf)は「今と反対にする」という意味です
0.1秒ごとに「見える → 消える → 見える…」を、time が invincibleTime(2秒)になるまでくり返します。 最後に model.SetActive(true); で必ず見える状態に戻してから、無敵を終わりにします。
ヒント
gameObject.SetActive(false) で Player 全体をかくすと、スクリプトも当たり判定も止まります。点滅のときは Player ではなく、見た目の Model だけをオン・オフしているので、自機は動かせるままです。
3-4. Model をつなぐ
- Hierarchy で
Playerの左の ▶ を開き、Bankの ▶ も開く Playerを選ぶ- Player Life コンポーネントの Model の欄に、Hierarchy の
Player > Bank > Modelをドラッグする - Invincible Time が
2になっていることを確認する - Ctrl+S(Macは Cmd+S)でシーンを保存する
4. ゲームオーバー画面を作る
4-1. Panel を作る
Panel(パネル)は、画面いっぱいに広がる四角い板です。ゲームオーバーの文字やボタンをのせる台にします。
- Hierarchy の
Canvasを右クリックし、UI > Panel を選ぶ - できた
Panelの名前をGameOverPanelに変える - Inspector の Image コンポーネントの Color をクリックする
- 色を黒にして、A(透明度)を
150くらいにする(うしろのゲームがうっすら見える)
4-2. GAME OVER の文字を作る
- Hierarchy の
GameOverPanelを右クリックし、UI > Text - TextMeshPro を選ぶ - 名前を
GameOverTextに変える - Rect Transform のアンカーが真ん中(middle-center)になっていることを確認する
- Pos X を
0、Pos Y を100にする - Width を
1200、Height を200にする - Text Input を
GAME OVERにする - Font Size を
120にする - Alignment の「中央よせ」と「上下の真ん中」のボタンを押す
4-3. RETRY ボタンを作る
- Hierarchy の
GameOverPanelを右クリックし、UI > Button - TextMeshPro を選ぶ - できた
Buttonの名前をRetryButtonに変える - Pos X を
0、Pos Y を-100にする - Width を
400、Height を100にする RetryButtonの ▶ を開き、子のText (TMP)を選ぶ- Text Input を
RETRYにして、Font Size を48にする
4-4. ボタンに Retry をつなぐ(On Click)
ボタンがクリックされたときに、GameManager の Retry 関数が呼ばれるようにします。
- Hierarchy で
RetryButtonを選ぶ - Inspector の Button コンポーネントの一番下にある On Click () を見る
- On Click () の右下の + を押す
- 増えた行の左下の None (Object) の欄に、Hierarchy の
GameManagerオブジェクトをドラッグする - 右側の No Function と書いてあるメニューをクリックする
- GameManager > Retry () を選ぶ
よくあるまちがい
None (Object) の欄には、Project の GameManager スクリプトではなく、Hierarchy の GameManager オブジェクトをドラッグします。 また、メニューに Retry が出ないときは、GameManager の Retry に public が付いているか確認しましょう。public な関数だけがボタンから呼べます。
4-5. GameManager につなぐ
- Hierarchy で
GameManagerを選ぶ - Game Manager コンポーネントの Lives Text に
LivesTextをドラッグする - Game Over Panel に
GameOverPanelをドラッグする - Lives が
3になっていることを確認する
4-6. ゲームオーバー画面をかくしておく
- Hierarchy で
GameOverPanelを選ぶ - Inspector の一番上、名前の左にあるチェックボックスを外す
- Game ビューから GAME OVER が消えたことを確認する
- Ctrl+S(Macは Cmd+S)でシーンを保存する
4-7. シーンが登録されているか確認する
SceneManager.LoadScene で読みこめるのは、Build Profiles の Scene List に登録されたシーンだけです。
- メニューの File > Build Profiles を開く
- 左の一覧で Scene List を選ぶ
- 右側のリストに
Scenes/Gameがあり、チェックが入っていることを確認する - なければ Add Open Scenes を押して追加する
- Build Profiles のウィンドウを閉じる
先生へ
テンプレートの SampleScene は最初から Scene List に入っているので、第1回で名前を Game に変えていれば、ふつうはそのまま登録されています。シーンの登録は第12回で改めて扱います。
5. ゲームオーバー中は敵を出さない
5-1. EnemySpawner を書きかえる
ゲームオーバーになっても敵が出つづけるのは変なので、止めましょう。EnemySpawner を開いて、次のように書きかえます。
csharp
using System.Collections;
using UnityEngine;
// 敵を一定時間ごとに出す
public class EnemySpawner : MonoBehaviour
{
public GameObject enemyPrefab; // 出す敵
public float interval = 1.5f; // 出す間隔(秒)
public float minY = -4f; // 出す高さの範囲
public float maxY = 4f;
void Start()
{
StartCoroutine(SpawnLoop());
}
IEnumerator SpawnLoop()
{
while (true)
{
yield return new WaitForSeconds(interval);
// ゲームが終わっていたら出さない(第11回) // ★追加
if (!GameManager.instance.isPlaying) continue; // ★追加
// ランダムに高さを決める
float y = Random.Range(minY, maxY);
Vector3 pos = new Vector3(transform.position.x, y, 0f);
Instantiate(enemyPrefab, pos, Quaternion.identity);
}
}
}保存します。
コードの解説
| 行 | 意味 |
|---|---|
GameManager.instance.isPlaying | GameManager の「ゲーム中かどうか」。ここでも static の掲示板が活やくする |
!GameManager.instance.isPlaying | ! で反対にするので「ゲーム中ではないなら」という意味 |
continue; | ループの残りを飛ばして、while の最初に戻る。つまり「今回は敵を出さずに、また待つ」 |
6. 動作確認
6-1. ためしてみる
- ▶ Play ボタンを押す
- わざと敵にぶつかる
- 爆発して「LIFE 2」になり、自機が左に戻って点滅すればOK
- 点滅しているあいだは、敵にぶつかっても何も起きないことを確かめる
- さらに2回ぶつかって「LIFE 0」にする
- 自機が消えて「GAME OVER」と RETRY ボタンが出て、新しい敵が出てこなくなればOK
- RETRY ボタンを押すと、スコア 000000・LIFE 3 から始まればOK
ヒント
テストのたびに3回ぶつかるのが大変なら、GameManager の Lives を Inspector で 1 にしてためしてもOKです。終わったら 3 に戻しましょう。
注意
RETRY のあと、画面が急に暗くなることがあります。これは Unity エディターの中だけで起きることがあり、ライトの情報が作られていないのが原因です。Window > Rendering > Lighting を開き、下の Generate Lighting を押すと直ります。
チャレンジ課題
- 最初の残機を5にしてみよう(ヒント: GameManager の Inspector で Lives を変える)
- 点滅を速くしたり遅くしたりしてみよう(ヒント:
0.1fを2か所とも変える) - 復活する位置を Inspector で変えられるようにしてみよう(ヒント:
public Vector3 respawnPosition = new Vector3(-6f, 0f, 0f);を作る) - GAME OVER の文字の色を赤にしたり、フチどりを付けたりしてみよう
つまずきポイント
| こんなときは | ここを確認 |
|---|---|
ぶつかったら NullReferenceException が出る | PlayerLife の Model、GameManager の Lives Text・Game Over Panel に入れ忘れがないか |
| 最初から GAME OVER が出ている | GameOverPanel のチェックボックスを外したか |
| 自機が点滅しない | Model に Player 自身を入れていないか。Player > Bank > Model を入れる |
| 1回ぶつかっただけで残機が2つへる | Player に Collider を2つ付けていないか。無敵の if (isInvincible) return; を書いたか |
| RETRY を押しても何も起きない | On Click () に GameManager.Retry が入っているか。シーンに EventSystem があるか |
| RETRY で「シーンが見つからない」エラー | File > Build Profiles の Scene List に Game が入っているか |
まとめ
- 残機は GameManager の
livesで管理し、LoseLifeでへらして 0 になったらGameOver - コルーチンと
SetActive(!model.activeSelf)で点滅させ、そのあいだは無敵にする - ボタンの On Click () に、オブジェクトと
publicな関数を登録するとクリックで呼べる SceneManager.LoadSceneでシーンを読みこみ直すと、ゲームを最初からやり直せる