モンスターハンターで学ぶオブジェクト指向

 本資料「モンスターハンターで学ぶオブジェクト指向」は、配列まで学習した初学者がスムーズにクラス設計へ進めるよう、人気アクションゲーム『モンスターハンター』(以下 MH)を題材に作成しました。

 MH には「ハンター」「モンスター」「武器」といった分かりやすい“役者”が揃っており、現実世界の概念をコードへ写し取るというオブジェクト指向(OOP)の第一歩を体験する教材として最適です。

目的

  1. 抽象語を減らす
    「クラス」「インスタンス」「継承」などの専門用語を、実在するゲーム要素に置き換えることで、イメージしづらい概念を身体感覚に近付けます。
  2. “データ+振る舞い” のまとまりを体感
    配列ベース実装とクラス導入後の実装を並置し、可読性・保守性の差異を自分の目で確認します。
  3. ゲームで得た学びを現場スキルへ橋渡し
    OOP の基礎を身に付けた後、職業訓練校で扱う業務アプリや Unity プロジェクトへスムーズに応用できるよう設計しています。

モンスターハンターの世界を “クラス図” に置き換えてみよう

現実のゲーム要素OOP での呼び名代表プロパティ代表メソッド
ハンターHunter クラスName, HP, Stamina, WeaponAttack(), Dodge(), UseItem()
モンスターMonster クラス(抽象)Name, MaxHP, CurrentHP, ElementRoar(), TakeDamage(int), DropItem()
武器Weapon クラス(基底)Type, AttackPower, SharpnessCalcDamage(), Sharpen()
クエストQuest クラスTargetMonster, TimeLimit, RewardStart(), Complete()

“モンスター” や “武器” はゲーム内では「もの・登場人物」ですが、OOP では クラス で表現し、実際にゲームを動かすときに インスタンス(オブジェクト) として生成します。


1. 配列だけで管理した場合(非 OOP)

// モンスター 3体の名前と HP を別々の配列で管理
string[] names = {"リオレウス", "ジンオウガ", "ラージャン"};
int[] maxHP  = {4500, 3700, 5000};
int[] currentHP = {4500, 3700, 5000};

// リオレウスに 320 ダメージ
int index = 0;           // リオレウスは先頭
currentHP[index] -= 320;
Console.WriteLine($"{names[index]} の残り HP: {currentHP[index]}");

配列が増えるにつれ管理が煩雑に。

「モンスター名」と「HP」を間違えて取り出すバグに気づきにくい。

2. Monster クラスを導入してみる

public class Monster
{
    public string Name;
    public int MaxHP;
    public int CurrentHP;

    public Monster(string name, int maxHP)
    {
        Name = name;
        MaxHP = CurrentHP = maxHP;
    }

    // 行動をメソッドとしてまとめられる
    public void TakeDamage(int amount)
    {
        CurrentHP = Math.Max(CurrentHP - amount, 0);
        Console.WriteLine($"{Name} は {amount} ダメージ! 残り {CurrentHP}/{MaxHP}");
    }
}

// --- 使う側 ---
Monster rathalos = new Monster("リオレウス", 4500);
rathalos.TakeDamage(320);

データ(プロパティ)と振る舞い(メソッド)が 1 つにまとまるので、「誰が何をするか」が読み取りやすくなります。

3. 継承で “大型” と “小型” を分ける

public class LargeMonster : Monster
{
    public bool CanFly;

    public LargeMonster(string name, int maxHP, bool canFly)
        : base(name, maxHP)
    {
        CanFly = canFly;
    }

    public void Roar()
    {
        Console.WriteLine($"{Name} が咆哮! ハンターは耳をふさぐ");
    }
}
// 基底クラス
public class Monster
{
    public string Name;
    public int MaxHP;
    public int CurrentHP;

    public Monster(string name, int maxHP)
    {
        Name      = name;
        MaxHP     = maxHP;
        CurrentHP = maxHP;
    }

