メインコンテンツへスキップ

【リズムゲームの作り方】 #10 判定結果とスコア・コンボの表示

目次
リズムゲームの作り方 - シリーズ
Part 10: この記事

前回までの内容で、リズムゲームの核となる部分の実装は完了しましたね!

今回は、見た目の部分を実装し、判定結果の表示やスコア・コンボといった機能を追加していきます。

判定結果の表示
#

リズムゲームでは、ノーツが判定されたときに「Perfect!」などと表示するものも多くあります。判定結果を表示することによって、自分の精度がどのくらいかを知ることができるようになります。

判定結果の表示に必要なのは、

  • 判定結果を表示するためのGameObject
  • 判定結果に応じて表示内容を変える機能
  • 時間が経つと表示内容が消える機能

といったところでしょうか。

判定結果を表示するためのGameObjectを設置する
#

まずは判定結果を表示するオブジェクトを作ります。

  1. Hierarchyウィンドウの+マークをクリックします。
  2. UI > Text - TextMeshPro を選択します。

TMP Importerというウィンドウが出るので、Import TMP Essentialsを押します。

処理が終わったら今操作したウィンドウを消します。

作ったゲームオブジェクトの名前をText (TMP)からNoteEvaluationTextに変更します。

HierarchyウィンドウでNoteEvaluationTextを選択し、Inspectorウィンドウで位置などを調整していきます。

PosX0PosY-100にします。文字はFont Size48にし、Alignmentを左右中央揃え、上下中央揃えにします。

Gameビューで確認してこのようになっていればOKです。

判定結果に応じて表示内容を変える機能
#

次は判定結果に応じて表示機能を変える機能を作ります。まずはスクリプトを作成します。

  1. ProjectウィンドウでAssets > Scriptsフォルダ内に移動します。
  2. 空いているところで右クリックします。
  3. Create > MonoBehaviour Scriptを選択してスクリプトを作成します。スクリプト名はTextControllerにします。

ChangeEvaluationTextというメソッドを作り、外部からテキストの内容を変える処理を書いていきます。以下のコードを記述してください。

TextController.cs
 1using UnityEngine;
 2using TMPro;
 3
 4public class TextController : MonoBehaviour
 5{
 6    [SerializeField] TextMeshProUGUI noteEvaluationText;
 7
 8    void Start()
 9    {
10
11    }
12
13    void Update()
14    {
15
16    }
17
18    // ノーツが判定されたときにテキストを更新
19    public void UpdateTexts(string noteEvaluation)
20    {
21        ChangeEvaluationText(noteEvaluation);
22    }
23
24    // 判定結果の文字列を受け取って、テキストを更新するメソッド
25    void ChangeEvaluationText(string noteEvaluation)
26    {
27        noteEvaluationText.text = noteEvaluation;
28    }
29}

なお、今後画面上の文字を変える処理をたくさん追加するので、UpdateTextsというメソッドを作ってUpdateTexts経由でChangeEvaluationTextを呼ぶようにしています。

さて、ChangeEvaluationTextですが、ノーツの判定評価を表示するテキストフィールドをnoteEvaluationText.text = noteEvaluation;として書き換える処理を追加しました。

[SerializeField]はUnityエディタ上から変数を設定できるようにするための記述でしたね。

テキストの操作にはusing TMPro;という記述も必要なのでそれも追加しています。

スクリプトが書けたら、Unityエディタに戻って設定を行います。

  1. ProjectウィンドウのAssets > Scripts にあるTextControllerを、HierarchyウィンドウのCanvasにドラッグ&ドロップしてアタッチします。

  1. HierarchyウィンドウのCanvasを選択し、アタッチしたスクリプトの NoteEvaluationTextという枠に、Hierarchyにある NoteEvaluationTextをドラッグ&ドロップして割り当てます。

また、判定されたときにUpdateTextsメソッドを呼ぶ仕組みも必要になります。

