【Unity/2D】Flappy Bird風ゲームを作ってみる話 #12 スコアの機能を実装する

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

投稿主の無能です。

前回は、SEを再生する機能を実装しました。

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

スコアの機能について考える

今回のゲームだと敵も出ないしアイテムもありません。
(勿論要素を加えることは可能です)

プレイヤーが土管を通過するだけなので、プレイヤーが土管を通過したらスコアが入る、という形にしていきます。

では具体的にどのような仕組みを作るかですが、上下の土管の間にゴールテープが張ってあります。
しかも生成される土管は毎回ゴールテープが張ってあるので、土管を通過する度にそのテープを切る度にスコアが入るイメージです。

この方針でスコアの機能を作っていきましょう。

スコアの当たり判定を用意する

上記の方法は、近しい接触判定で考えてみると、DestroyAreaに接触した時と違う性質になっています。

DestroyAreaに接触したらプレイヤーが非アクティブになるのに対し、スコアの当たり判定に接触するとスコアが加算され、スコアの当たり判定の方が破棄されます。

という事は、スコアの当たり判定を新たに作って、土管と共に生成されるような仕組みを追加すればいいですね。

それではスコアの当たり判定を用意します。

ScoreAreaを用意する

Colliderをアタッチ

空のオブジェクトを作成して、名前を「ScoreArea」とします。

例の如く座標はリセットしておいてください。

空のオブジェクトを作成し「ScoreArea」にする

そしてScoreAreaにBox Collider2Dをアタッチします。

Box Collider2Dをアタッチ

アタッチ出来たら、DestroyAreaを作成した時と同様に、Sizeを変更して画面サイズいっぱいに縦に長く伸ばしてあげます。

画面サイズいっぱいに伸ばす

そしてIs Triggerにチェックを入れます。

Is Triggerにチェックする

そしてBody TypeをKinematicにします。

Body TypeをKinematicにする

Rigidbody2Dをアタッチ

次にRigidbody2Dをアタッチします。

このScoreAreaを動かすにはTransformでもいいのですが、Rigidbodyにベクトルを持たせる方法で統一したいと思います。

Rigidbody2Dをアタッチ

Rigidbody2Dをアタッチしたら、このオブジェクトをPrefab化します。

ScoreAreaオブジェクトをPrefab化する

そしてHierarchyビューのScoreAreaを削除します。

HierarchyビューのScoreAreaを削除

土管と同様に自動で生成されるので、HierarchyビューのScoreAreaは不要になります。

これでオブジェクトの準備が出来たので、次はUIを準備していきます。

スコアのUIを準備する

GamePauseを複製

次はスコアを表示するためのUIを準備します。

UI_GamePauseを複製して、それぞれリネームします。

UI_GamePauseを複製しリネーム

UI_Scoreをアクティブにして見えるようにします。

UI_Scoreをアクティブにする

子オブジェクトのText_Scoreの表示テキストの位置やサイズなどを変更します。

Text_Scoreの位置を変更

表示テキスト・フォントサイズを変更

Gameビュー

青空の白い雲で見え辛いと思うので、表示テキストを縁取りします。

表示テキストの縁取り

そのままInspectorビューの下部を見ると、テキストに設定できるオプション項目が並んでいるので、Outlineの項目にチェックします。

そしてThicknessを0.2にすると、表示テキストに縁取りが出来ました。

OutlineにチェックしThicknessを0.2にする

表示テキストに縁取りができた

フォントの扱いの注意事項

フォントの扱いについての注意事項ですが、先程Scoreを縁取りました。

ところが、UI_GamePause・UI_GameOverをアクティブにして確認してみると、全て縁取りがされていることが分かります。

UI_GamePause

UI_GameOver

何故このようになるかというと、UI_GamePause・UI_GameOverのどちらも同じフォントを使っているためです。

共通してこのフォントを使用している

そしてUI_Scoreで使用しているフォントも同じものなので、どこかで変更が行われると、別のテキストオブジェクトでそのフォントを使用していれば変更が適用される、ということです。

因みに同色なので見た目では分かりませんが、UI_GameOverでボタンに表示しているテキストもテキストと同じ黒で縁取りされています。

今回のゲームでは視認性が良くなったので大丈夫ですが、オプションを反映させたくない場合は、そのフォントをコピーして、対象のオブジェクトにコピーしたフォントを設定し、コピーしたフォントとコピー元(オリジナル)のフォントで設定を分けるようにしてください。

スコアの数字部分を用意

ではText_Scoreを複製して「Text_ScoreNum」にリネームします。

Text_Scoreを複製して「Text_ScoreNum」にリネーム

リネームしたら表示テキストを0にします。

