【Unity】リファクタリングのポイント

2023年9月26日

プロジェクトの作成では、さまざまな実現方法がとられています
動作する時点で明らかな間違いではありませんが、保守性、拡張性、変更容易性を考えた場合、実現方法の更新(リファクタリング)をした方がいい場合があります

手順はいろいろありますが、ここでは、サンプルを通してみていきましょう

リファクタリングのポイント

次のポイントをリファクタリングすることで、新しいアイテムの追加、ゲームバランスの調整を用意になります
さまざまなテクニックを取り入れていますので、コーディングの奥深さを感じてみてください
ただ、わからないことが多いことでネガティブにはならないようにしてください

  • Updateでアイテムの落下をフレームレート固定から、Time.deltatimeを乗ずることでプラットフォームのパフォーマンス相違を吸収
  • 個々のアイテムオブジェクトに機能を持たせる
    アイテムを取得した時の処理がバスケットに記述されているので、他のアイテムの取得にも柔軟に対応できるように各アイテムにサウンドエフェクトや得点の更新処理を委任するように変更
  • サウンドを再生する場合、オブジェクトが廃棄されたら同時にサウンドの再生も破棄されてしまうため、AudioSource.PlayClipAtPointメソッドで実現します
  • Update内での時間経過関連の処理をコルーチンに変更
    コードが実行される回数を減らすことができます
  • インスタンスの取得は、インスペクターからアタッチするように変更
    コード中にマジックワード(ナンバー)を記述することがないため、依存性を低くする効果があります
    プライベートの場合は、[SeriarizeField]属性をつけ、パブリックの場合は、publicアクセス修飾子をつけます
  • UIの更新方法の変更
    UIの更新頻度が高いとパフォーマンスの低下につながるので、必要な時のみ更新するようにしました
    具体的には、タイマー表示は1秒おき、得点表示は得点の更新がかかった時になります
  • 個別のパラメータをまとめて扱う
    パラメータを1つのセットをクラス化することでクラスのデータ部分(属性)としてまとめます
  • パラメータ更新の容易性
    if文の羅列での更新は、コードを直接変更する必要があるため、推奨されません(マジックナンバー)
    パラメータは、スクリプタブルオブジェクト機能を使ってファイル化し、インスペクターで変更ができるように容易性をアップさせています
    まだ、配列で管理することでコードで処理できるように変更されています
    スクリプタブルオブジェクトのファイルはアセットメニュー(プロジェクトウィンドウのメニュー)に追加できるようにしています
  • タイマーが切れたタイミングで新しいアイテムが作成されないようにしています
    無限時間待ちのような終わり方からの変更になります

シーンの構成

ゲームオブジェクト

basket

BasketController

当たり判定では、アップルを取得すると100ポイント加算、爆弾を取得すると得点が半分になる処理を描くオブジェクトでの計算に変更
爆弾取得時にたとえば-50と減算であれば、アイテム共通のコードになるので、シンプルになります

また、IUpdatableインターフェースを実装していますが、これは、アップルと爆弾で計算処理が異なるためインターフェースを実装していれば処理が可能なようにするためです(アップルは加算処理、爆弾は今の得点が半分になる処理)

// UnityEngine名前空間を使用する
using UnityEngine;

// BasketControllerクラスを定義する
public class BasketController : MonoBehaviour
{
    // GameDirectorオブジェクトを参照する変数を宣言する
    public GameDirector gameDirector;

    // Colliderがトリガーに接触したときに呼び出されるメソッド
    void OnTriggerEnter(Collider other)
    {
        // ItemGetterコンポーネントを取得し、TouchBasketメソッドを呼び出す
        other.GetComponent<ItemGetter>().TouchBasket();

        // IUpdatableコンポーネントを取得し、UpdateScoreメソッドを呼び出し、GameDirectorのポイントを更新する
        var updatable = other.GetComponent<IUpdatable>();
        gameDirector.Point = updatable.UpdateScore(gameDirector.Point);
    }

    // 毎フレーム呼び出されるメソッド
    void Update()
    {
        // マウスの左ボタンがクリックされたとき
        if (Input.GetMouseButtonDown(0))
        {
            // Rayを作成し、マウス位置からRayを飛ばす
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

            // Rayが衝突したオブジェクトを取得する
            if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity))
            {
                // 衝突した位置のx座標とz座標を四捨五入して、BasketControllerの位置を変更する
                float x = Mathf.RoundToInt(hit.point.x);
                float z = Mathf.RoundToInt(hit.point.z);
                transform.position = new Vector3(x, 0, z);
            }
        }
    }
}

このコードはBasketControllerというクラスを定義し、Basketオブジェクトの制御を行います。