    public virtual void TakeTurn()
    {
        Console.WriteLine($"{Name} は様子をうかがっている…");
    }
}

// 大型モンスター
public class LargeMonster : Monster
{
    public bool CanFly;

    public LargeMonster(string name, int maxHP, bool canFly)
        : base(name, maxHP)
    {
        CanFly = canFly;
    }

    public override void TakeTurn()
    {
        Roar();                 // 大型だけが持つ行動
    }

    public void Roar()
    {
        Console.WriteLine($"{Name} が咆哮! ハンターは耳をふさぐ");
    }
}

// 小型モンスター
public class SmallMonster : Monster
{
    public SmallMonster(string name, int maxHP)
        : base(name, maxHP) { }

    public override void TakeTurn()
    {
        Console.WriteLine($"{Name} が小突いてきた!");
    }
}

// ---------------- 使う側 ----------------
var mizutsune = new LargeMonster("タマミツネ", 3200, canFly:false);
var jagras    = new SmallMonster("ジャグラス", 240);

Monster[] field = { mizutsune, jagras };

foreach (Monster m in field)
{
    m.TakeTurn();      // 基底型でまとめて扱える!
}

// 型固有のメンバーにアクセスしたい場合はキャスト or 'is' パターン
if (mizutsune is LargeMonster lm && lm.CanFly)
{
    Console.WriteLine($"{lm.Name} は空中へ舞い上がった!");
}

何がポイントか

  1. ポリモーフィズム
    Monster[] に派生クラスを混在させ、TakeTurn() を呼ぶだけで大型・小型それぞれの挙動に自動で切り替わります。
  2. 共通化と差分最小化
    HP など共通プロパティは基底 Monster に集約し、派生クラスでは“差分”だけを書くので重複が減ります。
  3. 型固有機能の呼び出し
    フィールドやメソッドが派生にしか無い場合は、is パターンマッチやキャストで呼び分ければ OK。(Unity の GetComponent<LargeMonster>() も同じ発想です)

この使い方を実演すると、「同じ配列で扱えるのに、動きはモンスターごとに違う」という“継承+ポリモーフィズム”の威力が直感的に伝わります。

LargeMonster は Monster を「拡張」したサブクラス。

共通部分を 再利用 できるのが継承の大きな利点です。

4. インターフェースで “攻撃できるもの” を抽象化

public interface IAttackable
{
    void Attack(Monster target);
}

public class Hunter : IAttackable
{
    public string Name;
    public Weapon Equipped;

    public Hunter(string name, Weapon weapon)
    {
        Name = name;
        Equipped = weapon;
    }

    public void Attack(Monster target)
    {
        int dmg = Equipped.CalcDamage();
        target.TakeDamage(dmg);
        Console.WriteLine($"{Name} が {Equipped.Type} で攻撃!");
    }
}
// ---------------- インターフェースと実装 ----------------
public interface IAttackable
{
    void Attack(Monster target);
}

// ハンター
public class Hunter : IAttackable
{
    public string Name;
    public Weapon Equipped;

    public Hunter(string name, Weapon weapon)
    {
        Name = name;
        Equipped = weapon;
    }

    public void Attack(Monster target)
    {
        int dmg = Equipped.CalcDamage();
        target.TakeDamage(dmg);
        Console.WriteLine($"{Name} が {Equipped.Type} で {dmg} ダメージ!");
    }
}

// オトモアイルー
public class Palico : IAttackable
{
    public string Nickname;

    public Palico(string nickname) => Nickname = nickname;

    public void Attack(Monster target)
    {
        int dmg = 30;
        target.TakeDamage(dmg);
        Console.WriteLine($"アイルー {Nickname} がサポート攻撃!({dmg} ダメージ)");
    }
}

// 置き型爆弾
public class BarrelBomb : IAttackable
{
    public int Power;

    public BarrelBomb(int power) => Power = power;

    public void Attack(Monster target)
    {
        target.TakeDamage(Power);
        Console.WriteLine($"大タル爆弾が爆発! {Power} ダメージ!");
    }
}

