【Unity/2D】スイカゲーム風の落ち物ゲームを作ってみる話 #7 ゲームオーバー処理を作る

記事をご覧いただき、誠にありがとうございます。
投稿主の無能です。

前回は、果物の進化の機能を実装しました。

今回はゲームオーバーの処理を作っていきたいと思います。

ゲームオーバーの処理

どんな時にゲームオーバーの処理をすればいいのか考えてみましょう。

容器の上部まで果物が積みあがって、一定のラインを超えたらゲームオーバーで良さそうです。

後は果物が容器の外に零れ落ちてしまっても駄目なので、この場合もゲームオーバーにします。

ゲームオーバーにするタイミングですが、容器の外に零れ落ちた時は、即座にゲームオーバーにします。

一定のラインを超えた場合は、即座にゲームオーバーにするのではなく、果物が進化したり崩れる事も考えて、少し待ってからゲームオーバーにするのが良さそうです。
少し待機する時間も、作り手側で設定できると良さそうです。

そうすると、果物にただ落下する重力だけではなく、何らかの動きを加えた方が良さそうです。

では見当も付いたので、果物に何らかの動きを加えるところからやっていきます。

Physics Material2Dをアタッチする

果物の基になるFruitsBaseに、物理シミュレーションを付けるためにPhysics Material2Dを作成してアタッチします。

Assetsフォルダを選択し、新たに「Materials」フォルダを作成します。

「Materials」フォルダを作成

作成したMaterialsフォルダの中に入って、メニュー > 2D > Physics Material 2DでPhysics Material2Dを作成し、名前を「Fruits」にします。

メニュー > 2D > Physics Material 2D

「Fruits」というPhysics Material2Dを作成

このFruitsのPhysics Material2DのInspectorビューを見てみます。

FruitsのInspectorビュー

この項目の説明はドキュメントで確認します。
ドキュメントがUnity 6以前のため、一部は3Dのドキュメントになりますが、そこはご容赦ください。



念の為に3Dとの違いを言っておくと、2DはPhysics Material2Dで、3DはPhysic Material
というように、複数形の「s」が2Dには付いて3Dはありません。

Frictionは、コライダーの摩擦係数なので、摩擦係数が高いほど接触オブジェクトが滑りにくくなります。
逆にFrictionが0に近いほど滑りやすくなります。

寒い冬の道路のアイスバーンを想像すると分かりやすいと思います。
普通のアスファルトの状態を1とすると、アイスバーンの道路は0.3とか0.4みたいな感じです。
0だと摩擦係数が無くなるので、滑りだしたら止まれなくなりますね。

Bouncinessは、衝突したときの弾む強さを表します。
0だと全く弾まず、1だと衝突した時の弾む力がそのまま返ってきます。

鉄球と良く弾むスーパーボールの違いを想像すると分かりやすいです。

Unity 6では、コライダーの表面値の組み合わせ方法として、Friction CombineとBounce Combineが追加されました。

これは二つのコライダーが接触した場合に、どのようにふるまう(計算)かを設定するものです。
  • Average:二つの値を合わせて2で割った値(平均値)を使う
  • Mean:? ※2024/11月時点でドキュメントが見付からないため不明(和訳すると平均なので、推測するとAverageに近いふるまいかも?)
  • Multiply:一方の値をもう一方の値で乗算した値を使う
  • Minimum:二つの値のうち小さい方の値を使う
  • Maximum:二つの値のうち大きい方の値を使う
現実世界のふるまいはAverageに近いものになります。

スーパーボールで例えると、地面が0、スーパーボールが1でふるまいをAverageとした場合、スーパーボールが地面に衝突した際にその衝突の運動量×0.5(地面0とスーパーボール1の平均)が次に衝突した時の運動量になります。
つまりスーパーボールは落下した時の半分だけ跳ね返るようになります。
そのまま再度落下した時は、衝突の運動量×0.5になるので、運動量が前回よりも少なくなります。
そして跳ね返る運動量が0になると跳ね返ってこなくなります。

