課題6: 依存性の逆転を取り入れて BasketController の肥大化を防ぐ

2025年3月26日

依存性の逆転の原則を適用し、BasketControllerがアイテムの種類に依存しない設計に変更することで、コードの拡張性と保守性を向上させる。

ステップ:

アイテムごとの共通の動作を定義するインターフェースを作成します。これにより、BasketControllerは具体的なアイテムクラスに依存せず、インターフェースを介してアイテムとやり取りします。

インターフェースの定義:

Collectible: 収集可能

// ICollectible.cs
using UnityEngine;

public interface ICollectible
{
    void Collect(); // アイテム収集時の処理
}

ItemControllerの修正:

ICollectibleインターフェースを実装し、サウンド再生とスコア変更を行うメソッドを定義します。
GameDirectorオブジェクトのGameDirectorコンポーネントのpoint(得点)に加算します
(これまでは、GetApple()メソッドなどで処理していました)

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

public class ItemController : MonoBehaviour, ICollectible
{
    public float dropSpeed = -0.03f;
    public AudioClip itemSE;  // 各アイテムが持つサウンド
    public int scoreChange = 0; // 各アイテムが持つポイント

    GameObject director;

    void Awake()
    {
        this.director = GameObject.Find("GameDirector");
    }

    protected virtual void Start()
    {
        // 共通の初期化処理をここに記述
    }

    void Update()
    {
        // アイテムの移動
        transform.Translate(0, this.dropSpeed, 0);

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

    public virtual void Collect()
    {
        // SoundManagerを使用してサウンドを再生
        SoundManager.instance.PlaySound(this.itemSE, transform.position);
        director.GetComponent<GameDirector>().point += scoreChange;
        Destroy(gameObject);
    }
}

GameDirectorスクリプトのpointフィールドをpublicにして、アクセス可能にします

public class GameDirector : MonoBehaviour
{
・・・
    public int point = 0;
・・・

このスクリプトは、ゲーム内のアイテムの挙動や収集処理を管理するクラスです。主な役割は、アイテムの落下、画面外に出た場合の破棄、収集時のサウンド再生とスコア加算を行うことです。


クラスの概要

  • クラス名とインターフェース
    ItemControllerMonoBehaviour を継承し、ICollectible インターフェースを実装しています。これにより、ゲーム内の収集可能なアイテムとして扱えます。
  • パブリック変数
    • dropSpeed
      アイテムが落下する速さを制御します。負の値により、下方向へ移動します。
    • itemSE
      アイテム固有の効果音(AudioClip)を保持します。
    • scoreChange
      アイテムを収集したときに変更されるスコア(ポイント)です。
  • プライベート変数
    • director
      ゲーム全体の進行やスコア管理を担当する GameDirector オブジェクトへの参照を保持します。

各メソッドの解説

Awake()

  • 役割
    ゲーム開始時に一度だけ呼ばれるメソッドで、GameDirector オブジェクトを名前で検索して director に格納します。
  • 目的
    アイテムが収集された際に、スコアを加算するために GameDirector のコンポーネントへアクセスするためです。

Start()

  • 役割
    基本的な初期化処理用の仮想メソッドです。
  • 拡張性
    派生クラスでオーバーライドすることで、アイテムごとの特別な初期化処理を追加可能です。

Update()

  • アイテムの移動
    毎フレーム、transform.Translate を用いて、アイテムを dropSpeed 分だけ下方向に移動させます。
  • 画面外判定
    アイテムのY座標が -1.0f 未満になると、Destroy(gameObject) によりアイテムがシーンから破棄されます。
    これにより、画面外に残って無駄なオブジェクトが溜まるのを防ぎます。

Collect()

  • 収集処理
    アイテムが収集されたときに呼び出されるメソッドです。
  • サウンド再生
    SoundManager.instance.PlaySound を用いて、アイテム固有の効果音を現在の位置で再生します。
  • スコアの更新
    収集時に、GameDirector コンポーネントの point プロパティに scoreChange を加算し、プレイヤーのスコアを更新します。
  • オブジェクトの破棄
    最後に Destroy(gameObject) により、収集されたアイテムがシーンから削除されます。

まとめ

この ItemController クラスは、以下の点でゲーム開発における基本的なアイテム管理を実現しています。