// (参照用)簡易モンスター
public class Monster
{
    public string Name;
    public int CurrentHP;

    public Monster(string name, int hp)
    {
        Name = name;
        CurrentHP = hp;
    }

    public void TakeDamage(int amount)
    {
        CurrentHP = Math.Max(CurrentHP - amount, 0);
        Console.WriteLine($"{Name} の残り HP: {CurrentHP}");
    }
}

// ---------------- 使う側 ----------------
Weapon gs = new Weapon { Type = "大剣", BasePower = 180 };
IAttackable[] attackers =
{
    new Hunter("コニシ", gs),
    new Palico("モモ"),
    new BarrelBomb(300)
};

Monster rathalos = new Monster("リオレウス", 1000);

// まとめて攻撃!
foreach (IAttackable atk in attackers)
{
    atk.Attack(rathalos);
}

ここで押さえておきたいポイント

観点何が嬉しいか
統一的な型IAttackable配列(または List<IAttackable>) に異なるオブジェクトを混在させ、一括で Attack() を呼び出せる。
拡張が容易新しく「操虫棍の猟虫」「設置型機雷」などを追加しても IAttackable を実装するだけで既存コードは変更不要(オープン/クローズド原則)。
依存性の逆転「攻撃できるかどうか」だけに依存し、具体的クラス(Hunter など)の詳細には依存しないため再利用性が高い。

実演アイデア

  • 配列に “爆弾だけ” 追加して再実行 → コード修正ゼロ で新しい攻撃が機能する様子を見せる
  • IAttackable を実装していないオブジェクトを配列に入れようとすると コンパイルエラー になることを確認し、型安全性を体感させる

このように インターフェース=「できること」の約束 を軸に設計すると、ゲームでも業務アプリでも “将来の拡張” がラクになることが直感的に理解できます。

インターフェース は「共通の能力だけを約束」する仕組み。
将来「オトモアイルー」や「罠」も IAttackable にすれば、
Attack() さえ実装すれば同じ扱いでコードが書けます。

5. 10〜20 分ワーク

内容目標ヒント
Weapon クラスに Sharpness を追加し、CalcDamage() に反映せよクラスのフィールドとメソッドを関連付けるゲージが下がるほど攻撃力を 0.8 倍, 0.6 倍…
Quest を配列 (Quest[]) で 3 件作成し、ハンターが順番に受注インスタンス配列の生成・操作foreach で回し、Quest.Start()→ Complete()
LargeMonster を継承した ElderDragon を実装し、咆哮の後に特殊攻撃継承チェーンとオーバーライドoverride void Roar() で派生クラス専用の処理

6. 学習へ繋げるには

  1. 導入 5 分
    Monster Hunter のプレイ経験を聞きながら「現実世界の“もの”=クラス」の発想へ。
  2. 配列 vs クラス 実演 10 分
    上記サンプルをリアルタイムで書き換え、可読性/保守性の差を体感。
  3. ミニワーク 15 分
    表の課題を個人 or 2 人組で実装(チーム開発練習にも最適)。
  4. 発表 & フィードバック 10 分
    “責務の分割” “再利用性” など OOP らしい観点でコメント。
  5. 次回予告
    List<Monster> でコレクションを柔軟に扱う(リスト未習のクラスでは 紹介だけ)。

まとめ

  • クラス は “モノの設計図”、インスタンス は “実体”
  • 配列だけ ではデータが散逸しがち。クラス導入 で「まとまり」が生まれる
  • 継承 で共通点をまとめ、インターフェース で “できること” を宣言
  • ゲームの具体物に置き換えると 抽象的な概念が格段に理解しやすくなる

Monster Hunter という親しみやすい題材をフックに、“クラスは何なのか”“なぜ必要か” を 体験ベース で学べる構成にすると、初学者でもオブジェクト指向の第一歩を踏み出しやすくなります。

訪問数 4 回, 今日の訪問数 4回