このクラスはUnityのコンポーネントであるMonoBehaviourクラスを継承しており、MonoBehaviourはUnityによって提供されるフレームワークの中核を成すクラスです。

以下に、BasketControllerクラスで定義されているメソッドについて説明します。

  1. OnTriggerEnter(Collider other)

このメソッドは、Basketオブジェクトに衝突した他のオブジェクトのColliderコンポーネントがトリガーになっている場合に呼び出されます。Basketオブジェクトに衝突したオブジェクトが持つItemGetterコンポーネントのTouchBasketメソッドを呼び出し、その後、衝突したオブジェクトが持つIUpdatableコンポーネントのUpdateScoreメソッドを呼び出して、ポイントを更新します。

  1. Update()

このメソッドは、フレームごとに呼び出されるメソッドで、マウスの左ボタンがクリックされた場合、Basketオブジェクトをマウスの位置に移動させます。具体的には、ScreenPointToRayメソッドを使用して、マウスの位置からRayを飛ばし、Rayが衝突したオブジェクトの位置を四捨五入してBasketオブジェクトの位置を変更します。

また、GameDirectorオブジェクトを参照するためのpublic変数gameDirectorを持ちます。GameDirectorはゲームの状態を管理するクラスで、ポイントの管理などの機能を持っています。BasketControllerクラスでは、GameDirectorクラスが持つポイントの値を更新するためにgameDirector変数を使用します。

ItemGenerater

ItemGenerator

using System.Collections;
using UnityEngine;

public class ItemGenerator : MonoBehaviour
{
    [SerializeField]
    GameObject applePrefab; // アップルのプレハブ
    [SerializeField]
    GameObject bombPrefab; // ボムのプレハブ

    float span; // アイテム生成の間隔
    float speed; // 落下速度
    float ratio; // ボムが出現する確率

    Coroutine generateCorutine; // コルーチン

    // アイテム生成パラメータを設定する
    public void SetParameter(GenerateParam itemParameter)
    {
        span = itemParameter.span;
        speed = itemParameter.speed;
        ratio = itemParameter.ratio;

        Debug.Log($"生成:{span}, {speed} {ratio}"); // ログを出力する
    }

    // アイテムの生成を開始する
    public void GenerateStart()
    {
        generateCorutine = StartCoroutine(Repeat()); // コルーチンを開始する
    }

    // アイテムの生成を停止する
    public void GenerateStop()
    {
        StopCoroutine(generateCorutine); // コルーチンを停止する
    }

    // アイテムの生成を繰り返すコルーチン
    private IEnumerator Repeat()
    {
        while (true)
        {
            GameObject itemPrefab;
            int dice = Random.Range(1, 11); // ランダムに1から10の整数を生成する

            // ランダムに選択した整数がratio以下ならばボムを、それ以外ならアップルを生成する
            itemPrefab = dice <= ratio ? bombPrefab : applePrefab;

            float x = Random.Range(-1, 2); // x座標をランダムに生成する
            float z = Random.Range(-1, 2); // z座標をランダムに生成する

            GameObject generateItem = Instantiate(itemPrefab, new Vector3(x, 4, z), Quaternion.identity); // アイテムを生成する

            generateItem.GetComponent<ItemController>().dropSpeed = speed; // アイテムの落下速度を設定する

            yield return new WaitForSeconds(span); // 一定時間待つ
        }

    }
}

このコードはUnityでゲームオブジェクトを生成するためのクラスで、以下のような機能が実装されています。

  • アイテム生成の間隔、落下速度、ボムが出現する確率を設定する(SetParameterメソッド)
  • アイテムの生成を開始する(GenerateStartメソッド)
  • アイテムの生成を停止する(GenerateStopメソッド)
  • アイテムの生成を繰り返すコルーチン(Repeatメソッド)

具体的には、生成するアイテムの種類として、アップルとボムの2種類のプレハブを持っています。アイテム生成の間隔、落下速度、ボムが出現する確率は、SetParameterメソッドで設定されます。

GenerateStartメソッドを呼び出すと、Repeatメソッドをコルーチンとして開始します。Repeatメソッドでは、アイテムの種類をランダムに決定し、位置と落下速度を設定して生成します。一定時間待機した後、再度アイテムを生成する処理を繰り返します。

GenerateStopメソッドを呼び出すと、Repeatメソッドのコルーチンを停止することができます。

GameDirector

GameDirector

using System.Collections;  // IEnumerator型を使うために必要な名前空間をインポート
using TMPro;  // TextMeshProUGUI型を使うために必要な名前空間をインポート
using UnityEngine;  // Unityの基本的な機能を使用するために必要な名前空間をインポート

