【Unity/2D】Flappy Bird風ゲームを作ってみる話 #8 ゲームオーバー時の処理

記事をご覧いただき、誠にありがとうございます。

投稿主の無能です。

前回は、ゲームを一時中断する機能を実装しました。

今回は、ゲームオーバー時の処理についてやっていこうと思います。

ゲームオーバー時に行う処理を考える

まず、ゲームオーバーとなった時、つまりプレイヤーが土管と接触する、若しくはゲーム画面の上下に移動して見える範囲にいなくなった場合に、どのような処理を行うのかを考えます。

ゲームの画面外に移動する、という問題については、いわばゲーム画面外でズルができる(ゲーム画面外で土管を通過できる)という事になります。

これはDestroyAreaと土管の出現位置を調整することで、プレイヤーが土管とDestroyAreaの隙間を飛び越えられないように調整してやれば問題ありません。

残るは土管との接触時のプレイヤーの振る舞いですね。

土管と接触する=プレイヤーがやられたものと解釈すると、土管との接触の時点でプレイヤーは操作不能に陥ればいいわけです。

そしてそのままDestroyAreaに落下し、非アクティブになってくれます。

問題はまだあって、土管は際限なく生成されてしまいますよね。

ゲームの進行も、プレイヤーが非アクティブになっただけで、そのまま進行します。

そしてゲームを終了するのか、リトライするのかも分かりません。

…と、簡単なゲームでも色々と考える事が出てきます。

ゲームの進行に関しては、前回一時中断の処理を作ったので、それを上手く活用したいと思います。

ゲームの終了・リトライについては、ユーザー側で判断できるように、キー入力という形で終了・リトライを選択してもらう方法を採りたいと思います。

方針が決まったので、準備をしていきます。

ゲームオーバーのUIを作る

UI_GamePauseの複製

まずはゲームオーバーのUIを作ります。

一時中断と同じ手順で作るので、UI_GamePause以下を複製してリネームしてやれば簡単にできますね。

Ctrl+dキーでUI_GamePauseを複製します。

複製したら、子オブジェクトを含めてリネームしていきます。

複製して作成されたオブジェクト群

オブジェクト名をそれぞれリネーム

非アクティブの状態だと視認できないので、一度ゲームオーバーのUIをアクティブにします。

視認できないため一時的にUI_GameOverをアクティブにする

テキストの変更

テキストを「GameOver」にします。
この辺りは各自で調整してください。

テキストのRect Transform

テキストを変更

Gameビュー

さて、ここまで来ると不足しているものが何なのか、分かってくると思います。

まず、どのキーがゲームの終了でリトライなのかが不明です。
「〇キーを押してゲーム終了」「〇キーを押してゲームをリトライ」みたいな記述が無いです。

ですので、どのキーがゲーム終了・リトライを決めて、それをテキストでユーザーに分かるように表示させてあげる必要があります。

ここでは、ゲームの終了を「Quit」の単語の文頭からQキーとします。

ゲームのリトライは、プレイヤーをスペースキーで操作するので、同じくスペースキーにした方が分かりやすいと思います。

テキストの複製

ではText_GameOverを複製して、英語にして分かりやすく表示できるようにしましょう。

Text_GameOverを複製する

複製したテキストをリネームします。
ここでは「Text_RetryOrQuit」とします。

複製したテキストを「Text_RetryOrQuit」へリネーム

そしたら表示テキストの内容を変更します。
表示テキストは下へ下げます。

表示テキストの内容を変更し位置を下にする

表示テキストを変更して調整する

Gameビュー

ただ、このままだとユーザーがキーを押して処理を決めてくれるかは分かりません。

なのでRetryとQuitのボタンを作成して、表示テキストを呼んでいないユーザーでも、分かりやすいようなUIにしていきます。

ボタンの追加

ではUI > Button - TextMeshProでボタンを追加します。

UI > Button - TextMeshProでボタンを追加

追加したボタンはUI_GameOverと同じ階層になっています。

ドラッグしてUI_GameOverの子オブジェクトにします。

ドラッグしてUI_GameOverの子オブジェクトにする

そして名前を「Button_Retry」にリネームします。

「Button_Retry」にリネーム

これでUI_GameOverの子オブジェクトにボタンが追加できました。

ボタンの位置・サイズの調整

ボタンが追加できたので、ボタンの位置やサイズを決めます。

追加したテキストの下に配置しましょう。

ボタンの位置・サイズを変更

ボタンに表示されるテキストは、ボタンの子オブジェクトになります。

ボタンの子オブジェクトのテキストを「Text_Retry」にリネームします。

「Text_Retry」にリネーム

そしたら表示テキストを変更し、フォントサイズなどを調整します。

表示テキストの変更・フォントサイズなどを調整

これでリトライボタンが出来ました。