  • アイテムの動作(落下と自動破棄)
  • 収集時のエフェクト(サウンド再生)とスコア管理
  • シングルトンパターンなど他のシステム(GameDirectorやSoundManager)との連携

これにより、拡張性のあるシンプルな収集アイテムの挙動を実装でき、ゲーム全体の管理も一元化しやすくなっています。

virtual として宣言しておくことで、派生クラス(サブクラス)側で Collect() の処理を上書き(オーバーライド)できるようになるのが大きな理由です。たとえば、ItemController を継承した BombControllerCoinController など、アイテムの種類ごとに取得時の処理を変えたいときに、Collect()override して独自の振る舞いを追加・変更できます。


例:BombController でのオーバーライド

public class BombController : ItemController
{
    public override void Collect()
    {
        // 爆弾特有の演出をここに追加
        // 例:爆発エフェクト、周囲のオブジェクトにダメージ など

        // ベースクラスの処理(サウンド再生・スコア変更・破壊)も呼びたい場合
        base.Collect();
    }
}

このように override を使ってベースクラスの Collect() を再利用しつつ、爆弾ならではの追加処理を挟むことができます。もし Collect()virtual ではなかった場合、サブクラスでオーバーライドできず、アイテムの種類ごとに異なる挙動を実装するのが難しくなってしまいます。


virtual を使うメリットまとめ

  1. サブクラスでメソッドを変更・拡張できる
  • ゲーム内で多様なアイテムを作る際、共通の処理はベースクラスで行い、個別の処理だけオーバーライドで追加するといった設計がしやすくなります。
  1. 重複コードを減らせる
  • 共通部分(サウンド再生やスコア加算・減算など)はベースクラスに書いておき、サブクラスでは必要な部分だけ上書き・追加するだけで済みます。
  1. 可読性・保守性が上がる
  • 継承関係とオーバーライドを適切に使うことで、アイテムの振る舞いの違いがコード上でもはっきりわかり、後から仕様変更があっても拡張しやすくなります。

ですので、アイテムの共通ロジックをベースクラスにまとめつつ、種類ごとの固有ロジックをサブクラスで上書きできるようにするためCollect()virtual として宣言しておくのが一般的な理由です。

C# の仕様として、基底クラスのメソッドを派生クラスで override(上書き)する場合、基底クラス側でそのメソッドに virtual、abstract、または override 修飾子を付ける必要があります。virtual を付けずに override を試みると、コンパイルエラーとなります。

もし virtual を付けずに同名のメソッドを派生クラスで定義したい場合は、new キーワードを使って隠蔽する方法もありますが、これはオーバーライドとは異なり、実行時のポリモーフィズム(動的バインディング)を得ることはできません。

つまり、ItemController の Collect() メソッドを派生クラスで正しく上書きするために、virtual として宣言する必要があるのです。

説明:

  • ICollectible インターフェースを実装することで、BasketController がアイテムの具体的なクラスに依存せずに Collect メソッドを呼び出せるようになります。
  • directorが持つ(管理する)得点に加減算されるようにします

3. アイテムごとのスクリプト作成

  • 各アイテムごとにクラスを作成し、ICollectibleインターフェースを実装します。これにより、各アイテムの固有の動作を定義できます。

例1: 爆弾アイテム

public class BombController : ItemController
{
    protected override void Start()
    {
        base.Start(); // 基底クラスのStartを呼び出す
        this.scoreChange = -50; // 爆弾はスコアを減少させる
    }

    public override void Collect()
    {
        base.Collect();
        // 爆弾特有の追加処理があればここに記述
    }
}

例2: ゴールドアップルアイテム

using UnityEngine;

public class GoldAppleController : ItemController
{
    protected override void Start()
    {
        base.Start(); // 基底クラスのStartを呼び出す
        this.scoreChange = 500; // ゴールドアップルはスコアを大幅に増加させる
    }