public class GameDirector : MonoBehaviour  // MonoBehaviourクラスを継承するGameDirectorクラスを定義
{
    float time = 30.0f;  // 制限時間の初期値を設定
    int point = 0;  // 得点の初期値を設定

    [SerializeField]  // privateな変数をインスペクタから設定できるようにする
    ItemGenerator generator;  // ItemGeneratorクラスのインスタンスを格納する変数

    [SerializeField]  // privateな変数をインスペクタから設定できるようにする
    TextMeshProUGUI TimeText;  // 制限時間を表示するTextMeshProUGUIオブジェクトを格納する変数

    [SerializeField]  // privateな変数をインスペクタから設定できるようにする
    TextMeshProUGUI ScoreText;  // 得点を表示するTextMeshProUGUIオブジェクトを格納する変数

    [SerializeField]  // privateな変数をインスペクタから設定できるようにする
    ItemParameter itemParameter;  // ItemParameterクラスのインスタンスを格納する変数

    public int Point  // pointフィールドのプロパティ
    {
        get => point;  // 値を取得するときの処理

        set  // 値を設定するときの処理
        {
            point = value;  // pointフィールドに値を設定

            ScoreText.text = $"{Point} point";  // 得点を表示するTextMeshProUGUIオブジェクトのテキストを更新
        }
    }

    void Start()  // 最初に実行されるメソッド
    {
        StartCoroutine(GenerateItem());  // GenerateItemメソッドをコルーチンとして実行

        InvokeRepeating(nameof(TimerUpdate), 0, 1);  // 1秒ごとにTimerUpdateメソッドを実行するように設定
    }

    private void TimerUpdate()  // 制限時間を更新するメソッド
    {
        time--;  // 制限時間を1秒減らす

        if (time < 0)  // 制限時間が0以下になったら
        {
            return;  // メソッドを終了する
        }

        TimeText.text = $"{time:F1}";  // 制限時間を表示するTextMeshProUGUIオブジェクトのテキストを更新
    }

    private IEnumerator GenerateItem()  // アイテムを生成するコルーチン
{
    for (int i = 0; i < itemParameter.generateParams.Length; i++)  // ItemParameterクラスのgenerateParams配列の要素数分ループ
    {
        generator.SetParameter(itemParameter.generateParams[i]);  // ItemGeneratorクラスのパラメータを設定

        generator.GenerateStart();  // アイテム生成を開始する

        var timeLap = itemParameter.generateParams[i].timeLapse;  // アイテム生成間隔を取得
        yield return new WaitForSeconds(timeLap);  // 次のアイテム生成まで待つ

        generator.GenerateStop();  // アイテム生成を停止する
    }
}

以上が、GameDirectorクラスのコードについてのコメントです。主な機能は、制限時間の表示と更新、得点の計算と表示、アイテムの生成と管理です。Coroutineを使用して、アイテム生成を非同期的に処理することで、ゲームの処理をスムーズに実行しています。また、SerializeField属性を使用して、privateな変数をインスペクタから設定できるようにしています。

パラメータ登録処理

スクリプタブルオブジェクトを登録するためのコード

このファイルには、2つのクラスが記述されています

GenerateParam
ItemParameter

using System; // C#言語で書かれたコードであることを示す
using UnityEngine; // Unityのエンジンを利用していることを示す

[Serializable] // シリアライズ可能なクラスであることを示す属性
public class GenerateParam // アイテム生成に必要なパラメータを持つクラス
{
    public float timeLapse; // アイテム生成までの時間間隔
    public float span; // アイテム生成の間隔
    public float speed; // アイテムの移動速度
    public int ratio; // アイテムの生成割合
}

[CreateAssetMenu(fileName = "ItemParam", menuName = "Custom/ItemParam")] // ScriptableObjectを作成するための属性
public class ItemParameter : ScriptableObject // アイテム生成パラメータを管理するScriptableObject
{
    public GenerateParam[] generateParams; // アイテム生成パラメータの配列
}

クラスはシリアライズ可能であり、アイテム生成に必要なパラメータを持つGenerateParamクラスと、アイテム生成パラメータを管理するItemParameterクラスが定義されています。また、ItemParameterクラスはScriptableObjectを継承しており、アイテム生成パラメータの配列を持っています

スクリプタブルオブジェクトファイルの作成方法

パラーメータの初期値の入力

Prefab(プレファブ)

applePrefab

ItemController

using UnityEngine; // UnityエンジンのAPIを使用するための宣言

public class ItemController : MonoBehaviour // ItemControllerクラスの宣言(MonoBehaviourクラスを継承)
{
    public float dropSpeed = -2f; // 落下速度を制御するための変数