地面0・スーパーボール1でふるまいをAverageに設定した場合

何となくで構わないのでイメージを掴んでみてください。

ではPhysics Material2DのFrictionを0、Bouncinessを0.1、Friction CombineをMinimum、Bounce CombineをAverageに設定します。

Frictionを0、Bouncinessを0.1、Friction CombineをMinimum、Bounce CombineをAverageに設定

そしてFruitsBaseのRigidbody2DとCircle Collider2DのMaterialに作成したPhysics Material2DのFruitsを設定します。

FruitsBaseのRigidbody2DとCircle Collider2DのMaterialにPhysics Material2DのFruitsを設定

これで容器や他の果物と衝突した時に、ちょっとだけ弾むような動きをするようになります。

もし本家のスイカゲームのように着地したら回転させたい場合は、Frictionを0、Friction CombineをMinimumにして、摩擦が影響しない状態で回転させればいいと思います。

Physics Material2DのFruitsは自由に設定してください。

ゲームオーバーの準備

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

次にゲームオーバーの線を作ります。

メニュー > 2D Object > Sprite > Squareで四角を作り、名前を「GameOverLine」にします。

「GameOverLine」というSquareを作成

GameOverLineを細い長方形にして、色を黄色にし、容器の上部に配置します。

細い長方形にして容器の上部に配置

色を黄色にする

Gameビュー

そしてポインターと同様に果物の表示を手前にするため、Order in Layerを-1にします。

Sprite RendererのOrder in Layerを-1にする

そしてBox Collider2Dをアタッチし、Is Triggerにチェックを入れます。

Box Collider2Dをアタッチし、Is Triggerにチェックを入れる

これでゲームオーバーの線ができました。

容器の外に落下した場合

容器の外に落下した場合は、果物に重力がついているので、下に落ちていきます。
容器の中に落下した時は、容器が受け止めているので、容器の底以下のY座標にはなり得ません。

ということはつまり、果物が容器の底のY座標より下になった場合にゲームオーバーになるよう、スクリプトで処理してやればいいです。

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

次にゲームオーバーになった時のUIを作成します。

メニュー > UI > CanvasでCanvasを作成し、名前を「UIs」とします。

メニュー > UI > Canvasで「UIs」という名前のCanvasを作成

Canvasを作成

この時、初めてUI関連のオブジェクトを作成したので、EventSystemというものが同時に作成されます。

このEventSystemはボタンなどのコンポーネントが操作された時に、スクリプトへの橋渡しをしてくれるので、削除したり非アクティブにしたりしないよう注意してください。

もし削除や非アクティブにしていると、UIからの入力は受け付けられなくなります。

次にメニュー > UI >Panelで「UI_GameOver」というPanelを作成します。
作成したUI_GameOverはUIsの子オブジェクトにします。

メニュー > UI >Panelで「UI_GameOver」というPanelを作成

UIsの子オブジェクトにする

次にUI_GameOverのアルファ値(透明度)を0にします。

アルファ値(透明度)を0にする

次にメニュー > UI > Text - TextMeshProで「Text_GameOver」というテキストを作成します。
Text_GameOverを作成したらUI_GameOverの子オブジェクトにします。

メニュー > UI > Text - TextMeshProで「Text_GameOver」というテキストを作成

Text - TextMeshProの初回作成時は、TextMeshProを使うための準備が必要なため、別ウィンドウが開きます。
必要なのは上のImport TMP Essentialsだけになるので、上のものをインストールしたらウィンドウを閉じてください。

Import TMP Essentialsを押してTextMeshProを使う準備をする

この状態になったらウィンドウを閉じる

Text_GameOverをUI_GameOverの子オブジェクトにする

テキストを編集してGame Overと表示します。
位置は真ん中あたりにします。

テキストの位置

表示するテキスト

フォントサイズ、配置

Gameビュー

ここまで出来たら、UI_GameOverを非アクティブにします。

UI_GameOverを非アクティブにする

では下準備ができたので、スクリプトを編集します。