    public override void Collect()
    {
        base.Collect();
        // ゴールドアップル特有の追加処理があればここに記述
    }
}

残りも同様に作成します

4. アイテムごとのスプリプトを各Prefabにアタッチ

これまで各アイテムには、全てItemControllerスクリプトをアタッチしていましたが、それぞれの作成したスクリプトに置き換えます

爆弾アイテムの登録サンプル

基本クラス(ItemController)のフィールド値がインスペクターに表示されているのも確認しておきましょう

注)得点が半分になるのではなく、50点の減点になるように仕様を変えています

キャプチャーのようにインスペクター上でアイテムごとのスコアを登録することもできますが、今回はコードで代入しているので、(this.scoreChange = -50; // 爆弾はスコアを減少させる)が優先されます
この行を削除すると、インスペクターの値が採用されます

5. BasketController の修正

  • BasketControllerでは、アイテムの種類に依存せず、ICollectibleインターフェースを介してアイテムとやり取りします。

目的:

  • BasketController が具体的なアイテムクラスに依存せず、ICollectible インターフェースを介してアイテムとやり取りできるようにする。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

    void OnTriggerEnter(Collider other)
    {
        // ICollectibleインターフェースを実装しているか確認
        ICollectible collectible = other.gameObject.GetComponent<ICollectible>();
        if (collectible != null)
        {
            collectible.Collect();
        }
    }

    void Update()
    {
        float moveDistance = 1.0f; // 移動する距離(1マス)
        Vector3 newPosition = transform.position;

        if (Input.GetKeyDown(KeyCode.LeftArrow))
        {
            newPosition += Vector3.left * moveDistance; // 左に1マス移動
        }
        if (Input.GetKeyDown(KeyCode.RightArrow))
        {
            newPosition += Vector3.right * moveDistance; // 右に1マス移動
        }
        if (Input.GetKeyDown(KeyCode.UpArrow))
        {
            newPosition += Vector3.forward * moveDistance; // 前(上)に1マス移動
        }
        if (Input.GetKeyDown(KeyCode.DownArrow))
        {
            newPosition += Vector3.back * moveDistance; // 後ろ(下)に1マス移動
        }

        // グリッド範囲内に制限(例: x, z が -1, 0, 1 の範囲)
        newPosition.x = Mathf.Clamp(Mathf.Round(newPosition.x), -1.0f, 1.0f);
        newPosition.z = Mathf.Clamp(Mathf.Round(newPosition.z), -1.0f, 1.0f);

        // バスケットの位置を更新
        transform.position = new Vector3(
            Mathf.Round(newPosition.x),
            transform.position.y,
            Mathf.Round(newPosition.z)
        );
    }
}

このスクリプトは、バスケット(例えばプレイヤーが操作するキャラクターやオブジェクト)の動作と、アイテムとの衝突処理を制御する役割を持っています。以下に各部分の詳細な解説を示します。


1. Start() メソッド

  • フレームレート設定
Application.targetFrameRate = 60;


この行で、ゲームのフレームレートを1秒あたり60フレームに固定しています。

  • 目的:
    一定のフレームレートを保つことで、動作の一貫性とパフォーマンスを確保するためです。

2. OnTriggerEnter(Collider other) メソッド

  • 衝突判定
    このメソッドは、バスケットのコライダーがトリガーとして設定された他のオブジェクトと衝突した時に呼ばれます。
  • ICollectible インターフェースの確認と処理
ICollectible collectible = other.gameObject.GetComponent<ICollectible>();
if (collectible != null)
{
    collectible.Collect();
}
  • GetComponent<ICollectible>() を使って、衝突したオブジェクトが ICollectible インターフェースを実装しているか確認します。
  • 実装していれば、Collect() メソッドが呼び出され、収集処理(例えば、効果音の再生やスコア加算など)が実行されます。

3. Update() メソッド

