Unity

【Unity】もう迷わない!オブジェクト指向を「実戦のコード」で理解する

Unityでゲーム開発をしていると、Enemy、Player、Itemといったクラスを自然に作ります。実はこの時点で、あなたはすでにオブジェクト指向の入り口に立っています。

しかし、以下のような疑問をスッキリ解決できているでしょうか?

  • なぜ Enemy 型の変数で Slime を扱えるのか?
  • なぜ if 文で分岐させず、Attack() を呼び出すだけでいいのか?
  • 継承ポリモーフィズムの決定的な違いは何か?

この記事では、Unityの具体的なコードを通して、「オブジェクト指向を理解して、設計に活かす」ためのエッセンスを解説します。

この記事でわかること

  • オブジェクト指向とは何か(Unity例)
  • 手続き型プログラムとの違い
  • オブジェクト指向の基本要素4つ
  • Enemy / Slime / Dragon を使った実践的な設計例

オブジェクト指向とは「役割のパッケージ化」

オブジェクト指向とは、一言で言えば「ゲーム内の登場人物や仕組みを、そのまま『独立したモノ』として定義する考え方」です。

Unityでは、GameObject にアタッチする MonoBehaviour 自体がオブジェクト指向の体現です。

手続き型 vs オブジェクト指向

かつての「手続き型」では、データと処理がバラバラになりがちでした。

特徴手続き型(if/switch依存)オブジェクト指向
中心処理の流れ(アルゴリズム)モノ(クラス)の役割
拡張性敵が増えるたびに if が増える新しいクラスを作るだけで完結
保守性どこを直せばいいか迷子になる関連するデータと処理が一箇所に集まる

手続き型の特徴

  • 処理の流れが中心
  • if / switch が増え続ける
  • 敵が増えるたびに修正が必要
int playerHp;
float playerSpeed;

void PlayerMove() {}
void PlayerAttack() {}
void EnemyAttack() {}

オブジェクト指向の特徴

  • 関連するものが1か所に集まる
  • 修正・拡張が楽
  • Unityのコンポーネント設計と相性抜群
class Player
{
    public int hp;
    public float speed;

    public void Move() {}
    public void Attack() {}
}

オブジェクト指向の4大要素:Unity実践編

Unity開発で特に重要な「カプセル化」「継承」「抽象化」「ポリモーフィズム」を解説します。

カプセル化:勝手にステータスを書き換えさせない

「HPを直接いじらせず、ダメージ計算用のメソッドを通す」のがカプセル化の基本です。

public class Player : MonoBehaviour
{
    // [SerializeField]でインスペクターには出すが、他クラスからは隠す
    [SerializeField] private int hp = 100;

    // HPへの操作は必ずこのメソッドを経由させる
    public void TakeDamage(int damage)
    {
        hp = Mathf.Max(0, hp - damage); // 0以下にならない等の制限をかけられる
        if (hp <= 0) Die();
    }

    private void Die() => Debug.Log("死亡しました");
}

抽象化 & 継承:共通のルールを決める

「敵(Enemy)なら攻撃(Attack)ができるはずだ」という共通のルールを作るのが抽象化、それを具体化するのが継承です。

// 【抽象化】Enemyとは何かを定義する(設計図)
public abstract class Enemy : MonoBehaviour
{
    public abstract void Attack(); // 具象クラスで必ず実装させる
}

// 【継承】Enemyのルールを引き継いで具体化
public class Slime : Enemy
{
    public override void Attack() => Debug.Log("スライムの体当たり!");
}

public class Dragon : Enemy
{
    public override void Attack() => Debug.Log("ドラゴンの火炎放射!");
}

ポリモーフィズム:同じ命令で違う動きをさせる

ここが一番の魔法です。相手がスライムかドラゴンかを知らなくても、「Enemy(敵)ならAttackしろ」と命令するだけで、適切な動きが実行されます。

public class BattleManager : MonoBehaviour
{
    void ExecuteEnemyTurn()
    {
        // ヒエラルキー上の全Enemyを取得(SlimeもDragonも混ざってOK)
        Enemy[] enemies = FindObjectsOfType<Enemy>();

        foreach (var enemy in enemies)
        {
            // 相手が誰であっても「Attack()」を呼ぶだけ!
            // Slimeなら体当たり、Dragonなら火炎が出る
            enemy.Attack();
        }
    }
}

なぜ「if文」より「オブジェクト指向」なのか?

もしポリモーフィズムを使わずに敵の攻撃を実装しようとすると、こうなります。

// NG例:敵が増えるたびにここを書き換える必要がある
void BadAttack(GameObject enemy)
{
    if (enemy.name == "Slime") { /* スライムの処理 */ }
    else if (enemy.name == "Dragon") { /* ドラゴンの処理 */ }
    else if (enemy.name == "Goblin") { /* 新しい敵が増えたら? */ }
}

このように if や switch で分岐させる設計は、「修正漏れによるバグ」「GameManagerの肥大化」を招きます。オブジェクト指向を使えば、GameManager は何も変えずに、新しい敵クラスを追加するだけで済みます。

まとめ

  • カプセル化:private を使い、予期せぬ数値変更を防ぐ。
  • 抽象化・継承:共通の「役割」を親クラスで作る。
  • ポリモーフィズム:型を親クラスでまとめ、命令を共通化する。

Unityの GetComponent<T>() や OnCollisionEnter(Collision collision) も、実はこのオブジェクト指向の仕組みの上で動いています。
まずは「共通の役割を持つ敵」を abstract class で作るところから始めてみませんか?