モンスターハンターで学ぶオブジェクト指向
本資料「モンスターハンターで学ぶオブジェクト指向」は、配列まで学習した初学者がスムーズにクラス設計へ進めるよう、人気アクションゲーム『モンスターハンター』(以下 MH)を題材に作成しました。
MH には「ハンター」「モンスター」「武器」といった分かりやすい“役者”が揃っており、現実世界の概念をコードへ写し取るというオブジェクト指向(OOP)の第一歩を体験する教材として最適です。
目的
- 抽象語を減らす
「クラス」「インスタンス」「継承」などの専門用語を、実在するゲーム要素に置き換えることで、イメージしづらい概念を身体感覚に近付けます。 - “データ+振る舞い” のまとまりを体感
配列ベース実装とクラス導入後の実装を並置し、可読性・保守性の差異を自分の目で確認します。 - ゲームで得た学びを現場スキルへ橋渡し
OOP の基礎を身に付けた後、職業訓練校で扱う業務アプリや Unity プロジェクトへスムーズに応用できるよう設計しています。
モンスターハンターの世界を “クラス図” に置き換えてみよう
現実のゲーム要素 | OOP での呼び名 | 代表プロパティ | 代表メソッド |
---|---|---|---|
ハンター | Hunter クラス | Name, HP, Stamina, Weapon | Attack(), Dodge(), UseItem() |
モンスター | Monster クラス(抽象) | Name, MaxHP, CurrentHP, Element | Roar(), TakeDamage(int), DropItem() |
武器 | Weapon クラス(基底) | Type, AttackPower, Sharpness | CalcDamage(), Sharpen() |
クエスト | Quest クラス | TargetMonster, TimeLimit, Reward | Start(), 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} は空中へ舞い上がった!");
}
何がポイントか
- ポリモーフィズム
Monster[] に派生クラスを混在させ、TakeTurn() を呼ぶだけで大型・小型それぞれの挙動に自動で切り替わります。 - 共通化と差分最小化
HP など共通プロパティは基底 Monster に集約し、派生クラスでは“差分”だけを書くので重複が減ります。 - 型固有機能の呼び出し
フィールドやメソッドが派生にしか無い場合は、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. 学習へ繋げるには
- 導入 5 分
Monster Hunter のプレイ経験を聞きながら「現実世界の“もの”=クラス」の発想へ。 - 配列 vs クラス 実演 10 分
上記サンプルをリアルタイムで書き換え、可読性/保守性の差を体感。 - ミニワーク 15 分
表の課題を個人 or 2 人組で実装(チーム開発練習にも最適)。 - 発表 & フィードバック 10 分
“責務の分割” “再利用性” など OOP らしい観点でコメント。 - 次回予告
List<Monster> でコレクションを柔軟に扱う(リスト未習のクラスでは 紹介だけ)。
まとめ
- クラス は “モノの設計図”、インスタンス は “実体”
- 配列だけ ではデータが散逸しがち。クラス導入 で「まとまり」が生まれる
- 継承 で共通点をまとめ、インターフェース で “できること” を宣言
- ゲームの具体物に置き換えると 抽象的な概念が格段に理解しやすくなる
Monster Hunter という親しみやすい題材をフックに、“クラスは何なのか”“なぜ必要か” を 体験ベース で学べる構成にすると、初学者でもオブジェクト指向の第一歩を踏み出しやすくなります。
ディスカッション
コメント一覧
まだ、コメントがありません