スクリプトを編集

スクリプトの新規作成

今回はゲームオーバーの処理を実装するため、新たにGameManagerを作成します。

メニュー > Create Emptyで空のオブジェクトを作成し、名前を「GameManager」とします。

メニュー > Create Emptyで「GameManager」を作成

次に新たにスクリプトをScriptsフォルダに作成します。

二つスクリプトを新規作成するのですが、まずGameOverLineにアタッチするGameOverスクリプトを作成します。

GameOverスクリプトを作成

GameOverLineにGameOverスクリプトをアタッチ

次にGameManagerにGameManagerスクリプトを作成してアタッチします。

GameManagerスクリプトを作成

GameManagerにGameManagerスクリプトをアタッチ

ではスクリプトが準備できたので編集していきます。

Variables.cs

using UnityEngine;

public class Variables : MonoBehaviour
{
    #region Var-Number
    public const float zero = 0f;                               // floatの0
    public const float half = 2f;                               // floatの2
    #endregion

    #region Var-String
    public const string tag_Fruits = "Fruits",                  // タグ:果物
                        tag_Bottom = "Bottom";                  // タグ:容器の底              
    #endregion
}

容器の底に付けるタグの定数の文字列を追加しました。

次は果物のFruitsを編集します。

Fruits.cs

using UnityEngine;

public class Fruits : MonoBehaviour
{
    #region Var-Fruits
    [Header("果物の種類")]
    [SerializeField] type fruitsType;
    #endregion

    #region Var-Evolution
    [Header("進化する果物")]
    [SerializeField] Fruits evolutionFruits;
    #endregion

    #region Var-Internal
    public static int number = 0;                                           // 果物の基礎の通し番号
    int fruitsNumber = 0;                                                   // 果物の個別の通し番号
    BoxCollider2D border;                                                   // 容器の底のBoxCollider2D
    enum type                                                               // 果物の種類
    {
        cherry = 1, strawberry, grape, orange, persimmon, apple,
        pear, peach, pineapple, melon, watermelon
    }
    #endregion

    #region Start
    void Start()
    {
        // 果物に個別の通し番号を付ける
        fruitsNumber = number;
        // 基礎の通し番号を加算
        number++;
        // 容器の底のBoxCollider2Dを取得
        border = GameObject.FindWithTag(Variables.tag_Bottom).GetComponent<BoxCollider2D>();
    }
    #endregion

    #region Update
    void Update()
    {
        // 容器の外に果物が落下した時の処理
        FallOutContainer();
    }
    #endregion

    #region OnCollisionEnter2D
    // 果物の接触判定
    void OnCollisionEnter2D(Collision2D collision)
    {
        // 接触オブジェクトのタグが果物ではない場合
        if (!collision.gameObject.CompareTag(Variables.tag_Fruits))
        {
            // 処理を中断
            return;
        }
        // 接触オブジェクトと自身のFruitsを取得
        Fruits _collisionFruits = collision.gameObject.GetComponent<Fruits>(),
               _fruits = GetComponent<Fruits>();
        // 接触した果物と自身の果物の種類が同じ場合
        if (_collisionFruits.fruitsType == _fruits.fruitsType)
        {
            // 接触した果物の通し番号より自身の果物の通し番号が大きい場合
            if (_collisionFruits.fruitsNumber < _fruits.fruitsNumber)
            {
                // 進化する果物を生成
                Evolution(collision);
            }
            // 接触した果物を破棄
            Destroy(collision.gameObject);
            // 自身を破棄
            Destroy(gameObject);
        }
    }
    #endregion

    #region Evolution
    // 果物の進化
    void Evolution(Collision2D collision)
    {
        // 進化する果物がnullの場合
        if (evolutionFruits == null)
        {
            // 処理を中断
            return;
        }
        // 接触した果物と自身の果物の中心を結んだ中心位置を算出
        Vector2 _center = (collision.transform.position + transform.position) / Variables.half;
        // 進化する果物を生成 生成物:進化する果物 位置:算出した中心位置 回転無し
        Instantiate(evolutionFruits, _center, Quaternion.identity);
    }
    #endregion