表示テキストを0にする

あとは「Score」と表示されている下に位置を変更します。

Text_ScoreNumの位置を変更

「Score」の下にText_ScoreNumを表示

これでスコアのUIが準備出来ました。

GameManagerにタグ付けする

後程必要になるので、GameManagerにタグ付けしておきます。

「GameManager」というタグはデフォルトのタグには無いので、新規で追加します。

HierarchyビューのGameManagerオブジェクトを選択し、タグのリストの最下部のAdd Tagを選択します。

Add Tagを選択

Inspectorビューが変わってタグとレイヤーの画面になります。

Tag & Layersに切り替わる

右下の+ボタンを押すと新しく追加するタグ名を入力できるので、GameManagerと入力して保存します。

追加するタグ名を入力して保存

これで新たにタグが追加できました。

新たにタグが追加できた

ではこのタグをGameManagerオブジェクトに設定します。

GameManagerオブジェクトに設定

それでは準備できたので、スクリプトを書いていきます。

スクリプトを書く

ではスクリプトを書いて、スコアの機能を実装しましょう。

新たにスクリプトを作成し、名前を「ScoreController」にします。

新たに「ScoreController」スクリプトを作成

ではスコアの機能を書いていきましょう。

スコアの機能は、接触したという判定をするScoreControllerから、スコアの機能を有するGameManagerに、プレイヤーがScoreAreaに接触した時にスコアを追加するGameManagerのスコアの更新の関数を呼び出します。

そして呼び出されたGameManagerのスコアの更新をする関数は、スコアを加算する処理をします。

何となくでもいいので、処理の流れが分かったでしょうか。

では、はじめにタグを追加したので、Variablesスクリプトで追加したタグを共有できるようにします。

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

public class Variables : MonoBehaviour
{
    #region const-float
    public const float zero = 0;                                // ゼロの定数
    #endregion

    #region Var-string
    public const string tag_Player = "Player",                  // タグ:プレイヤー
                        tag_GameManager = "GameManager";        // タグ:ゲームマネージャー
    #endregion

    #region Var-Vector2
    public static Vector2 screenMin, screenMax;                 // 画面サイズ取得用
    #endregion
}
これでVariablesスクリプトの追記は以上です。

では次に、新たに追加したScoreControllerを見ていきます。

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

public class ScoreController : MonoBehaviour
{
    #region Var-ScoreArea
    [Header("スコアエリアの動く速度")]
    [SerializeField] float areaMoveSpeed = 1f;
    #endregion

    #region Var-Internal
    Rigidbody2D scoreAreaRB;                            // スコアエリアのリジッドボディ
    GameManager gameManager;                            // ゲームマネージャー
    #endregion

    #region Start
    void Start()
    {
        // リジッドボディを取得
        scoreAreaRB = GetComponent<Rigidbody2D>();
        // ゲームマネージャーを取得
        gameManager = GameObject.FindWithTag(Variables.tag_GameManager).GetComponent<GameManager>();
        // スコアエリアを動かす
        ScoreAreaMove();
    }
    #endregion

    #region ScoreAreaMove
    // スコアエリアの移動
    void ScoreAreaMove()
    {
        // スコアエリアに左方向のベクトルを持たせる
        scoreAreaRB.velocity = Vector2.left * areaMoveSpeed;
    }
    #endregion

    #region OnTriggerEnter2D
    // 接触判定
    private void OnTriggerEnter2D(Collider2D collision)
    {
        // プレイヤーと接触した場合
        if (collision.gameObject.CompareTag(Variables.tag_Player))
        {
            // スコアを更新する
            gameManager.ScoreUpdate();
            // オブジェクトを破棄
            Destroy(gameObject);
        }   
    }
    #endregion
}
では見ていきましょう。

メンバ変数

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

はじめに、スコアエリアの動く速度となるfloatのareaMoveSpeedです。

残りの変数は内部処理用になります。

ScoreAreaを動かすためのRigidbody2DとなるscoreAreaRBです。

そしてGameManagerを呼び出すために取得するgameManagerです。

メンバ変数は以上です。

Start関数

次にStart関数を見ていきましょう。

Start関数では、はじめにScoreAreaを動かすためのscoreAreaRBを取得します。

次に、スコアを加算する関数を呼び出すために、スコアを加算する関数を持っているGameManagerを取得します。

そしてscoreAreaを動かすためScoreAreaMove関数を呼び出しています。

ここら辺は必要なものを取得した後に、土管と同様に動かす関数を呼び出しています。

Start関数は以上です。

ScoreAreaMove関数

次はScoreAreaMove関数になります。

この関数は土管が左に動く処理と一緒で、左方向のベクトルを持たせています。