JudgementManagerJudgeされたときにUpdateTextsメソッドを呼び出すようにします。

JudgementManager.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class JudgementManager : MonoBehaviour
{
    List<NoteInfo> noteInfoList = new();
    NotesMover notesMover;
    [SerializeField] TextController textController;

    // ...(略)

    void Judge(int lane)
    {
        // 最も近いノーツを探す
        NoteInfo nearestNote = FindNearestNote(lane);

        // もしノーツが見つからなかったら何もしない
        if (nearestNote == null)
        {
            return;
        }

        // ノーツのベストタイミングを渡して、判定評価を取得する
        string evaluation = GetNoteEvaluation(nearestNote.time);

        // 判定対象外(NonTarget)でなければ、判定結果をコンソールに表示し、対応するノーツを削除する
        if (evaluation != "NonTarget")
        {
            Debug.Log($"レーン{lane}: {evaluation}!");
            nearestNote.gameObject.SetActive(false);
            noteInfoList.Remove(nearestNote);
            textController.UpdateTexts(evaluation);
        }
    }

    // ...(略)

    void JudgeLostNote()
    {
        // 1. スルーされた(Missになった)ノーツを探してリストに一時保存する
        List<NoteInfo> lostNotes = new();
        float elapsedTime = Time.time - notesMover.startTime;

        foreach (NoteInfo noteInfo in noteInfoList)
        {
            float diffTime = elapsedTime - noteInfo.time;
            // 判定線を過ぎていて、かつ判定範囲外まで離れてしまったノーツ
            if (diffTime > 0.0f && GetNoteEvaluation(noteInfo.time) == "NonTarget")
            {
                lostNotes.Add(noteInfo);
            }
        }

        // 2. 見つかったノーツをMiss扱いにして画面とリストから消す
        foreach (NoteInfo lostNote in lostNotes)
        {
            Debug.Log($"レーン{lostNote.lane}: Miss!");
            lostNote.gameObject.SetActive(false);
            noteInfoList.Remove(lostNote);
            textController.UpdateTexts("Miss");
        }
    }
}

TextControllerSerializeFieldで取得するので、忘れずにセットしましょう。

HierarchyウィンドウでNotesParentを選択し、InspectorウィンドウのText ControllerにHierarchyウィンドウのCanvasをセットします。

これでシーンを再生するとノーツを押すたびに判定評価の表示が変わります。

時間が経つと表示内容が消える機能
#

今の状態だとノーツをしばらく押していない状態でも表示が消えず、曲が終わっても判定評価が表示され続けています。

これだと少し見栄えが良くないので、時間が経つと表示内容が消える機能をつけたいと思います。

今回は判定結果の表示を行ってからだんだん薄くすることで表示を消していきます。

以下のようにコードを追加します。

TextController.cs
 1using UnityEngine;
 2using TMPro;
 3
 4public class TextController : MonoBehaviour
 5{
 6    [SerializeField] TextMeshProUGUI noteEvaluationText;
 7    [SerializeField] float fadeSpeed = 1.0f;
 8
 9    void Start()
10    {
11        noteEvaluationText.alpha = 0.0f;
12    }
13
14    void Update()
15    {
16        noteEvaluationText.alpha = Mathf.Max(noteEvaluationText.alpha - fadeSpeed * Time.deltaTime, 0.0f);
17    }
18
19    // ノーツが判定されたときにテキストを更新
20    public void UpdateTexts(string noteEvaluation)
21    {
22        ChangeEvaluationText(noteEvaluation);
23    }
24
25    // 判定結果の文字列を受け取って、テキストを更新するメソッド
26    public void ChangeEvaluationText(string noteEvaluation)
27    {
28        noteEvaluationText.text = noteEvaluation;
29        noteEvaluationText.alpha = 1.0f;
30    }
31}