リトライボタンが出来た

ボタンの複製

ではリトライボタンを複製して、終了ボタンを作りましょう。

ボタンを複製してリネーム

終了ボタンの位置を変更

表示テキストを変更

終了ボタンが追加出来た

これでゲームオーバー時に表示するUIが出来たので、UI_GameOverオブジェクトを非アクティブにしておきます。

これでゲームオーバー時に表示するUIの準備が整いました。

スクリプトを書く

ではスクリプトを書いていきましょう。

一時中断と同じくGameManagerスクリプトに書いていきますが、処理の分岐が方々に及ぶため、処理の流れが追い辛くなるところもあります。

そういう所はなるべく無くしていくようにするので、お付き合い頂ければ幸いです。

まずは、プレイヤーを非アクティブにする処理を書いた、PipeControllerスクリプトを見ていきます。

PipeControllerスクリプトから見ていきます。

PipeController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEditor.Search;
using UnityEngine;

public class PipeController : MonoBehaviour
{
    #region Var-Pipe
    [Header("土管の動く速度")]
    [SerializeField] float pipeMoveSpeed = 1f;
    #endregion

    #region Var-Internal
    Rigidbody2D pipeRB;                             // 土管のRigidbody
    #endregion

    #region Start
    void Start()
    {
        // 土管のRigidbodyを取得
        pipeRB = GetComponent<Rigidbody2D>();
        // 土管を動かす
        PipeMove();       
    }
    #endregion

    #region PipeMove
    void PipeMove()
    {
        // 土管に左方向のベクトルを持たせる
        pipeRB.velocity = Vector2.left * pipeMoveSpeed;
    }
    #endregion

    #region OnCollisionEnter2D
    // 土管との接触判定
    // (Is Triggerが有効でないオブジェクト同士の場合)
    void OnCollisionEnter2D(Collision2D collision)
    {
        // 接触オブジェクトがプレイヤーの場合
        if (collision.gameObject.CompareTag(Variables.tag_Player))
        {
            // 土管との接触判定をtrue
            PlayerController.isCollided = true;
        }
    }
    #endregion
}
では見ていきましょう。

今回のスクリプトの追加で加えた点は、上記の色を変えている部分になります。
色を変えた部分以外は変更がありません。

OnCollisionEnter2D関数を追加し、プレイヤーが土管との接触した場合に、PlayerControllerに設けたフラグを切り替えるという処理になります。

今回の接触判定は、プレイヤー・土管双方にIs Triggerはチェックが無いので、OnCollisionEnter2Dで判定します。

このように、スクリプトを跨いで変数を扱う場合は、「○○.hensu(何処其処の変数名)」という形で、明確な場所と変数名を指定する必要があります。

これでプレイヤーがDestroyAreaに接触して非アクティブになると、PlayerControllerのいsCollidedという土管との接触判定フラグがtrueになります。

では、次にPlayerControllerを見ていきます。

PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    #region Var-Jump
    [Header("ジャンプ力")]
    [SerializeField] float jumpPower = 1f;
    #endregion

    #region Var-Internal
    Rigidbody2D playerRB;                               // プレイヤーのRigidbody2D
    public static bool isCollided = false;              // 土管との接触判定のフラグ
    #endregion

    #region Start
    void Start()
    {
        // プレイヤーのRigidbodyを取得
        playerRB = GetComponent<Rigidbody2D>();        
    }
    #endregion

    #region Update
    void Update()
    {
        // 接触フラグがtrueの場合
        if (isCollided)
        {
            // 処理を中断
            return;
        }

        // スペースキーが押された場合
        if (Input.GetKey(KeyCode.Space))
        {
            // ジャンプする
            Jump();
        }

        // 土管との接触判定がtrueの場合
        if (isCollided)
        {
            // 処理を中断
            return;
        }
    }
    #endregion

    #region Jump
    // ジャンプする
    void Jump()
    {
        // プレイヤーに上方向のベクトルを持たせる
        playerRB.velocity = Vector2.up * jumpPower;
    }
    #endregion
}
では見ていきましょう。

メンバ変数

メンバ変数を一つ追加しました。

先程PipeControllerスクリプトで呼び出した、土管との接触判定の役割であるboolのisCollidedです。

Update関数

Update関数に条件分岐を追加しました。

isCollidedがtrueの場合、処理を中断するというものです。

土管と接触すると、土管と接触したというフラグのisCollidedがtrueになります。

そしてスペースキーが押された場合の処理の前に追加したので、ジャンプの処理には未到達の状態で処理が中断されます。

これで土管と接触したらプレイヤーが操作不能になるわけです。

PlayerControllerスクリプトの追記は以上です。

次はGameManagerスクリプトを見ていきます。

GameManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    #region Var-GamePause
    [Header("一時中断のUI")]
    [SerializeField] GameObject UI_GamePause;
    #endregion

    #region Var-GameOver
    [Header("ゲームオーバーのUI")]
    [SerializeField] GameObject UI_GameOver;
    #endregion

    #region Var-Internal
    const float timeScaleReset = 1f;                                // TimeScaleのリセット用
    bool isPause = false;                                           // 一時中断のフラグ
    public static bool isGameOver = false;		      // ゲームオーバーのフラグ
    bool isRetry = false;					  // リトライのフラグ						// リトライのフラグ
    #endregion

    #region Update
    void Update()
    {
        // Escキーが押された場合
        if (Input.GetKeyDown(KeyCode.Escape))
        {
            // 一時中断のフラグがfalseの場合
            if (!isPause)
            {
                // ゲームを一時中断する
                GamePause();
                // 一時中断のUIをアクティブ
                UI_GamePause.SetActive(true);
                // 一時中断フラグをtrue
                isPause = true;
            }
            // それ以外の場合
            else
            {
                // 一時停止を解除する
                GamePauseRelease();
                // 一時中断のUIを非アクティブ
                UI_GamePause.SetActive(false);
                // 一時中断フラグをfalse
                isPause = false;
            }
        }

        // ゲームオーバーのフラグがtrueの場合
        if (isGameOver)
        {
            // ゲームオーバーの処理
            GameOver();
        }

        // リトライのフラグがtrueの場合
        if (isRetry)
        {
            if (Input.GetKey(KeyCode.Space))
            {
                // リトライする
                GameRetry();
            }
            // Escキーが押された場合
            else if (Input.GetKeyDown(KeyCode.Escape))
            {
                // ゲームを終了する
                GameQuit();
            }
        }
    }
    #endregion

    #region GamePause
    // ゲームを一時中断する
    void GamePause()
    {
        // タイムスケールを0にする
        Time.timeScale = Variables.zero;
    }
    #endregion

    #region GamePauseRelease
    // 一時停止の解除
    void GamePauseRelease()
    {
        // タイムスケールを戻す(1にする)
        Time.timeScale = timeScaleReset;
    }
    #endregion

    #region GameOver
    // ゲームオーバーの処理
    void GameOver()
    {
        // ゲームを一時中断の状態にする
        GamePause();
        // リトライのフラグをtrue
        isRetry = true;
        // ゲームオーバーのUIを表示
        UI_GameOver.SetActive(true);
    }
    #endregion

    #region GameRetry
    // ゲームのリトライ処理
    public void GameRetry()
    {
        // 一時中断を解除
        GamePauseRelease();
        // ゲームオーバーのフラグをfalse
        isGameOver = false;
        // リトライのフラグをfalse
        isRetry = false;
        // 接触判定の倉具をfalse
        PlayerController.isCollided = false;
        // ゲームオーバーのUIを非表示
        UI_GameOver.SetActive(false);
        // アクティブなシーンを再読み込み
        GameSceneReload();
    }
    #endregion

    #region GameSceneReload
    // ゲームシーンの再読み込み
    void GameSceneReload()
    {
        // ゲームのシーンを再読み込み
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
    #endregion

    #region GameQuit
    // ゲームの終了
    public void GameQuit()
    {
        // アプリケーションを終了する
        Application.Quit();
    }
    #endregion
}
では見ていきましょう。

名前空間

スクリプトの文頭に、using~という幾つかの文が書かれています。

これは名前空間(name space)と呼ばれているもので、要はお道具箱です。
usingはそのお道具箱の中身を使うよ、と宣言しているものになります。

そしてよく見ると、usingが一つ追加されています。

追加されたUnityEngine.SceneManagementは、シーンを扱うために必要なお道具箱です。

後程シーンを再読み込みする処理が出て来るので、そのための追加です。

メンバ変数

次はメンバ変数です。

先程追加したゲームオーバーのUIとなる、GameObjectのUI_GameOverです。

後は外部のスクリプトからアクセスが必要な変数としてboolのisGameOverです。

外部からのアクセスが必要な変数は、public staticを付けて変数を宣言する必要があります。

publicでアクセス範囲を拡大して、staticで静的な変数とすることで、外部からのアクセスに対して反応出来るようになります。

詳細はMicrosoft Learnを確認してください。

そして内部処理用としてboolのisRetryです。

Update関数

ではUpdate関数に追加した内容を見ていきます。

ゲームオーバーのフラグがtrueの場合の条件分岐を追加しています。

ゲームオーバーだったらGameOver関数を呼び出して、ゲームオーバーの処理をします。

次にリトライのフラグがtrueの場合の条件分岐が追加されています。

リトライの場合は、処理内にスペースキーが押された場合はGameRetry関数を呼び、Escキーが押された場合はGameQuit関数が呼ばれます。

これでUpdate関数は以上です。

GameOver関数

