矢を発射するゲームのリファクタリングガイド

この資料では、Unityで作成されたシンプルな矢を発射するゲームのリファクタリングについて解説します。コードのリファクタリングにより、可読性、メンテナンス性、再利用性を向上させ、より良い設計を目指します。本資料を通じて、オブジェクト指向プログラミングやクリーンコードの基本概念を学ぶことができます。


1. ArrowControllerのリファクタリング

元のコード

public class ArrowController : MonoBehaviour
{
    GameObject player;

    void Start()
    {
        this.player = GameObject.Find("player");
    }
    
    void Update()
    {
        // フレームごとに等速で落下させる
        transform.Translate(0, -0.1f, 0);

        // 画面外に出たらオブジェクトを破棄する
        if(transform.position.y < -5.0f)
        {
            Destroy(gameObject);
        }

        // 当たり判定
        Vector2 p1 = transform.position;                
        Vector2 p2 = this.player.transform.position;    
        Vector2 dir = p1 - p2;
        float d = dir.magnitude;
        float r1 = 0.5f;    
        float r2 = 1.0f;    

        if(d < r1 + r2)
        {
            GameObject director = GameObject.Find("GameDirector");
            director.GetComponent<GameDirector>().DecreaseHp();
            Destroy(gameObject);
        }
    }
}

リファクタリング後のコード

public class ArrowController : MonoBehaviour
{
    private GameObject player;
    private const float fallSpeed = 0.1f;
    private const float boundaryY = -5.0f;
    private const float arrowRadius = 0.5f;
    private const float playerRadius = 1.0f;

    void Start()
    {
        player = GameObject.FindWithTag("Player");
    }

    void Update()
    {
        Fall();
        CheckOutOfBounds();
        CheckCollisionWithPlayer();
    }

    private void Fall()
    {
        transform.Translate(0, -fallSpeed, 0);
    }

    private void CheckOutOfBounds()
    {
        if (transform.position.y < boundaryY)
        {
            Destroy(gameObject);
        }
    }

    private void CheckCollisionWithPlayer()
    {
        Vector2 arrowPos = transform.position;
        Vector2 playerPos = player.transform.position;
        float distance = Vector2.Distance(arrowPos, playerPos);

        if (distance < arrowRadius + playerRadius)
        {
            GameDirector.Instance.DecreaseHp();
            Destroy(gameObject);
        }
    }
}

変更点と改善点

  1. メソッド分割: 矢の落下処理、画面外チェック、衝突判定をそれぞれ別メソッドに分け、可読性と再利用性を向上させました。
  2. 定数化: マジックナンバーを定数として明示し、コードの意図が明確になりました。
  3. FindWithTag: プレイヤーをタグで見つけることで、コードの柔軟性を高めました。

2. ArrowGeneratorのリファクタリング

元のコード

public class ArrowGenerator : MonoBehaviour
{
    public GameObject arrowPrefab;
    float span = 1.0f;
    float delta = 0;

    void Update()
    {
        this.delta += Time.deltaTime;
        if(this.delta > this.span)
        {
            this.delta = 0;
            GameObject go = Instantiate(arrowPrefab);
            int px = Random.Range(-6, 7);
            go.transform.position = new Vector3(px, 7, 0);
        }
    }
}

リファクタリング後のコード

public class ArrowGenerator : MonoBehaviour
{
    public GameObject arrowPrefab;
    private const float spawnInterval = 1.0f;
    private float timer = 0;

    void Update()
    {
        timer += Time.deltaTime;
        if (timer >= spawnInterval)
        {
            timer = 0;
            GenerateArrow();
        }
    }

    private void GenerateArrow()
    {
        GameObject arrow = Instantiate(arrowPrefab);
        arrow.transform.position = new Vector3(Random.Range(-6, 7), 7, 0);
    }
}

変更点と改善点