Updateメソッド内で、だんだん不透明度を小さくする(=だんだん透明になる)ように処理を書きました。マイナスの値にならないように、Mathf.Maxメソッドを使って最小値を0.0fにしています。

そして、ChangeEvaluationTextnoteEvaluationText.alpha1.0fにすることで文字を不透明にし、判定されたタイミングでははっきり表示するようにしています。

なお、ゲーム開始直後は何も表示しないでほしいので、Startメソッド内に文字色を透明にする処理を書いています。

シーンを再生すると、判定評価が表示されてだんだん薄くなります!

累計判定評価の表示
#

次は累計判定評価(合計Perfect数など)を表示できるようにしていきます。

完成するとこのような表示になります。

UIを作る
#

まずは「Perfect」や「Good」などの文字を画面に表示していきましょう。

  1. HierarchyウィンドウのCanvasを右クリックします。
  2. UI > Canvas を選択します。

作ったCanvasの名前はTotalNoteEvaluationLabelsにします。

作ったTotalNoteEvaluationLabelsはInspectorウィンドウからLeft-1500Top-800にします。

HierarchyウィンドウのTotalNoteEvaluationLabelsを右クリックし、UI > Text - TextMeshProをクリックして名前をPerfectにします。

Perfectを選択して、Inspectorウィンドウからテキストの内容をPerfectに、PosXPosY0に、フォントサイズを48にします。

Perfectと同様に、GoodMissのテキストも作り、テキストの内容をGoodMissに、PosX0PosYをそれぞれ-70-140に、フォントサイズを48にしましょう。

ここまで作ると、HierarchyウィンドウとGameビューはこのようになります。

次に累計判定評価の数を表示するUIを作成します。先ほど作ったTotalNoteEvaluationLabelsを流用したいと思います。

TotalNoteEvaluationLabelsを右クリック→DuplicateTotalNoteEvaluationLabelsを複製して、名前をTotalNoteEvaluationCountsに変えます。

HierarchyウィンドウでTotalNoteEvaluationCountsを選択し、InspectorウィンドウからLeft-1400にします。

またTotalNoteEvaluationCountsの子であるPerfectGoodMissのそれぞれで、テキストの内容を0にし、右揃えにします。

これで累計判定評価に関するUIの作成は完了です。

実装1. 変数の準備
#

累計判定評価の実装は、

  1. 累計判定評価を記録する変数を準備する
  2. 判定評価をカウントする
  3. カウントをUIに反映する

という流れになります。

まずは判定評価を記録する仕組みを準備していきましょう。

TextControllerクラスに記述を追加していきます。

UIを反映できるようにTotalNoteEvaluationCountsの各要素を保存できるようにするのと、累計判定評価を保存できるようにしていきます。

TextController.cs
 1using UnityEngine;
 2using TMPro;
 3
 4public class TextController : MonoBehaviour
 5{
 6    [SerializeField] TextMeshProUGUI noteEvaluationText;
 7    [SerializeField] float fadeSpeed = 1.0f;
 8
 9    [SerializeField] TextMeshProUGUI perfectCountText;
10    [SerializeField] TextMeshProUGUI goodCountText;
11    [SerializeField] TextMeshProUGUI missCountText;
12    int perfectCount = 0;
13    int goodCount = 0;
14    int missCount = 0;
15
16    void Start()
17    {
18        noteEvaluationText.alpha = 0.0f;
19    }
20
21    // ...(以下略)
22}

判定評価それぞれのテキストと合計値を保存できるようにしました。

SerializeFieldにしたので、忘れずにセットしましょう。

Unityエディタに戻り、HierarchyウィンドウでCanvasを選択し、InspectorウィンドウのPerfectCountTextGoodCountTextMissCountTextにHierarchyウィンドウのTotalNoteEvaluationCounts配下のPerfect, Good, Missをセットしましょう。

実装2. 判定評価をカウントする
#

次は、判定されたときに判定評価を増やす処理を書いていきます。