  • 移動処理
    毎フレーム実行され、バスケットの移動をキーボード入力に基づいて行います。
  • 移動距離の設定
float moveDistance = 1.0f; // 移動する距離(1マス)
  • バスケットは1回の入力で「1マス」分の移動を行います。
  • キーボード入力による移動方向の決定
if (Input.GetKeyDown(KeyCode.LeftArrow))
{
    newPosition += Vector3.left * moveDistance; // 左に1マス移動
}
if (Input.GetKeyDown(KeyCode.RightArrow))
{
    newPosition += Vector3.right * moveDistance; // 右に1マス移動
}
if (Input.GetKeyDown(KeyCode.UpArrow))
{
    newPosition += Vector3.forward * moveDistance; // 前(上)に1マス移動
}
if (Input.GetKeyDown(KeyCode.DownArrow))
{
    newPosition += Vector3.back * moveDistance; // 後ろ(下)に1マス移動
}
newPosition.x = Mathf.Clamp(Mathf.Round(newPosition.x), -1.0f, 1.0f);
newPosition.z = Mathf.Clamp(Mathf.Round(newPosition.z), -1.0f, 1.0f);
  • Mathf.Round を用いて、計算された位置を整数(グリッド状の値)に丸めます。
  • Mathf.Clamp により、X座標とZ座標が -1.0~1.0 の範囲に収まるように制限しています。
  • 目的:
    バスケットの移動を特定のグリッド範囲(例えば、-1, 0, 1 の3マス)に固定し、プレイヤーの動きを制限します。
  • 位置の更新
transform.position = new Vector3(
    Mathf.Round(newPosition.x),
    transform.position.y,
    Mathf.Round(newPosition.z)
);
  • Y座標は変更せずに、計算された新しいX, Z座標でバスケットの位置を更新します。

まとめ

  • フレームレート固定
    ゲーム全体のスムーズな動作のために、フレームレートが60に設定されています。
  • アイテムとの衝突処理
    OnTriggerEnter で、衝突したオブジェクトが ICollectible を実装しているかをチェックし、該当する場合は収集処理を実行します。
  • グリッドに沿った移動
    Update メソッド内で、キーボード入力に基づき、バスケットは1マス単位で移動します。移動はグリッドの範囲(X, Z座標が -1~1)に制限されています。

このように、BasketController クラスは、ユーザー操作によるグリッド上の移動と、収集アイテムとのインタラクションをシンプルかつ効率的に実現しています。

コードの調整:

  • BasketControllerがアイテムの種類に依存しないため、GetApple()GetBomb()といった具体的なメソッド呼び出しを排除し、イベント駆動設計へと移行します。

テストと確認:

  • ゲームをプレイし、バスケットが各アイテムに衝突した際に、各アイテムが正しくサウンドを再生し、スコアが適切に変更されることを確認する。

学習ポイント:

  • オブジェクト指向設計の基本概念。
  • インターフェースの定義と実装方法。
  • 依存性の逆転の原則(Dependency Inversion Principle)の理解と適用。

依存性逆転の原則(DIP:Dependency Inversion Principle)とは?

依存性逆転の原則(DIP)は、SOLID原則の一つであり、以下の2つのルールから構成されます:

  1. 高レベルモジュールは低レベルモジュールに依存すべきではない。 両者は抽象(インターフェースや抽象クラス)に依存すべきである。
  2. 抽象は詳細に依存すべきではない。 詳細は抽象に依存すべきである。

この原則を適用することで、システムの柔軟性、拡張性、保守性が向上します。

リファクタリング前のクラス図

リファクタリング前のクラス図では、BasketController が GameDirector を介して ItemController に依存しています。具体的なアイテム処理が高レベルモジュール(BasketController)から低レベルモジュール(ItemController)に直接依存しているため、DIPに違反しています。

リファクタリング前のクラス図の特徴

  • BasketController
    • OnTriggerEnter メソッド内でアイテムのタグをチェックし、GameDirector の GetApple() や GetBomb() を呼び出しています。
  • DIPへの影響
    • 高レベルモジュール(BasketController)が低レベルモジュール(ItemController)に依存しています。
    • 具体的なアイテム処理が BasketController に組み込まれているため、新しいアイテムを追加する際に BasketController を変更する必要があります。
    • これはDIPに違反し、システムの柔軟性と拡張性を低下させます。

リファクタリング後のクラス図(DIP適用)

リファクタリング後のクラス図では、ICollectible インターフェースを導入し、BasketController が具体的なアイテムクラスに依存するのではなく、抽象に依存するように設計を変更しました。これにより、DIPが遵守され、システムの柔軟性と拡張性が向上しています。

リファクタリング後のクラス図の特徴