    void Update() // 毎フレーム呼ばれる処理
    {
        // アイテムを落下させる処理(y方向に落下する)
        transform.Translate(0, this.dropSpeed * Time.deltaTime, 0);

        // アイテムが画面外に出た時、アイテムを破壊する
        if (transform.position.y < -1.0f)
        {
            Destroy(gameObject);
        }
    }
}

このスクリプトは、Unityでゲームオブジェクトにアタッチされることを想定しています。アタッチされたオブジェクトを上方向から落下させ、画面外に出た時には自動的にオブジェクトを破壊します。 Updateメソッドは毎フレーム呼ばれるため、オブジェクトはフレームごとに下に移動します。

ItemGetter

using UnityEngine;

public class ItemGetter : MonoBehaviour
{
    public AudioClip itemSE; // アイテムを取った際に再生する効果音

    public AudioSource aud; // AudioSourceコンポーネント

    public void TouchBasket() // バスケットに触れたときに呼び出される関数
    {
        // アイテムを取った際に効果音を再生する
        // PlayClipAtPointメソッドを使用して、itemSEを再生する位置にあるオブジェクトで音を再生する
        AudioSource.PlayClipAtPoint(itemSE, transform.position);

        // アイテムを取った後、このスクリプトがアタッチされているゲームオブジェクトを削除する
        Destroy(gameObject);
    }
}

このスクリプトは、以下のような動作をします。

  1. アイテムを取得すると、TouchBasket() 関数が呼び出されます。
  2. AudioSource.PlayClipAtPoint() メソッドを使用して、アイテムを取った際に再生する効果音を再生します。再生する位置は、アイテムを取得するオブジェクトの位置であり、 transform.position で取得できます。
  3. Destroy() メソッドを使用して、このスクリプトがアタッチされているゲームオブジェクトを削除します。これにより、アイテムが取得されたことを表現します。

以上のように、このスクリプトはアイテムを取得するときに効果音を再生し、自身を削除することでアイテムが取得されたことを表現する機能を持っています。

ItemUpScore

using UnityEngine;

// MonoBehaviourクラスを継承しているItemUpScoreクラスを宣言
// IUpdatableインターフェースを実装している
public class ItemUpScore : MonoBehaviour, IUpdatable
{
    // アイテムによって加算される得点を表す変数
    public int point;

    // IUpdatableインターフェースで定義されているメソッドを実装
    // score: 更新前のスコア値
    // pointを加算したスコア値を返す
    public int UpdateScore(int score)
    {
        return score += point;
    }
}

このコードはUnityのゲームオブジェクトにアタッチされるItemUpScoreクラスを定義しています。このクラスは、IUpdatableインターフェースを実装していることに注目してください。

このクラスには、pointというpublicなint型の変数があります。この変数は、アイテムを取得した時に加算する得点数を表します。

また、UpdateScoreというpublicなint型のメソッドがあります。このメソッドは、現在のスコアを引数として受け取り、point変数の値を現在のスコアに加算して、加算後のスコアを返します。

つまり、このクラスはアイテムを取得した時にスコアをアップデートするために使用されます。

IUpdatable

// IUpdatableという名前のpublicなインターフェースを宣言しています。
public interface IUpdatable
{
    // UpdateScoreという名前のメソッドを宣言しています。
    // このメソッドはint型の引数pointを受け取り、int型の戻り値を返します。
    int UpdateScore(int point);
}

このコードは、IUpdatableという名前のpublicなインターフェースを定義しています。このインターフェースは、UpdateScoreという名前のメソッドを定義しています。このメソッドは、int型の引数pointを受け取り、int型の戻り値を返します。このインターフェースを実装するクラスは、UpdateScoreメソッドを実装しなければなりません。また、UpdateScoreメソッドの引数と戻り値の型は、IUpdatableインターフェースで定義された型と一致しなければなりません。

bombPrefab

ItemControllerとItemGetterはapplePrefabと同じものになります

ItemHalfScore

using UnityEngine;  // Unityエンジンのライブラリを使用するためにインポート

public class ItemHalfScore : MonoBehaviour, IUpdatable  // IUpdatableインタフェースを実装するItemHalfScoreクラスを定義
{
    public int UpdateScore(int score)  // IUpdatableインタフェースで定義されたメソッドを実装する
    {
        return score / 2;  // 引数で渡されたスコアを半分にして返す
    }
}

上記のコードは、Unityゲームエンジンのライブラリを使用しているため、Unityプロジェクト内で動作することを想定しています。また、ItemHalfScoreクラスは、IUpdatableインタフェースを実装することによって、更新可能なスコアを持つアイテムを表しています。UpdateScoreメソッドは、アイテムがスコアを半分にするために使用されるものであり、引数として受け取ったスコアを半分にして返します。

クラス図