機能追加をしやすいコードを目指そう(2)

前回の更新作業で、ずいぶん依存性を下げることができましたが、ジェネレーターのところでは、まだ大きな依存が残っています。今回はここを更新していく中で同時に学習材料としましょう

追加、または更新したスクリプト

GameDirector(更新)

レベルデザインのための処理をごそっと削除しています(レベルデザインを担当するスクリプトに移動)

インスペクターでドラッグ&ドロップしてインスタンスを登録

いきなりText型とすることでGetComponentメソッドの呼び出しも省略できます

[SerializeField]
Text timerText;

// GetComponentが不要になります
timerText.text = $"{time:F1}";

文字列補間を使った表現

書籍の表示から更新しています
処理は同じです

timerText.text = $"{time:F1}";
pointText.text = $"{point} point";

全体のコード

using UnityEngine;
using UnityEngine.UI;

public class GameDirector : MonoBehaviour
{
    [SerializeField]
    Text timerText;
    [SerializeField]
    Text pointText;

    [field: SerializeField]
    public int point { get; set; } = 0;
    [field: SerializeField]
    public float time { get; set; } = 30;

    public void AddPoint(Collider itemCollider)
    {
        point += itemCollider.GetComponent<ItemPoint>().HitPoint;
    }

    private void Update()
    {
        timerText.text = $"{time:F1}";
        pointText.text = $"{point} point";
    }
}

LevelDesigner(新規:GameDirectorオブジェクトにアタッチ)

レベルデザインを担当します
GameDirectorが担っていた処理を独立させています
ゲームの難易度調整、ゲームバランスをここで登録します

インスペクターでのタイトル表示

インスペクターでタイトルを表示されるには次の属性にします

[Header("制限時間")]

当初のレベルデザイン仕様に合わせること、また、アイテムの追加に対応できることを目的に更新しています
当初のデザインとは、時間経過によって、次のパラメータを変化させることです

  • アイテムを発生させる間隔
  • アイテムの落下速度
  • アイテムがどの種類かを決める

連続した曲線を使ってレベルデザインを登録

AnimationCurve spanCurve;

if文で時間経過を確認して、経過時間に応じて上記パラメータを登録しているコードでしたが、数値で合わせる必要があり、人が調整するのが難しいと思いましたので、アニメーションカーブを使って感覚で登録できるようにしています

経過時間を割合で取得

例えば、制限時間30秒で15秒を経過した時点では、0.5が得られます。つまり50%の時間まできたとの考え方です。その場合、startTimeには30が入ります。timeは減っていく時間で最初30で徐々に減っていきます

float timeRatio = (startTime - time) / startTime;

// 時間を減らす処理(書籍と同じ)
time -= Time.deltaTime;

各アイテムの出現確率を取得

ItemPrefabs変数には、アイテムのプレファブのリストが保存されています。
プレファブには、出現確率を登録しているスクリプトがアタッチされています(今回新規)
itemRatios変数はに全てのアイテムの発生確率を抽出して代入しているリストになります
アニメーションカーブで経過時間時点の出現確率を取得できるようにしていますので、経過時間を正規化したものを引数で渡してその値を取得しています
(正規化とは、実際の時間間隔を0から1の間隔に置き換えたもの)

// 登録アイテムに設定されている出現確率をリストで取得
        List<float> itemRatios = itemPrefabs.Select(item =>
        item.GetComponent<OccurrenceProbability>().GetRatio(timeRatio)).ToList();

ガチャ計算メソッドを呼んで、選択された要素番号を取得

経過時間時点の各アイテムの出現確率が取得できたので、それを使ってどのアイテムを選択するかを決定します
計算はややこしいので、Probabilityクラスを作ってGetProbabilityメソッドを呼び出し、チョイスされた結果だけをもらっています

// ガチャ計算!(各アイテムの出現確率のリストを引数で渡して選んでもらう)
// 戻り値は、リストの要素番号
int selectItemIndex = Probability.GetProbability(itemRatios);

チョイスされたプレファブを取得