ScoreAreaMove関数は以上です。

OnTriggerEnter2D関数

次はOnTriggerEnter2D関数になります。

この関数では、プレイヤーと接触した場合に、GameManagerのスコアの更新を行う関数を呼んでいます。

そして、自身のオブジェクト、つまりscoreAreaを破棄します。

これでScoreControllerスクリプトは以上です。

次は、スコアの処理を追加したGameManagerを見ていきましょう。

GameManager.cs
using System.Collections;
using System.Collections.Generic;
using TMPro;
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-Score
    [Header("スコアの表示テキスト")]
    [SerializeField] TMP_Text UI_ScoreText;
    [Header("加算するスコア")]
    [SerializeField] int score = 1;
    #endregion

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

    #region Start
    void Start()
    {
        // スコアを初期化
        InitScore();
    }
    #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

    #region InitScore
    // スコアの初期化
    void InitScore()
    {
        // スコアの総計を0にする
        totalScore = (int)Variables.zero;
        // スコアのUIを更新
        ScoreUIUpdate();
    }
    #endregion

    #region ScoreUpdate
    // スコアの更新
    public void ScoreUpdate()
    {
        // スコアの総計にスコアを足し合わせる
        totalScore += score;
        // スコアのUIを更新
        ScoreUIUpdate();
    }
    #endregion

    #region ScoreUIUpdate
    void ScoreUIUpdate()
    {
        // スコアのUIを更新
        UI_ScoreText.SetText(totalScore.ToString());
    }
    #endregion
}
では見ていきましょう。

メンバ変数

追加したメンバ変数から見ていきましょう。

最初にスコアの得点を表示するUIとなるTMP_TextのUI_ScoreTextになります。

そして加算するスコアとなるintのscoreです。

次に内部処理用の変数で、スコアの総計となるintのtotalScoreです。

メンバ変数は以上です。

Start関数

新たに追加したStart関数です。

このStart関数では、スコアの初期化を行うInitScore関数を呼んでいます。

Start関数は以上です。

InitScore関数

次はスコアの初期化を行うInitScore関数です。

この関数では、スコアの総計を0にします。

(int)が文頭に付いているのは、(int)の後の数値を、型が違う状態でもint型と見做す、というキャストになります。

Variablesスクリプトのzeroはfloatなので、これをint型の0と見做すよ、という処理になります。

プログラムは「これはこうだね」という風に、曖昧さを回避するようになっています。
曖昧さが残っていると、書いたプログラムを正確に処理できないからです。

ですので「0」という数値でも、floatなのかintなのか明確にする必要があるので、キャストというもので明確にfloatの0をintの0と見做す、という処理になります。

そして、次にスコアのUIを更新するScoreUIUpdate関数を呼び出します。

これで表示しているスコアのUIが、totalScoreの0になります。

InitScore関数は以上です。

ScoreUpdate関数

次はスコアを更新するScoreUpdate関数になります。

まずtotalScoreに加算されるスコアを足し合わせます。

そしてScoreUIUpdate関数を呼び、スコアの表示を更新します。

ScoreUpdate関数は以上です。

ScoreUIUpdate関数

最後にScoreUIUpdate関数です。

この関数では、totalScoreの数値をテキストに変換して、スコアのテキストを更新します。

これでScoreControllerスクリプトは以上です。

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

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

動作確認前の準備

ではUnityで動作確認前の準備のしましょう。

GameManagerのInspectorビューに追加された項目があるので設定します。

スコアの表示テキストはText_ScoreNumを設定します。

Inspectorビューに追加された項目を設定する

そしてPrefabsフォルダ内のscoreAreaにScoreControllerスクリプトをアタッチします。

スクリプトをアタッチするとscoreAreaの移動する速度があるので、土管と同じ速度を設定します。

scoreAreaにScoreControllerスクリプトをアタッチ
スクリプトをアタッチするとscoreAreaの移動する速度があるので、土管と同じ速度を設定する

これで準備が出来ました。

動作確認

では動作確認します。

今回の確認事項は、土管の隙間を通過すると、GameManagerに設定したスコアが加算されるかを確認します。

土管の隙間を通過するとスコアが加算される

これでスコアの機能を実装出来ました。

まとめ

今回は
  • スコアの機能について考えた
  • スコアのUI、Prefabを作成した
  • スクリプトを作成し、スコアの機能を実装した
という事をやりました。

ここまでで、ゲームの基本的なところは押さえることができました。

後はご自身のアイディアを形にしていって、より面白いゲームに仕上げてください。

またゲームのネタがまとまり次第お目にかかる機会があると思います。

ではまた、いつかどこかで!

コメント