0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ゲーム開発における「モジュール化」の考え方

Posted at

ゲーム開発していると、コードの設計時に毎回頭を悩ませますよね。
「きちんとモジュール化すれば再利用しやすい」と言われますが、仕様変更とかで後から依存関係が増えたり、インターフェースに定義がどんどん増えていったりする経験をした人は多いんじゃないでしょうか?
あれこれ考え抜いた末に、結局“神クラス”や“神インターフェース”が生まれてしまった…なんて、よくある話だと思います。

基本的に適切なモジュール化をしようとしていた時期がありましたが、最近はある種の諦め+解のような考え方に落ち着いたので、今回はそれをまとめてみようと思います。

今日はその理由と、一つの解決策を具体的なUnityのコード例を通して考えてみましょう。

なぜすべてをモジュール化すると非効率なのか?

ゲームアプリケーションは仕様が変化しやすく、開発後半で「あれもこれも追加したい」といった要望が頻繁に発生しますよね。
最初から完璧にモジュール化したつもりでも、アプリケーション側(ゲームプレイやUI)に近いコードほど後から予期しない仕様変更が起こりやすく、結果として無理な共依存関係が生じがちです。
これ、「あ~わかるわかる!」って思う人も多いんじゃないでしょうか。

たとえば、プレイヤーの状態管理を考えてみましょう。

モジュール化した例(最初の設計)

// プレイヤーの状態を管理するインターフェース
public interface IPlayerStatus {
    void TakeDamage(int amount);
    void Heal(int amount);
}

// プレイヤークラス
public class Player : MonoBehaviour, IPlayerStatus {
    private int hp = 100;

    public void TakeDamage(int amount) {
        hp -= amount;
        // HPバー表示処理などを呼び出す
        UIManager.Instance.UpdateHP(hp);
    }

    public void Heal(int amount) {
        hp += amount;
        UIManager.Instance.UpdateHP(hp);
    }
}

ここまではシンプルに見えますが、途中で「ダメージを受けた時に特定のエフェクトや音を再生する」「特定の敵からの攻撃はダメージを半減させる」などの要求が生まれた場合、このインターフェースや実装を変更する必要が出てきます。

結局以下のようなことになりがちです。

// 後から追加された複雑な仕様
public void TakeDamage(int amount, EnemyType type) {
    int finalDamage = (type == EnemyType.Fire) ? amount / 2 : amount;
    hp -= finalDamage;

    UIManager.Instance.UpdateHP(hp);

    if (type == EnemyType.Fire) {
        EffectManager.Instance.Play("FireHitEffect");
        SoundManager.Instance.Play("FireHitSound");
    }
}

更にしようが追加され、責務を諦めた状態

と思ったら、EnemyのStatusとかも取得して計算したいとかいう要望が来て、「じゃあEnemyのGameObject丸ごと渡してGetComponentで全部取っちゃおう」となり、もう責務を諦めた神インターフェースになりがちです。

public void TakeDamage(int amount, GameObject enemy) {
    var enemyStatus = enemy.GetComponent<Status>();
    int finalDamage = (enemyStatus.type == EnemyType.Fire) ? amount / 2 : amount;
    hp -= finalDamage;

    UIManager.Instance.UpdateHP(hp);

    if (enemyStatus.type == EnemyType.Fire) {
        EffectManager.Instance.Play("FireHitEffect");
        SoundManager.Instance.Play("FireHitSound");
    }
}

このように、当初の綺麗なモジュール化が破綻し、責務が曖昧になります。
しかもインターフェースを変えるたびに実装クラスの方も全書き換えしなきゃいけないので、変更量が増えて大変ですよね。

最初から直書きしていたらどうだったか?

もし最初から完璧なモジュール化をせずに、「必要なものだけ書く」スタイルで進めていた場合は次のようになります。

仕様変更前の直書きコード

public class Player : MonoBehaviour {
    private int hp = 100;

    public void TakeDamage(int amount) {
        hp -= amount;
        UIManager.Instance.UpdateHP(hp);
    }
}

最初の段階はシンプルそのものです。

仕様変更後の直書きコード

仕様が追加されたら、それにあわせて明確に新しいメソッドを追加します。インターフェースじゃないので、複数の実装場所を気にしないで良いぶん、気軽にスピーディに変更できます。

public class Player : MonoBehaviour {
    private int hp = 100;

    public void TakeDamage(int amount) {
        hp -= amount;
        UIManager.Instance.UpdateHP(hp);
    }

    // 追加仕様に明確に対応したメソッドを追加
    public void TakeFireDamage(int amount, Status enemyStatus) {
        int finalDamage = amount / 2; // Fireタイプなので半減
        hp -= finalDamage;

        UIManager.Instance.UpdateHP(hp);
        EffectManager.Instance.Play("FireHitEffect");
        SoundManager.Instance.Play("FireHitSound");
    }
}

こちらはメソッドを増やしただけなので、コードの責務が曖昧になることはなく、読みやすさを維持できます。
「とりあえず直書きしておいて、あとで本当に必要になったらリファクタリングする」でもいいのでは?っていう考え方ですね。

システム側のコードはモジュール化すると効果的

逆にサウンドやエフェクトのようなシステム側のコードは、ある程度落ち着いた仕様が決まったらモジュール化しておくほうがメンテナンスが楽です。

良いモジュール化の例(SoundManager)

public interface ISoundManager {
    void Play(string soundName);
}

public class SoundManager : MonoBehaviour, ISoundManager {
    private Dictionary<string, AudioClip> clips;

    public void Play(string soundName) {
        AudioClip clip;
        if (clips.TryGetValue(soundName, out clip)) {
            AudioSource.PlayClipAtPoint(clip, Vector3.zero);
        }
    }
}

これをモジュール化していない場合、以下のように問題が発生します。

モジュール化しない場合の悪い例(直書き)

public class Player : MonoBehaviour {
    public AudioClip fireHitSound;
    private int hp = 100;

    public void TakeFireDamage(int amount) {
        hp -= amount / 2;
        UIManager.Instance.UpdateHP(hp);
        AudioSource.PlayClipAtPoint(fireHitSound, Vector3.zero);
    }
}

これは一見シンプルに見えますが、サウンドを変更したり、複数箇所で再生処理をする際に修正箇所が多くなり、後々非常に困るパターンですね。
「こんなとこにもハードコーディングしてたのか…」みたいになりがち。

結論:「日々書くコードは設計を気にしないで良い」

  • アプリケーション側のコード(プレイヤーや敵、UIなど)は、完璧なモジュール化よりも「読みやすくて、柔軟に変更できるコード」を優先するほうが、結果的に開発がスムーズに進むことが多いです
  • システム側のコード(サウンドやエフェクトなど)は、モジュール化しておけば変更に強く、メンテナンス性も高いです

リファクタリングは、エンバグが出て開発スピードが落ちるとか、重複コードがあちこちに増えて作業が面倒になってきた時に行えば十分だと思います。
シンプルにしてあるほうが、いざ実装を捨てるときの心理的ハードルも下がって「理論上はできるけどやりたくない…」みたいな空気を減らせますし、ゲーム開発全体が円滑に進みやすいんですよね。
設計に悩みすぎず、日々の開発はシンプルに、柔軟性重視で進める。これがゲームの品質を上げるポイントにもなるんじゃないでしょうか。

0
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?