22:クラス設計演習 応用編1 — Enemy Rush

C++ コンソールミニゲームを段階的に作りながら、継承・オーバーライド・抽象クラスを体験する演習です。

学習目標

前提:C++17以降。標準入出力のみ。ファイル名は game_stepXX.cpp
各ステップは 前のファイルをコピーして差分を書き加えて作ること。

Step 01: 継承元の骨組みを作る

ファイル:22_step01_base.cpp
目標:基底 GameObject と派生 Enemy。名前とHPを表示するだけ。
// game_step01.cpp
#include <iostream>
#include <string>
using namespace std;

class GameObject {
public:
    string name;
    void show() const { cout << "名前: " << name << "\n"; }
};

class Enemy : public GameObject {
public:
    int hp = 10;
    void info() const { cout << "HP: " << hp << "\n"; }
};

int main() {
    Enemy e;
    e.name = "スライム";
    e.hp = 12;
    e.show();
    e.info();
}

Step 02: オーバーライドとポリモーフィズム

ファイル:22_step02_override.cpp
目標Enemyvirtual attack() を用意。SlimeGoblin が上書きする。
// game_step02.cpp  <- game_step01.cpp をコピーして改造
#include <iostream>
#include <string>
using namespace std;

class GameObject {
public:
    string name;
    void show() const { cout << "名前: " << name << "\n"; }
};

class Enemy : public GameObject {
public:
    int hp = 10;
    virtual void attack() { cout << name << " が殴ってきた!\n"; }
    virtual ~Enemy() = default; // 後で動的確保するので仮想デストラクタ
};

class Slime : public Enemy {
public:
    void attack() override { cout << "スライムが体当たり!\n"; }
};

class Goblin : public Enemy {
public:
    void attack() override { cout << "ゴブリンが棍棒!\n"; }
};

int main() {
    Enemy* es[2] = { new Slime(), new Goblin() };
    es[0]->name = "ぷるぷる"; es[1]->name = "ゴブさん";
    for (int i = 0; i < 2; i++) {
        es[i]->show();
        es[i]->attack();
        delete es[i];
    }
}

Step 03: 抽象クラス化して設計を強制

ファイル:22_step03_abstract.cpp
目標Enemy を抽象クラスにし、attack() を純粋仮想に。
// game_step03.cpp  <- step02 をコピーして改造
#include <iostream>
#include <string>
using namespace std;

class GameObject {
public:
    string name;
    void show() const { cout << "名前: " << name << "\n"; }
};

class Enemy : public GameObject {
public:
    int hp = 10;
    virtual void attack() = 0; // 純粋仮想
    virtual ~Enemy() = default;
};

class Slime : public Enemy {
public:
    void attack() override { cout << "スライムが体当たり!\n"; }
};

class Goblin : public Enemy {
public:
    void attack() override { cout << "ゴブリンが棍棒!\n"; }
};

int main() {
    Enemy* e1 = new Slime(); e1->name = "ぷるぷる";
    Enemy* e2 = new Goblin(); e2->name = "ゴブさん";
    Enemy* es[2] = { e1, e2 };
    for (int i = 0; i < 2; i++) { es[i]->show(); es[i]->attack(); }
    for (int i = 0; i < 2; i++) delete es[i];
}

Step 04: 範囲forで一覧処理

ファイル:22_step04_vector.cpp
目標vector<Enemy*> と範囲forで全体処理。
// game_step04.cpp  <- step03 をコピー
#include <iostream>
#include <string>
#include <vector>
using namespace std;

class GameObject {
public:
    string name;
    void show() const { cout << "名前: " << name << "\n"; }
};

class Enemy : public GameObject {
public:
    int hp = 10;
    virtual void attack() = 0;
    virtual ~Enemy() = default;
};

class Slime : public Enemy {
public:
    void attack() override { cout << "スライムが体当たり!\n"; }
};

class Goblin : public Enemy {
public:
    void attack() override { cout << "ゴブリンが棍棒!\n"; }
};

int main() {
    vector<Enemy*> enemies;
    auto s = new Slime();  s->name = "ぷるぷる"; enemies.push_back(s);
    auto g = new Goblin(); g->name = "ゴブさん"; enemies.push_back(g);

    for (Enemy* e : enemies) {
        e->show();
        e->attack();
    }
    for (Enemy* e : enemies) delete e;
}

Step 05: 動的生成と削除の体験 (new/delete と寿命)

ファイル:22_step05_dynamic.cpp
目標:ターンごとに敵が湧き、倒れたら delete~Enemy() のログで寿命を確認。
// game_step05.cpp  <- step04 をコピー
#include <iostream>
#include <string>
#include <vector>
#include <limits>
using namespace std;

class GameObject {
public:
    string name;
    void show() const { cout << "名前: " << name << "\n"; }
};

class Enemy : public GameObject {
public:
    int hp = 10;
    virtual void attack() = 0;
    virtual void takeDamage(int d) { hp -= d; }
    bool isDead() const { return hp <= 0; }
    virtual ~Enemy() { cout << name << " が消滅\n"; }
};

