通常インベーダーに加え、2回被弾が必要な装甲シールド敵(S)や、上空を高速横切るボーナスUFO(U)が登場!C言語の「巨大なswitch-case」による構造化設計の限界と破綻を暴き、C++の抽象基底クラス・純粋仮想関数・仮想関数テーブル(vtable)の驚異的な拡張性を徹底解説します。
4.3 実装解説:BaseEnemyから広がる通常敵・シールド敵・UFOのクラス設計
実際に第4章のインベーダーゲームを動かしているC++(クラシックOOP)コードの全貌を公開します。1つの基底クラスから、いかにして多彩な敵キャラクターが生み出されるかを確認してください。
📄 Enemy.h
#pragma once
// 【第4章:抽象基底クラス Enemy】
// ポリモーフィズム(多態性)の根幹となるインターフェース定義
class Enemy {
protected:
int m_x;
int m_y;
int m_hp;
bool m_alive;
public:
Enemy(int x, int y, int hp = 1)
: m_x(x), m_y(y), m_hp(hp), m_alive(true) {}
// 【重要:仮想デストラクタ】
// 基底クラスポインタで破棄された際、派生クラスのデストラクタが必ず呼ばれるようにする
virtual ~Enemy() {}
// 【純粋仮想関数(= 0)】
// 派生クラスに実装を強制する契約
virtual void update(int dirX, bool moveDown) = 0;
virtual char getGlyph() const = 0;
virtual int getScore() const = 0;
// 共通インターフェース
virtual bool hit() {
if (!m_alive) return false;
m_hp--;
if (m_hp <= 0) {
m_alive = false;
}
return true;
}
bool isAlive() const { return m_alive; }
int getX() const { return m_x; }
int getY() const { return m_y; }
int getHp() const { return m_hp; }
};
📄 NormalEnemy.h
#pragma once
#include "Enemy.h"
// 通常インベーダー:標準的な左右往復移動
class NormalEnemy : public Enemy {
public:
NormalEnemy(int x, int y) : Enemy(x, y, 1) {}
void update(int dirX, bool moveDown) {
if (!m_alive) return;
if (moveDown) {
m_y += 1;
} else {
m_x += dirX;
}
}
char getGlyph() const {
return 'V';
}
int getScore() const {
return 100;
}
};
📄 ShieldEnemy.h
#pragma once
#include "Enemy.h"
// シールド敵:装甲耐久2。1発当たると外装が壊れて 's' になり、2発目で撃破!
class ShieldEnemy : public Enemy {
public:
ShieldEnemy(int x, int y) : Enemy(x, y, 2) {}
void update(int dirX, bool moveDown) {
if (!m_alive) return;
if (moveDown) {
m_y += 1;
} else {
m_x += dirX;
}
}
char getGlyph() const {
// 耐久値に応じて見た目が変化(カプセル化された独自表現)
return (m_hp > 1) ? 'S' : 's';
}
int getScore() const {
return 200;
}
};
📄 UfoEnemy.h
#pragma once
#include "Enemy.h"
#include "Common.h"
// ボーナスUFO:最上段を高速直進し、画面端で自滅する特別敵
class UfoEnemy : public Enemy {
private:
int m_speed;
public:
UfoEnemy(int x, int y)
: Enemy(x, y, 1), m_speed(1) {}
// 通常の編隊移動(dirXやmoveDown)を無視し、独自の飛行アルゴリズムを実行!
void update(int /*dirX*/, bool /*moveDown*/) {
if (!m_alive) return;
m_x += m_speed;
if (m_x >= SCREEN_WIDTH - 2) {
m_alive = false; // 画面外離脱
}
}
char getGlyph() const {
return 'U';
}
int getScore() const {
return 500;
}
};
📄 Game.h
#pragma once
#include <vector>
#include "Player.h"
#include "Enemy.h"
#include "Bullet.h"
#include "Particle.h"
class Game {
private:
Player m_player;
// 【重要:基底クラスのポインタを管理するコンテナ】
// NormalEnemy, ShieldEnemy, UfoEnemy すべてを多態的に一括保持!
std::vector<Enemy*> m_enemies;
std::vector<Bullet> m_bullets;
std::vector<Particle> m_particles;
int m_score;
bool m_gameOver;
bool m_gameClear;
int m_enemyDir;
int m_enemyMoveTimer;
int m_ufoTimer;
void processInput();
void update();
void render();
void spawnExplosion(int x, int y, bool isUfo = false);
public:
Game();
~Game(); // 動的確保した敵オブジェクトを一括解放
void run();
};
📄 Game.cpp
#include "Game.h"
#include "NormalEnemy.h"
#include "ShieldEnemy.h"
#include "UfoEnemy.h"
#include "Common.h"
#include <iostream>
Game::Game() : m_player(SCREEN_WIDTH / 2 - 1, SCREEN_HEIGHT - 2), m_score(0) {
// 異なる種類の敵を同じベクターに追加!
for (int col = 0; col < 6; col++) {
int x = 4 + col * 4;
if (col == 2 || col == 3) {
m_enemies.push_back(new ShieldEnemy(x, 2));
} else {
m_enemies.push_back(new NormalEnemy(x, 2));
}
}
}
Game::~Game() {
// 【重要:仮想デストラクタによる安全な解放】
// Enemy* 経由で delete しても、仮想デストラクタにより各派生クラスが正しく破棄される
for (size_t i = 0; i < m_enemies.size(); ++i) {
delete m_enemies[i];
}
}
void Game::update() {
// 【多態的ディスパッチ】
// 敵の種類を意識せず、基底ポインタ経由で update() を呼ぶだけで
// 各敵固有の実装が自動実行される!
for (size_t i = 0; i < m_enemies.size(); ++i) {
if (m_enemies[i]->isAlive()) {
m_enemies[i]->update(m_enemyDir, hitWall);
}
}
}
💡 このセクションの重要ポイント
- 【レガシーOOPの正解】開放閉鎖原則(OCP)と抽象基底クラス: 既存のGameクラスのループコードに一切手を触れずに、新しい敵クラスを追加するだけで機能拡張できる古典的オブジェクト指向の真骨頂。
- 仮想デストラクタの絶対原則: 基底クラスポインタ経由で派生オブジェクトを安全にdeleteするために、基底クラスのデストラクタは必ず virtual にする。
- 動的ディスパッチ(vtable / vptr)のコスト意識: インスタンス毎に4〜8バイトの__vptrを消費し間接呼び出しコストが生じるため、極限の省リソースでは「局所化されたswitch」や「関数ポインタテーブル」と適材適所で使い分ける。