課題7: GameDirectorの肥大化対策としてイベントシステムの導入

2024年9月28日

目的GameDirector が各アイテムの詳細に依存せず、イベント駆動設計を用いてスコア管理を行うことで、GameDirector の肥大化を防ぎ、コードの拡張性と保守性を向上させる。

背景

現在の設計では、GameDirectorが各アイテム(リンゴ、爆弾、ゴールドアップルなど)ごとに専用のメソッド(GetApple()GetBomb()GetGoldApple())を持っています。これにより、新しいアイテムを追加するたびにGameDirectorに新しいメソッドを追加する必要があり、GameDirectorが肥大化してしまいます。

目標

  • GameDirectorが具体的なアイテムの種類に依存せず、汎用的な方法でスコア管理を行う。
  • イベント駆動設計を導入し、アイテムが収集された際にGameDirectorに通知する仕組みを構築する。
  • 新しいアイテムを追加する際に、GameDirectorを変更する必要がない設計にする。

ステップ:

1. スコアイベントの定義

  • アイテムが収集された際に発行されるイベントを定義します。このイベントは、収集されたアイテムのスコア増減情報を含みます。
// ScoreEventArgs.cs
using System;

public class ScoreEventArgs : EventArgs
{
    public int ScoreChange { get; private set; }

    public ScoreEventArgs(int scoreChange)
    {
        ScoreChange = scoreChange;
    }
}

2. ICollectible インターフェースの拡張

  • ICollectible インターフェースに、収集時にスコアを通知するイベントを追加します。
// ICollectible.cs
using System;

public interface ICollectible
{
    void Collect(); // アイテム収集時の処理
    event EventHandler<ScoreEventArgs> OnCollected; // スコア変更イベント
}

3. ItemController の修正

  • 各アイテムが収集された際に、スコア変更イベントを発行するように ItemController を修正します。
  • OnCollected イベントを発行し、GameDirectorにスコア変更を通知します。
using System;
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; // 各アイテムが持つポイント

    public event EventHandler<ScoreEventArgs> OnCollected;

    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);

        // スコア変更イベントを発行
        OnCollected?.Invoke(this, new ScoreEventArgs(scoreChange));

        Destroy(gameObject);
    }
}

4. 各アイテムクラスの修正

  • 各アイテムごとにスコア変更量を設定します。例えば、爆弾はスコアを減少させ、ゴールドアップルはスコアを増加させます。

爆弾アイテム

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

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

ゴールドアップルアイテム

using UnityEngine;

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

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

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

  • GameDirectorが各アイテムの具体的な種類を知らずに、スコアを管理できるようにします。アイテムから発行されたスコア変更イベントを受け取って処理します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro; // TextMeshProを使う時は忘れないように注意!!

public class GameDirector : MonoBehaviour
{
    GameObject timerText;
    GameObject pointText;
    float time = 30.0f;
    public int point = 0;
    GameObject generator;

    void Start()
    {
        this.timerText = GameObject.Find("Time");
        this.pointText = GameObject.Find("Point");
        this.generator = GameObject.Find("ItemGenerator");
    }

    void Update()
    {
        this.time -= Time.deltaTime;

        if (this.time < 0)
        {
            this.time = 0;
            this.generator.GetComponent<ItemGenerator>().SetParameter(10000.0f, 0, 0);
        }
        else if (0 <= this.time && this.time < 4)
        {
            this.generator.GetComponent<ItemGenerator>().SetParameter(0.3f, -0.06f, 0);
        }
        else if (4 <= this.time && this.time < 12)
        {
            this.generator.GetComponent<ItemGenerator>().SetParameter(0.5f, -0.05f, 6);
        }
        else if (12 <= this.time && this.time < 23)
        {
            this.generator.GetComponent<ItemGenerator>().SetParameter(0.8f, -0.05f, 4);
        }
        else if (23 <= this.time && this.time < 30)
        {
            this.generator.GetComponent<ItemGenerator>().SetParameter(1.0f, -0.03f, 2);
        }

        this.timerText.GetComponent<TextMeshProUGUI>().text = this.time.ToString("F1");
        UpdateScoreUI();
    }

    // スコア変更イベントハンドラ
    public void HandleScoreChange(object sender, ScoreEventArgs e)
    {
        this.point += e.ScoreChange;
        UpdateScoreUI();
    }

    // スコアUIの更新
    private void UpdateScoreUI()
    {
        this.pointText.GetComponent<TextMeshProUGUI>().text = this.point.ToString() + " point";
    }
}

ポイント:

  • 汎用的なスコア管理:
    • HandleScoreChange メソッドで、発行されたスコア変更イベントを受け取り、スコアを更新します。これにより、GameDirectorは具体的なアイテムの種類を知らずにスコア管理が可能です。

6. ItemGenerator の修正

  • 新しく生成されるアイテムに対して、GameDirectorのスコア変更イベントハンドラを登録します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ItemGenerator : MonoBehaviour
{
    public GameObject applePrefab;
    public GameObject bombPrefab;
    public GameObject goldApplePrefab; // ゴールドアップルのプレハブを追加
    float span = 1.0f;
    float delta = 0;
    int ratio = 2;
    float speed = -0.03f;

    public void SetParameter(float span, float speed, int ratio)
    {
        this.span = span;
        this.speed = speed;
        this.ratio = ratio;
    }

    void Update()
    {
        this.delta += Time.deltaTime;
        if (this.delta > this.span)
        {
            this.delta = 0;
            GameObject item;
            int dice = Random.Range(1, 11);
            if (dice <= this.ratio)
            {
                item = Instantiate(bombPrefab);
            }
            else if (dice == this.ratio + 1) // ゴールドアップルの出現割合を1に設定
            {
                item = Instantiate(goldApplePrefab);
            }
            else
            {
                item = Instantiate(applePrefab);
            }
            float x = Random.Range(-1, 2);
            float z = Random.Range(-1, 2);
            item.transform.position = new Vector3(x, 4, z);
            item.GetComponent<ItemController>().dropSpeed = this.speed;

            // GameDirectorのインスタンスを取得し、イベントハンドラを登録
            GameDirector director = GameObject.Find("GameDirector").GetComponent<GameDirector>();
            ICollectible collectible = item.GetComponent<ICollectible>();
            if (collectible != null)
            {
                collectible.OnCollected += director.HandleScoreChange;
            }
        }
    }
}

テストと確認:

  • ゲームをプレイし、バスケットが各アイテムに衝突した際に、各アイテムが正しくサウンドを再生し、GameDirectorのスコアが適切に更新されることを確認する。
  • 新しいアイテムを追加した際に、GameDirectorを変更せずにスコア管理が行われることを確認する。

学習ポイント:

  • イベント駆動設計の基本。
  • 依存性の逆転の原則の適用。
  • インターフェースを用いた柔軟なコード設計。

補足:

イベントの解除:

OnDestroyメソッドはUnityのイベントで定義されているもので、ゲームオブジェクトが消滅(Destroy)される直前に実行されます

void OnDestroy()
{
    OnCollected -= HandleScoreChange;
}

スコア管理の拡張:

  • 将来的にスコア以外の要素(例: ライフ、パワーアップなど)を管理する場合も、同様にイベント駆動設計を活用することで、GameDirectorの肥大化を防ぐことができます。

依存性注入(Dependency Injection)の導入:

  • プロジェクトがさらに大規模になる場合、依存性注入のフレームワーク(例: VContainerなど)の導入を検討すると、依存関係の管理が容易になります。

Unity

Posted by hidepon