    #region FallOutContainer
    // 果物が容器の外に落下した時の処理
    void FallOutContainer()
    {
        // 容器の底の高さよりも果物の高さが下になった場合
        if (border.transform.position.y > gameObject.transform.position.y)
        {
            // ゲームマネージャーのゲームオーバーのフラグをtrue
            GameManager.isGameOver = true;
        }
    }
    #endregion
}

では追加したメンバ変数から見ていきます。

メンバ変数

追加したメンバ変数は、容器の底のBox Collider2Dのborderになります。
容器の底のY座標を取得するため、アタッチしたBox Collider2Dコンポーネントを指定しています。

Start関数

Start関数では、容器の底のborderを取得しています。
容器の底が取得できればいいので、ここをSprite Rendererにしても問題ありません。

Update関数

Update関数では、容器の外に果物が落下した場合のFallOutContainer関数を呼んでいます。

FallOutContainer関数

FallOutContainer関数では、容器の底のY座標よりも自身のY座標が下になった場合に、ゲームマネージャーのゲームオーバーのフラグをtrueにします。

次にPointerControllerを編集します。

PointerController.cs

using UnityEngine;

public class PointerController : MonoBehaviour
{
    #region Var-Container
    [Header("容器の壁:左")]
    [SerializeField] GameObject container_Left;
    [Header("容器の壁:右")]
    [SerializeField] GameObject container_Right;
    [Header("容器の壁との余白")]
    [SerializeField] float marginX = 0.1f;
    #endregion

    #region Var-Pointer
    [Header("ポインターの移動速度")]
    [SerializeField] float moveSpeed = 1f;
    #endregion

    #region Var-Internal
    Rigidbody2D pointerRB;                                                  // 落下する地点のRigidbody2D
    Vector2 moveDirection;                                                  // ポインターの移動ベクトル
    float movableRange_Left, movableRange_Right;                            // ポインターの移動可能範囲
    #endregion

    #region Start
    void Start()
    {
        // ポインターのRigidbody2Dを取得
        pointerRB = GetComponent<Rigidbody2D>();
        // 移動可能範囲を取得
        movableRange_Left = container_Left.transform.position.x + marginX;         // 左
        movableRange_Right = container_Right.transform.position.x - marginX;       // 右
    }
    #endregion

    #region Update
    void Update()
    {
        // ゲームマネージャーのゲームオーバーのフラグがtrueの場合
        if (GameManager.isGameOver)
        {
            // ポインターの移動ベクトルを0にする
            pointerRB.linearVelocity = Vector2.zero;
            // 処理を中断
            return;
        }
        
        // 移動の受付処理
        InputProcess();
        // ポインターの移動
        PointerMove();
    }
    #endregion

    #region InputProcess
    // 移動の受付処理
    void InputProcess()
    {
        // 水平の入力を取得
        float horizontalInput = Input.GetAxis("Horizontal");
        // ポインターの移動ベクトルを作成
        moveDirection = new Vector2(horizontalInput, Variables.zero);
    }
    #endregion

    #region PointerMove
    // ポインターの移動
    void PointerMove()
    {
        // ポインターの移動範囲を制限
        MoveClamp();
        // ポインターを移動
        pointerRB.linearVelocity = moveDirection * moveSpeed;
    }
    #endregion

    #region MoveClamp
    // 移動範囲の制限
    void MoveClamp()
    {
        // 横の移動範囲を制限
        float pointerX = Mathf.Clamp(gameObject.transform.position.x, movableRange_Left, movableRange_Right);
        // ポインターの位置を取得した位置にする
        gameObject.transform.position = new Vector2(pointerX, gameObject.transform.position.y);
    }
    #endregion

}
メンバ変数に特に追加は無いので、処理を見ていきます。

Update関数

Update関数では、ゲームマネージャーのゲームオーバーのフラグがtrueの場合、ポインターの移動ベクトルを0にして、処理を中断します。