1. this の使用

  • 元のコードでは this.delta や this.span という形で this を使っていますが、リファクタリング後のコードでは this が使われていません。C# では this はインスタンスメンバーにアクセスする場合に省略できますが、統一的に使うか省略するかを決めると良いです。
  • this をつけるのは、例えばパラメータ名とメンバー変数名が同じときに明示するためや、可読性を高めるために使う場合があります。今回のようなケースでは省略して問題ないでしょう。

2. アクセス修飾子の追加

  • リファクタリング後のコードで private を明示的に追加しています。クラス内での変数やメソッドに対して適切なアクセス修飾子を指定することは、意図しない外部アクセスを防ぎ、コードの保守性を高めます。
  • private を付けるべきかどうかは、そのメンバーがクラス外部で使用されるかどうかに依存します。外部に公開する必要がなければ private を付けるのが適切です。span や delta にアクセス修飾子がなかったのは、クラス外からアクセスされるべきではないのに明示されていないという点で改善の余地があります。

3. 命名規則の改善

  • 元のコードでは span や delta という名前を使用していましたが、リファクタリング後のコードでは spawnInterval や timer というより具体的で意図が伝わりやすい名前に変更されています

3. メソッドの分割

  • 元の矢の生成処理をGenerateArrow()メソッドとして分割しました。これにより、更新処理と生成処理が明確に分離され、拡張が容易になりました。

3. GameDirectorのリファクタリング

元のコード

public class GameDirector : MonoBehaviour
{
    GameObject hpGauge;

    void Start()
    {
        this.hpGauge = GameObject.Find("hpGauge");
    }

    public void DecreaseHp()
    {
        this.hpGauge.GetComponent<Image>().fillAmount -= 0.1f;
    }
}

リファクタリング後のコード

public class GameDirector : MonoBehaviour
{
    private GameObject hpGauge;

    private static GameDirector instance;
    public static GameDirector Instance => instance;

    void Awake()
    {
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            Destroy(gameObject);
        }
    }

    void Start()
    {
        hpGauge = GameObject.Find("hpGauge");
    }

    public void DecreaseHp()
    {
        hpGauge.GetComponent<Image>().fillAmount -= 0.1f;
    }
}

変更点と改善点

  1. シングルトンパターンGameDirectorをシングルトンパターンに変更し、他のスクリプトからInstanceプロパティを介して簡単にアクセスできるようにしました。
  2. 冗長なコードの削減GameObject.Findの呼び出しを減らし、アクセスの手間を省きました。

4. PlayerControllerのリファクタリング

元のコード

public class PlayerController : MonoBehaviour
{
    void Start()
    {
        Application.targetFrameRate = 60;
    }

    void Update()
    {
        // 左矢印が押された時
        if(Input.GetKeyDown(KeyCode.LeftArrow))
        {
            transform.Translate(-3, 0, 0); // 左に「3」動かす
        }

        // 右矢印が押された時
        if (Input.GetKeyDown(KeyCode.RightArrow))
        {
            transform.Translate(3, 0, 0); // 右に「3」動かす
        }
    }
}

リファクタリング後のコード

public class PlayerController : MonoBehaviour
{
    private const float moveDistance = 3.0f;

    void Start()
    {
        Application.targetFrameRate = 60;
    }

    void Update()
    {
        HandleMovement();
    }

    private void HandleMovement()
    {
        if (Input.GetKeyDown(KeyCode.LeftArrow))
        {
            Move(-moveDistance);
        }
        else if (Input.GetKeyDown(KeyCode.RightArrow))
        {
            Move(moveDistance);
        }
    }

    private void Move(float distance)
    {
        transform.Translate(distance, 0, 0);
    }
}

変更点と改善点

  1. メソッド分割Move()メソッドに移動処理をまとめ、コードの再利用性を高めました。
  2. 定数化: 移動距離を定数として宣言し、将来的な変更が容易になりました。

おわりに

今回のリファクタリングでは、コードの分割やシングルトンパターンの導入、定数の利用を通して、コードの可読性や拡張性を大きく向上させました。リファクタリングにより、将来的な機能追加や修正が容易になり、バグの発生を抑えることができます。

参考

Unity

Posted by hidepon