アイテムリストのどの要素番号のものが選択されたかを取得できたので、アイテムのプレファブリストから該当するアイテムを取得します(リストでは、要素番号は0から始まります)
itemPrefabs[要素番号]となります

// リストから選ばれたアイテムを取得する
GameObject selectedItemPrefab = itemPrefabs[selectItemIndex];

出現間隔とドロップスピードを取得

直感的に登録できるようにアニメーションカーブから取得します
spanCurve.Evaluate(timeRatio)でtimeRaitoをxの値としたyの値を取得できます
xの値は0〜1を選択してもらって、それにmaxを掛けて実際の値を算出しています

// 各パラメータをアニメーションカーブから取得する
float span = spanCurve.Evaluate(timeRatio) * maxSpan;
float speed = speedCurve.Evaluate(timeRatio) * maxSpeed;

アイテムジェネレータにパラメータを渡す

各作成したパラメータをアイテムジェネレータに渡しています
パラメータは3つですね。最初の2つは書籍と同じです
第3引数は、書籍ではAppleの発生確率を渡していますが、このメソッドでは選択されたプレファブを渡すように変更しています。アイテムの追加が想定される前提なのでAppleだけの出現確率に依存しないようにするためです

// アイテムジェネレーターにパラメータ(引数)を渡してInstantiateしてもらう
this.generator.SetParameter(span, speed, selectedItemPrefab);

全体のコード

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class LevelDesigner : MonoBehaviour
{

    [SerializeField]
    ItemGenerator generator;
    [SerializeField]
    GameDirector director;

    [SerializeField, Header("制限時間")]
    float startTime = 30.0f;

    [SerializeField, Header("ドロップ間隔推移")]
    AnimationCurve spanCurve;
    [SerializeField, Header("最大ドロップ間隔")]
    float maxSpan = 1.0f;

    [SerializeField, Header("ドロップスピード推移")]
    AnimationCurve speedCurve;
    [SerializeField, Header("最大ドロップスピード")]
    float maxSpeed = -0.03f;

    [SerializeField, Header("アイテム一覧")]
    List<GameObject> itemPrefabs;

    float time;
    void Start()
    {
        time = startTime;
    }

    void Update()
    {
        float timeRatio = (startTime - time) / startTime;

        if (time < 0)
        {
            time = 0;
        }

        director.time = time;

        if (time == 0)
        {
            Time.timeScale = 0;
            return;
        }

        time -= Time.deltaTime;

        // 登録アイテムに設定されている出現確率をリストで取得
        List<float> itemRatios = itemPrefabs.Select(item =>
        item.GetComponent<OccurrenceProbability>().GetRatio(timeRatio)).ToList();

        // ガチャ計算!(各アイテムの出現確率のリストを引数で渡して選んでもらう)
        // 戻り値は、リストの要素番号
        int selectItemIndex = Probability.GetProbability(itemRatios);

        // リストから選ばれたアイテムを取得する
        GameObject selectedItemPrefab = itemPrefabs[selectItemIndex];

        // 各パラメータをアニメーションカーブから取得する
        float span = spanCurve.Evaluate(timeRatio) * maxSpan;
        float speed = speedCurve.Evaluate(timeRatio) * maxSpeed;

        // アイテムジェネレーターにパラメータ(引数)を渡してInstantiateしてもらう
        this.generator.SetParameter(span, speed, selectedItemPrefab);
    }
}

ItemGenerator(更新)

書籍では、このスクリプトでどのアイテムを出現させるかを乱数とif文で決定する役割を担っていました。
その処理は、LevelDesignerスクリプトに移しています。アイテム数に依存している箇所をリファクタリングしていきます

アイテムプレファブの登録を不要にする

どのインスタンスを作成するかはLevelDesignerに任せているので、ここでは引数でもらったプレファブの生成することだけ担当します。次のようなコードはこのスクリプトでは不要です

書籍のコード

public GameObject applePrefab;
public GameObject bombPrefab;

イテレータ(コルーチン)

Startメソッドをコルーチンにすることができます(バージョンアップでできるようになりました)

IEnumerator Start()

一定時間の待ち