次にGameOverを編集します。

GameOver.cs

using UnityEngine;

public class GameOver : MonoBehaviour
{
    #region Var-GameOver
    [Header("ゲームオーバーの線")]
    [SerializeField] GameObject gameOverLine;
    [Header("ゲームオーバーになる時間の閾値")]
    [SerializeField] float gameOverTime = 3f;
    #endregion

    #region Var-Internal
    float count = 0;
    #endregion

    #region OnTriggerStay2D
    // ゲームオーバーの線に接触し続ける時の処理
    void OnTriggerStay2D(Collider2D collision)
    {
        // 接触し続けるオブジェクトのタグが果物の場合
        if (collision.gameObject.CompareTag(Variables.tag_Fruits))
        {
            // ゲームオーバーのカウント開始
            count += Time.deltaTime;
            // ゲームオーバーのカウントが閾値を超えた場合
            if (count > gameOverTime)
            {
                // ゲームマネージャーのゲームオーバーのフラグをtrue
                GameManager.isGameOver = true;
                // ゲームオーバーのカウントを0にする
                count = Variables.zero;
            }
        }        
    }
    #endregion

    #region OnTriggerExit2D
    // ゲームオーバーの線から離れた時の処理
    private void OnTriggerExit2D(Collider2D collision)
    {
        // ゲームオーバーのカウントを0にする
        count = Variables.zero;
    }
    #endregion
}

ではメンバ変数から見ていきます。

メンバ変数

まずゲームオーバーの線となるGameObjectのgameOverLineになります。

次にゲームオーバーとなる時間の閾値となるfloatのgameOverTimeになります。

後は内部処理用の内部カウント用のfloatのcountになります。

では処理内容を見ていきます。

OnTriggerStay2D関数

ゲームオーバーの線と接触し続けた時の処理となるOnTriggerStay2D関数になります。

接触の段階は三段階に分かれて、
  • Enter:接触が始まった時
  • Stay:接触し続ける時
  • Exit:接触状態から離れた時
に分かれます。

今回は接触し続ける状態の時に処理をしたいので、OnTriggerStay2Dとなります。

接触し続けるオブジェクトのタグが果物の場合、ゲームオーバー用の内部カウントを始めます。

そしてゲームオーバーの内部カウントがゲームオーバーになる時間の閾値のgameOverTimeを超えた場合、ゲームマネージャーのゲームオーバーのフラグをtrueにします。

そしてゲームオーバーの内部カウントを0に戻します。

OnTriggerExit2D関数

今度はゲームオーバーの線から離れた時のOnTriggerExit2D関数になります。

接触オブジェクトがゲームオーバーの線から離れた時は、ゲームオーバーの内部カウントを0に戻します。

最後にGameManagerを編集します。

GameManager.cs

using UnityEngine;

public class GameManager : MonoBehaviour
{
    #region Var-GameOverUI
    [Header("ゲームオーバーのUI")]
    [SerializeField] GameObject UI_GameOver;
    #endregion

    #region Var-Internal
    public static bool isGameOver = false;                                      // ゲームオーバーのフラグ
    #endregion

    #region Update
    void Update()
    {
        // ゲームオーバーのフラグがtrueだった場合
        if (isGameOver)
        {
            // ゲームオーバーのUIが非アクティブの場合
            if(!UI_GameOver.activeInHierarchy)
            {
                // ゲームオーバー処理
                GameOver();
            }
        }
    }
    #endregion

    #region GameOver
    // ゲームオーバー
    void GameOver()
    {
        // ゲームオーバーのUIをアクティブ
        UI_GameOver.SetActive(true);
    }
    #endregion
}

ではメンバ変数から見ていきましょう。

メンバ変数

メンバ変数はゲームオーバーのUIとなるGameObjectのUI_GameOverです。

それから内部処理用のゲームオーバーのフラグとなるboolのisGameOverです。

それでは処理を見ていきましょう。

Update関数