  • ICollectible インターフェース
    • Collect() メソッドを定義し、アイテム収集時の共通処理を標準化しています。
    • 高レベルモジュール(BasketController)と低レベルモジュール(ItemController およびその派生クラス)がこのインターフェースに依存します。
  • ItemController クラスと派生クラス
    • ItemController が ICollectible を実装し、基本的なアイテムの挙動(Collect() メソッド)を提供します。
    • BombController や GoldAppleController は ItemController を継承し、Collect() メソッドをオーバーライドして各アイテム特有の処理を実装します。
  • BasketController クラス
    • ICollectible インターフェースに依存するようになり、具体的なアイテムクラスには依存しません。
    • これにより、新しいアイテムタイプを追加する際に BasketController を変更する必要がなくなります。
  • ItemGenerator クラス
    • 生成するアイテムの型として ICollectible を使用し、具体的なアイテムクラスに依存しない設計に変更しました。
    • 新しいアイテムタイプを追加する際にも、ItemGenerator の変更は最小限に抑えられます。
  • DIPへの影響
    • 高レベルモジュール(BasketController)が低レベルモジュール(具体的なアイテムクラス)に依存しなくなり、抽象(ICollectible)に依存するようになりました。
    • 低レベルモジュール(具体的なアイテムクラス)が抽象(ICollectible)に依存するようになりました。

リファクタリング前後のクラス図比較による学習ポイント

リファクタリング前

  • 高レベルモジュールが低レベルモジュールに依存
    • BasketController が ItemController に依存しているため、具体的なアイテム処理に強く結合しています。
    • 新しいアイテムを追加する際に BasketController や GameDirector の変更が必要になる可能性があります。
  • 拡張性の低さ
    • 新しいアイテムタイプを追加する際に既存の高レベルモジュールに影響を与えるため、システム全体の拡張が困難です。

リファクタリング後

  • 高レベルモジュールが抽象に依存
    • BasketController が ICollectible インターフェースに依存することで、具体的なアイテムクラスには依存しなくなりました。
    • 新しいアイテムを追加する際に BasketController や GameDirector の変更が不要です。
  • 低レベルモジュールが抽象に依存
    • 具体的なアイテムクラス(BombControllerGoldAppleController)が ICollectible インターフェースを実装することで、抽象に依存しています。
    • これにより、具体的な実装の変更が高レベルモジュールに影響を与えません。
  • 拡張性と保守性の向上
    • 新しいアイテムタイプを追加する際に、ICollectible を実装する新しいクラスを作成するだけで済み、システムの拡張が容易になります。
    • モジュール間の結合度が低くなるため、保守性が向上します。

学習ポイント

  • 依存性逆転の原則(DIP)の重要性
    • 高レベルモジュールが低レベルモジュールに直接依存する設計は避け、抽象に依存することでシステムの柔軟性と拡張性を高める。
  • インターフェースの活用
    • インターフェースを用いることで、具体的な実装に依存せずにモジュール間の相互作用を設計できる。
  • ポリモーフィズムの利点
    • 派生クラスが基底クラスやインターフェースを実装することで、異なるアイテムタイプ間の共通の動作を統一的に扱える。

8. まとめ

このリファクタリングを通じて、依存性逆転の原則(DIP)を適用することで、BasketController と ItemController 間の依存関係が抽象に基づくものへと変わり、システム全体の柔軟性と拡張性が向上しました。具体的には以下の点が改善されました:

  • 高レベルモジュールが低レベルモジュールに直接依存しなくなった。
  • 新しいアイテムタイプの追加が容易になり、既存のコードに影響を与えない。
  • モジュール間の結合度が低くなり、保守性とテスト容易性が向上。

オブジェクト指向設計の原則を適用することで、コードベースの品質と可読性を高め、長期的なプロジェクトの成功に寄与します。今後もSOLID原則を念頭に置いた設計を心がけることで、より堅牢で拡張性の高いソフトウェア開発が可能となるでしょう。


Unity

Posted by hidepon