次は追加したGameOver関数です。

はじめにゲームを一時中断の状態にするため、GamePause関数を呼び出します。

次にリトライのフラグをtrueにします。

この関数の最後にゲームオーバーのUIを表示させます。

GameOver関数は以上です。

GameRetry関数

次はGameRetry関数です。

この関数のアクセス範囲はpublicになります。

まず一時中断の状態を解除するためGamePauseRelease関数を呼び出します。

次にゲームオーバーとリトライのフラグをfalseにします。

そしてPlayerControllerスクリプトのisCollidedをfalseにします。

後は表示していたゲームオーバーのUIを非アクティブにします。

最後にアクティブなシーン、つまり現在のシーンを再読み込みするGameSceneReload関数を呼び出します。

この関数は主にフラグとUI後処理と、シーンの再読み込みとなります。

GameSceneReload関数

次はGameSceneReload関数です。

この関数の処理はSceneManagerで指定したシーンを読み込む処理をしています。

SceneManager.GetActiveScene().nameで現在アクティブなシーンの名前を取得して、それをLoadSceneで読み込みます。

これで読み込んだシーンは現在のゲームのシーンなので、始めからになるわけですね。

GameQuit関数

最後にGameQuit関数です。

この関数のアクセス範囲もpublicになります。

アプリケーションの終了の処理が書かれています。

スクリプトの追記はこれで以上です。

では動作確認前の準備をしていきます。

動作確認前の準備

では追記した部分がUnityに反映されているので設定していきます。

GameManagerオブジェクトのInspectorビューで、ゲームオーバーのUIを設定します。

一時中断のUIと同じように、UI_GameOverパネルを設定します。

UI_GameOverパネルを設定

後は追加したボタンのOnClickに、スクリプトで追記した関数が呼び出せるように設定していきます。

まずリトライのボタンからです。

ボタンのInspectorビュー内のButtonコンポーネントを見てください。

下部にOn Click ()という項目があります。

ここにボタンが押された処理を追加します。

リストの追加方法は、右下の+ボタンを押します。
するとリストが追加されます。

右下の+ボタンを押す

リストが追加された

後は追加されたリストを設定します。

Noneになっているところは、ボタンを押した時に呼び出す関数が書かれたスクリプトアタッチしたオブジェクトを設定します。

この場合はリトライの処理を呼び出したいので、GameManagerスクリプトをアタッチしてあるGameManagerオブジェクトを設定します。

その設定の際に、別ウィンドウが表示されデフォルトだとAssetタブになっていますが、隣のSceneタブを選択してください。

Sceneタブ内にGameManagerがありますので、選択すると設定できます。

右側のオブジェクト選択ボタンを押す

Sceneタブを選択

GameManagerを選択

GameManagerが設定できた

GameManagerが設定できたら、No FunctionからGameManagerを辿り、GameRetry関数を辿って設定します。

No Function > GameManager > GameRetry関数を辿って設定

GameRetry関数が設定できた

publicした理由は、ここで外部のUnityからアクセスできるようにするためです。

publicではない関数を呼び出そうとしても、一覧には表示されないので、関数のアクセス範囲に注意してください。

同様の手順で、終了ボタンにGameQuit関数を設定します。

終了ボタンにGameQuit関数を設定

これで動作確認前の準備が出来ました。

動作確認

では動作確認をしていきます。

今回の確認事項は
  • プレイヤーが土管と接触すると操作不能になる
  • DestroyAreaに接触したらゲームオーバーのUIが表示される
  • スペースキーを押すとリトライされる
となります。

ゲームの終了は、Unityだと確認出来ないので、ビルドしてアプリになった時に確認します。

では確認しましょう。

土管と接触すると操作不能になった

プレイヤーは操作不能のまま落下し、DestroyAreaに接触
DestroyAreaに接触するとゲームオーバーのUIが表示された

スペースキーでシーンが再読み込みされた

一通り確認出来ましたね。

後は上のDestroyAreaに接触してもゲームオーバーのUIが表示されるか、ボタンを押してリトライも確認しておきましょう。

まとめ

今回は
  • ゲームオーバー時に処理する内容を考えた
  • ゲームオーバーのUIを作った
  • スクリプトにゲームオーバーの処理を追記した
という事をやりました。

次回は、背景が寂しいので背景を表示させたいと思います。

では、また次回!

コメント

  1. とても丁寧な解説と動画、本当にありがとうございます。

    ゲームオーバーのUIが表示されず詰まってしまって、解説文の内容から
    ObjectDestroyer.cs に

    GameManager.isGameOver = true;

    を追記してみたのですが、これで良かったでしょうか…?
    (一応ゲームオーバーのUIが表示され、リトライも可能でした)

    記事のどこかに書かれていましたら申し訳ありません…

    返信削除

コメントを投稿