Update関数では、ゲームオーバーのフラグがtrueだった場合の条件分岐がはじめにあり、さらにその中でゲームオーバーのUIが非アクティブの場合、ゲームオーバー処理のGameOver関数を呼んでいます。

GameOver関数

GameOver関数ではゲームオーバーのUIをアクティブにします。

ではスクリプトが編集できたので、Unityの作業に移ります。

Unityでの作業

まずゲームマネージャーからです。

ゲームオーバーのUIという項目が追加されているので、非アクティブのUI_GameOverを設定します。

ゲームオーバーのUIを設定

次に容器の底のContainer_Bottomに新たに「Bottom」というタグを作成して設定します。

新たに「Bottom」というタグを作成し、容器の底のContainer_Bottomに設定

次にゲームオーバーの線のGameOverLineに項目が追加されているので、ゲームオーバーの線とゲームオーバーになる時間の閾値を設定します。

ゲームオーバーの線とゲームオーバーになる時間の閾値を設定

そしてFruitsBaseのRigidbody2DのSleeping ModeをNever Sleepに設定します。

Rigidbody2DのSleeping ModeをNever Sleepに設定

この理由は後述します。

では設定ができたので動作確認をします。

動作確認

今回の確認事項は
  • 果物がゲームオーバーになる時間の閾値を超えたらゲームオーバーの表示がされる
  • 容器の外に果物が落下したらゲームオーバーの表示がされる
  • ゲームオーバーが表示されたらポインター操作不能になり、果物も落下しないようになる
を確認します。

では確認しましょう。

ゲームオーバーの線に触れ続けて閾値を超えたらゲームオーバーの表示がされた
ゲームオーバーの表示が出てからはポインターも移動せず果物も落下しないようになった

ゲームオーバーの線に接触して、ゲームオーバーになる時間の閾値を超えたらゲームオーバーの表示がされるようになりました。
ゲームオーバーの表示後はポインターも移動ができなくなり、果物も落下しないようになりました。

次は容器の外に果物が落下した時の処理を確認したいのですが、ポインターはClampで移動制限がかかっており、左右の容器の外に出られません。
そこでポインターに設定した、容器の壁との余白を一時的にマイナスにしてやることで、容器の外に移動できるようになります。

ポインターの容器の壁との余白を一時的にマイナスに設定

容器の壁との余白をマイナスにすることで容器の壁の外に出られるようになった

この状態で果物を落下させます。

果物を落下させる

ゲームオーバーの表示が出た

これで想定通りの機能が実装出来ました。

Rigidbody2DのSleeping Mode

理由を後述するとしていたRigidbody2DのSleeping Modeですが、デフォルトのStart Awakeのままにしていると、物理演算の処理負荷軽減のため、動かなくなったオブジェクトはSleep状態になってしまいます。

このSleep状態に移行する時間はEdit > Project Settings > Physics 2D > Time to Sleepで設定できますが、デフォルトでは0.5秒になっています。

Time to Sleepはデフォルトでは0.5秒になっている

そのため、ゲームオーバーになる時間の閾値以前でSleep状態になり、ゲームオーバーの判定が上手く行かなくなります。

デフォルトのStart AwakeにするとSleep状態になる

Sleep状態に移行した

そのため手っ取り早い方法でSleep状態に移行しないようNever Sleepにしましたが、この方法だと毎フレーム呼ばれるので処理負荷が増えるため、注意が必要です。

ですので、Time to Sleepをゲームオーバーになる時間の閾値以上に設定するといいです。

Time to Sleepを5秒に設定

ゲームオーバーになった

このゲームは軽いのでそこまでの影響はありませんが、類似の処理をする際は注意が必要です。

まとめ

今回は
  • ゲームオーバーの線を作成した
  • ゲームオーバーのUIを作成した
  • ゲームオーバーの機能を実装した
という事をやりました。

次回は、スコアの機能を実装したいと思います。

今回はちょっとボリュームが多かったですね。
お疲れ様でした。

では、また次回!

コメント