判定評価を受け取って、その内容に応じてperfectCountなどを+1すれば良さそうです。

TextController.cs
using UnityEngine;
using TMPro;

public class TextController : MonoBehaviour
{
    /// ...(略)

    // ノーツが判定されたときにテキストを更新
    public void UpdateTexts(string noteEvaluation)
    {
        ChangeEvaluationText(noteEvaluation);
        UpdateEvaluationCount(noteEvaluation);
    }

    // 判定結果の文字列を受け取って、テキストを更新するメソッド
    public void ChangeEvaluationText(string noteEvaluation)
    {
        noteEvaluationText.text = noteEvaluation;
        noteEvaluationText.alpha = 1.0f;
    }

    // 判定評価の文字列を受け取って、累計判定評価を変えるメソッド
    void UpdateEvaluationCount(string noteEvaluation)
    {
        if (noteEvaluation == "Perfect")
        {
            perfectCount++;
        }
        if (noteEvaluation == "Good")
        {
            goodCount++;
        }
        if (noteEvaluation == "Miss")
        {
            missCount++;
        }
    }
}

UpdateEvaluationCountメソッドを追加しました。ノーツが判定されると、判定結果に応じて内部の累計判定評価が増えます。

実装3. カウントをUIに反映する
#

この状態だと内部のカウントは増えますが表示を変える処理を書いておらず画面上の表示が変わらないままなので、内部のカウントの値を表示する処理を追加します。

TextController.cs
using UnityEngine;
using TMPro;

public class TextController : MonoBehaviour
{
    // ...(略)

    // ノーツが判定されたときにテキストを更新
    public void UpdateTexts(string noteEvaluation)
    {
        ChangeEvaluationText(noteEvaluation);
        UpdateEvaluationCount(noteEvaluation);
        UpdateTotalNoteEvaluationCountText();
    }

    // 判定結果の文字列を受け取って、テキストを更新するメソッド
    public void ChangeEvaluationText(string noteEvaluation)
    {
        noteEvaluationText.text = noteEvaluation;
        noteEvaluationText.alpha = 1.0f;
    }

    // 判定評価の文字列を受け取って、累計判定評価を変えるメソッド
    void UpdateEvaluationCount(string noteEvaluation)
    {
        if (noteEvaluation == "Perfect")
        {
            perfectCount++;
        }
        if (noteEvaluation == "Good")
        {
            goodCount++;
        }
        if (noteEvaluation == "Miss")
        {
            missCount++;
        }
    }

    void UpdateTotalNoteEvaluationCountText()
    {
        perfectCountText.text = perfectCount.ToString();
        goodCountText.text = goodCount.ToString();
        missCountText.text = missCount.ToString();
    }
}

これで、カウントがUIに反映されるようになりました。

スコアの表示
#

次はスコアを表示する機能を追加していきます。

スコアの計算方法はゲームによってさまざまで、コンボ数でノーツあたりのスコアが変わるものもありますが、今回は単純に全体のノーツ数で割った1ノーツあたりのスコアと評価を使ったスコアにします。

すべてPerfectだった場合のスコアを10000点とし、判定によるスコアの割合を以下のようにします。

判定評価 スコア割合
Perfect ×1.0
Good ×0.5
Miss ×0.0

計算式で言えば、1ノーツあたりのスコアは10000 ÷ (総ノーツ数) × (スコア割合)となりますね。

これを使ってスコアを計算・表示していきます。

UI
#

まずはスコアを表示するUIを作ります。

HierarchyウィンドウのCanvasを右クリックし、UI > Text - TextMeshProCanvasの子にTextMeshProを作ります。名前はScoreLabelにします。

位置と文字を調整します。作ったScoreLabelを選択し、InspectorウィンドウからPosX600PosY400、テキストの内容をScore、フォントサイズを48にします。