class Slime : public Enemy {
public:
    Slime() { hp = 18; }
    void attack() override { cout << "スライムの体当たり!\n"; }
};

class Goblin : public Enemy {
public:
    Goblin() { hp = 25; }
    void attack() override { cout << "ゴブリンの棍棒!\n"; }
};

int main() {
    vector<Enemy*> enemies;
    const int DAMAGE = 12;
    int turnMax = 6;

    for (int turn = 1; turn <= turnMax; ++turn) {
        // 出現(動的生成)
        if (turn % 2) {
            auto e = new Slime();  e->name = "スライム#" + to_string(turn); enemies.push_back(e);
        } else {
            auto e = new Goblin(); e->name = "ゴブリン#" + to_string(turn); enemies.push_back(e);
        }

        cout << "\n--- Turn " << turn << " ---\n";
        int idx = 1;
        for (Enemy* e : enemies) {
            cout << idx++ << ") "; e->show();
        }

        cout << "攻撃対象番号 (0でスキップ, -1で終了): ";
        int choice;
        if (!(cin >> choice)) { cin.clear(); cin.ignore(numeric_limits<streamsize>::max(), '\n'); choice = 0; }

        if (choice == -1) break;
        if (choice > 0 && choice <= (int)enemies.size()) {
            Enemy* t = enemies[choice - 1];
            t->takeDamage(DAMAGE);
            cout << "→ " << t->name << " に " << DAMAGE << " ダメージ\n";
            if (t->isDead()) {
                cout << "撃破! delete 実行\n";
                delete t;
                enemies.erase(enemies.begin() + (choice - 1));
            }
        }

        // 敵の行動
        for (Enemy* e : enemies) e->attack();
    }

    // 残骸掃除
    for (Enemy* e : enemies) delete e;
}

Step 06: 追加のオーバーライドで差別化

ファイル:22_step06_dragon.cpp
目標Dragon を追加。被ダメ軽減など挙動差分を増やす。
// game_step06.cpp  <- step05 をコピーし Dragon を追加
// (step05の全クラス定義はそのまま)

class Dragon : public Enemy {
public:
    Dragon() { name = "ドラゴン"; hp = 40; }
    void attack() override { cout << "ドラゴンの炎ブレス!\n"; }
    void takeDamage(int d) override { hp -= (d - 3 > 0 ? d - 3 : 0); }
};

// main() は step05 をそのまま使いつつ、出現ロジックを追加:
// if (turn % 3 == 0) { Dragon } else if (turn % 2) { Slime } else { Goblin }

Step 07: 仕上げの小要素 (範囲forの参照・全体処理)

ファイル:22_step07_final.cpp
目標:範囲forの参照で全員にバフ/デバフをかけるなどの全体更新を追加。
// 例: ターン開始時に全員が小回復(HP+1)
// vector<Enemy*> enemies; がある前提
for (Enemy*& e : enemies) {
    e->hp += 1; // 参照で書き換え。値コピーにしないこと。
}

最終課題 — 各自のオリジナル仕上げ

下から好きに 2つ以上実装。設計とコードを提出。動作ログまたは短い動画を添付。

  1. スコアとコンボの実装。
  2. クリティカル/回避(乱数)。
  3. 状態異常:毒・炎上・鈍足。
  4. ターン経過で出現ペースや敵種が変わる。
  5. unique_ptr<Enemy> 版に書き換え(delete忘れ対策)。
  6. Boss 追加。特性:行動が2回、HP閾値でフェーズ移行。
  7. ログ出力のオン/オフ(-DDEBUG マクロやフラグで制御)。
提出コメントに含めること

注意点と評価観点

注意点
評価観点

理解度チェック

問題22-1

抽象クラス Enemyvirtual ~Enemy()(仮想デストラクタ)を定義する理由として最も適切なものはどれか。

解説を表示 正解:イ
Enemy* e = new Slime(); delete e; のように基底クラスのポインタで派生クラスを削除するとき、デストラクタが仮想でないと Enemy のデストラクタしか呼ばれず、Slime のリソースがリークする。仮想デストラクタにすることで正しく派生クラスのデストラクタも呼ばれる。

問題22-2

vector<Enemy*> と範囲forを使って全敵に attack() を呼ぶコードとして正しいものはどれか。

解説を表示 正解:ウ
vector<Enemy*> はポインタを格納しているので、範囲forの変数も Enemy* 型になる。メンバ関数へのアクセスはポインタなので -> 演算子を使う。

問題22-3

次のコードで、enemies から倒れた敵を削除して delete するとき、正しい順序はどれか。

if (t->isDead()) {
    // ??? の順序
}
解説を表示 正解:ア
delete してからベクタの要素を削除するのが安全。erase 後に delete をすると、すでに無効なポインタにアクセスする危険がある。また delete だけではベクタに無効ポインタが残る。