このコードに到達すると、dropSpan秒分待ちになります
経過後、次の行(下の行)が実行されます
待っている間、他のスクリプトのUpdateなどの実行が進みます
なので、アプリケーションが固まったようには見えません

yield return new WaitForSeconds(dropSpan);

// dropSpan秒後に実行されるコード
GameObject item = Instantiate(itemPrefab);

全体のコード

using System.Collections;
using UnityEngine;

public class ItemGenerator : MonoBehaviour
{
    float span = 1.0f;
    float speed = -0.03f;

    GameObject itemPrefab;

    public void SetParameter(float span, float speed, GameObject itemPrefab)
    {
        this.span = span;
        this.speed = speed;
        this.itemPrefab = itemPrefab;
    }

    IEnumerator Start()
    {
        while (true)
        {
            float dropSpan = span;

            yield return new WaitForSeconds(dropSpan);

            GameObject item = Instantiate(itemPrefab);

            float x = Random.Range(-1, 2);
            float z = Random.Range(-1, 2);

            item.transform.position = new Vector3(x, 4, z);
            item.GetComponent<ItemController>().dropSpeed = speed;
        }
    }
}

OccurrenceProbability(新規:アイテムのプレファブにアタッチ)

アイテムのプレファブにアタッチするスクリプトです
出現確率を登録しますが、単純に数値を登録するのではなく、時間経過で変化させることができます
直感的に操作できるようにアニメーションカーブを使っています

インスペクターでマウスを変数名にしばらく置くとヒントが表示される

Tooltip属性で実現できます

[Tooltip("x軸:経過時間(0~1),y軸:確率(0~1)")]

アニメーションカーブから値を取得

indexの値をx軸の値としてy軸の値を返すメソッドです
indexは0〜1を代入し、結果も0〜1を返すようにします
アニメーションカーブは上記の制限がありませんので操作はその範囲に収まるようにします

return ratioCurve.Evaluate(index);

全体のコード

using UnityEngine;

public class OccurrenceProbability : MonoBehaviour
{
    [SerializeField,Header("出現確率"),Tooltip("x軸:経過時間(0~1),y軸:確率(0~1)")]
    AnimationCurve ratioCurve;

    public float GetRatio(float index)
    {
        return ratioCurve.Evaluate(index);
    }
}

Probability(新規:アタッチしません)

ガチャ計算を担当します
計算用のスクリプト、つまりC#クラスです。インスタンスを作成する用途ではない(シーンに登場しない)ので、プロジェクトウィンドウに置いたままでOKです。
どれかのオブジェクトが所有している必要がありませんよね

発生させるアイテムを決定する

発生確率のリストを渡して、どの要素番号を選ぶかを決定します
このメソッドにアイテムの発生確率が格納されたリストを渡すと、アイテムを決定してくれます

public static int GetProbability(List<float> ratioSet)

例えば、[0.2, 0.5, 0.7](つまり1つ目のアイテムのガチャ確率を20%、2つ目50%、3つ目70%)を渡すとすると、2番目が選ばれると戻り値は1になります(0が1番目のため)

ここは、まず、使い方だけ覚えておけばいいでしょう

全体のコード

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

static class Probability
{
    static List<float> ratioList = new();

    public static int GetProbability(List<float> ratioSet)
    {
        ratioList.Clear();

        float ratioSum = ratioSet.Sum();
        float addSum = 0;

        foreach (var ratio in ratioSet)
        {
            addSum += ratio;
            ratioList.Add(addSum / ratioSum);
        }

        float rand = Random.Range(0f, 1.0f);

        for (int i = 0; i < ratioList.Count - 1; i++)
        {
            if (ratioList[i] > rand)
            {
                return i;
            }
        }

        return ratioList.Count - 1;
    }
}

ガチャ計算の参考資料

アイテムプレファブの構成

出現確率を登録するスクリプトをアタッチします
時間経過による出現確率をアニメーションカーブで登録できます

アイテムプレファブのコンポーネント

アニメーションカーブでの登録

アニメーションカーブの調整資料

コミットしましょう

「レベルデザインの仕組みを変更」のタイトルでコミットしましょう

参考