同様にしてもう一つTextMeshProを作り、名前をScoreTextにします。こちらはPosX750PosY400、テキストの内容を0、フォントサイズを48にし、テキストを右揃えにします。

実装
#

ここからコードを書いていきますが、スコアの計算には各累計判定評価だけでなく、総ノーツ数も必要になります。総ノーツ数は、NotesGeneratorで計算したあと、TextControllerに知らせる形にします。

また、ScoreTextを更新するため、UIの取得も必要です。

TextController.cs
using UnityEngine;
using TMPro;

public class TextController : MonoBehaviour
{
    [SerializeField] TextMeshProUGUI noteEvaluationText;
    [SerializeField] float fadeSpeed = 1.0f;

    [SerializeField] TextMeshProUGUI perfectCountText;
    [SerializeField] TextMeshProUGUI goodCountText;
    [SerializeField] TextMeshProUGUI missCountText;
    int perfectCount = 0;
    int goodCount = 0;
    int missCount = 0;

    int totalNotes;
    [SerializeField] TextMeshProUGUI scoreText;

    // ...(略)

    void UpdateTotalNoteEvaluationCountText()
    {
        perfectCountText.text = perfectCount.ToString();
        goodCountText.text = goodCount.ToString();
        missCountText.text = missCount.ToString();
    }

    public void SetTotalNotes(int total)
    {
        totalNotes = total;
    }
}
NotesGenerator.cs
 1using System;
 2using System.Collections.Generic;
 3using UnityEngine;
 4
 5public class NotesGenerator : MonoBehaviour
 6{
 7    [SerializeField] TextAsset textAsset;
 8    [SerializeField] GameObject note;
 9    [SerializeField] float bpm;
10    float noteSpeed;
11    public List<NoteInfo> noteInfoList = new();
12
13    [SerializeField] TextController textController;
14
15    void Start()
16    {
17        // noteSpeedを読み込む
18        noteSpeed = gameObject.GetComponent<NotesMover>().noteSpeed;
19
20        // 譜面を読み込む
21        string[] chart = textAsset.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries);
22
23        // ノーツを生成
24        GenerateNotes(chart);
25
26        // TextControllerに総ノーツ数を知らせる
27        textController.SetTotalNotes(noteInfoList.Count);
28    }
29
30    // ...(以下略)
31}

保存してUnityエディタに戻り、CanvasTextControllerscoreTextNotesParentNotesGeneratorTextControllerをセットしておきましょう。

HierarchyウィンドウでCanvasを選択して、InspectorウィンドウのScore Textに、HierarchyウィンドウのScoreTextをドラッグ&ドロップします。

また、HierarchyウィンドウでNotesParentを選択して、InspectorウィンドウのTextControllerに、HierarchyウィンドウのCanvasをドラッグ&ドロップします。

それでは、実際にスコアを計算して表示する機能を作りましょう。

TextController.cs
using UnityEngine;
using TMPro;

public class TextController : MonoBehaviour
{
    // ...(略)

    // ノーツが判定されたときにテキストを更新
    public void UpdateTexts(string noteEvaluation)
    {
        ChangeEvaluationText(noteEvaluation);
        UpdateEvaluationCount(noteEvaluation);
        UpdateTotalNoteEvaluationCountText();
        UpdateScore();
    }

    // ...(略)

    public void SetTotalNotes(int total)
    {
        totalNotes = total;
    }

    void UpdateScore()
    {
        float score = (perfectCount * 1.0f + goodCount * 0.5f) / totalNotes * 10000.0f;
        scoreText.text = Mathf.RoundToInt(score).ToString();
    }
}

計算したscorefloat型ですが、表示するときは小数だと見づらいのでMathf.RoundToInt(score)とすることで小数点以下を四捨五入して整数にしています。

保存してシーンを再生すると、ゲーム中にスコアが更新されます!

ノート

「1ノーツあたりのスコアがわかるんだから判定のたびに1ノーツあたりのスコアを足せば良いんじゃない?」と思いましたか?

もちろんそれでもOKです!が、その場合スコアを整数で保存しないように注意してください。計算のたびに小数点以下を切り捨てたり四捨五入したりすると全てPerfect評価でも10000点にならない場合があります。

理論値なのに理論値じゃない!?みたいなことが起きないように注意しましょう!

コンボの表示
#

最後に、コンボを表示する機能を作ります。

今回はPerfectやGoodでコンボが続き、Missでコンボが途切れる、というものを作っていきます。

UI
#

コンボを表示するUIを作ります。作り方はスコアと概ね同じです。

HierarchyウィンドウのCanvasを右クリックし、UI > Text - TextMeshProCanvasの子にTextMeshProを作ります。名前はComboLabelにします。

位置と文字を調整します。作ったComboLabelを選択し、InspectorウィンドウからPosX750PosY0、テキストの内容をCombo、フォントサイズを36にし、テキストを上下左右中央揃えにします。

同様にもう一つTextMeshProを作り、名前をComboTextにします。

こちらはPosX750PosY100、Widthを300、テキストの内容を0、フォントサイズを128にし、テキストを上下左右中央揃えにします。

Gameビューではこのようになります。

実装
#

まずは先ほど作成したUIを登録できるようにし、コンボ用の変数を作ります。

TextController.cs
 1using UnityEngine;
 2using TMPro;
 3
 4public class TextController : MonoBehaviour
 5{
 6    [SerializeField] TextMeshProUGUI noteEvaluationText;
 7    [SerializeField] float fadeSpeed = 1.0f;
 8
 9    [SerializeField] TextMeshProUGUI perfectCountText;
10    [SerializeField] TextMeshProUGUI goodCountText;
11    [SerializeField] TextMeshProUGUI missCountText;
12    int perfectCount = 0;
13    int goodCount = 0;
14    int missCount = 0;
15
16    int totalNotes;
17    [SerializeField] TextMeshProUGUI scoreText;
18
19    [SerializeField] TextMeshProUGUI comboText;
20    int combo = 0;
21
22    void Start()
23    {
24        noteEvaluationText.alpha = 0.0f;
25    }
26
27    // ...(以下略)
28}

書き終わったら、UnityエディタからcomboTextをセットしておきましょう。

HierarchyウィンドウでCanvasを選択し、InspectorウィンドウのCombo TextにHierarchyウィンドウのComboTextをドラッグ&ドロップします。

次はUpdateComboメソッドを追加し、判定評価によってコンボを継続または0にする処理を書いていきます。

TextController.cs
using UnityEngine;
using TMPro;

public class TextController : MonoBehaviour
{
    // ...(略)

    // ノーツが判定されたときにテキストを更新
    public void UpdateTexts(string noteEvaluation)
    {
        ChangeEvaluationText(noteEvaluation);
        UpdateEvaluationCount(noteEvaluation);
        UpdateTotalNoteEvaluationCountText();
        UpdateScore();
        UpdateCombo(noteEvaluation);
    }

    // ...(略)

    void UpdateScore()
    {
        float score = (perfectCount * 1.0f + goodCount * 0.5f) / totalNotes * 10000.0f;
        scoreText.text = Mathf.RoundToInt(score).ToString();
    }

    void UpdateCombo(string noteEvaluation)
    {
        if (noteEvaluation == "Perfect" || noteEvaluation == "Good")
        {
            combo++;
        }
        else
        {
            combo = 0;
        }

        comboText.text = combo.ToString();
    }
}

コードを保存してシーンを再生してみましょう。判定結果に応じてコンボが継続またはリセットされます!

まとめと次回予告
#

UIの実装が終わりましたね!UIがつくことでリズムゲームっぽさが上がったのではないかと思います。

次回は、音ズレを修正していきます。

リズムゲームの作り方 - シリーズ
Part 10: この記事