【C++】フライウェイトパターンとは?|大量の敵でメモリを節約する方法

C++

今回は、ゲーム開発でよく出てくる「フライウェイトパターンの使い方」について解説していきます。

「フライウェイトパターンって何?」
「敵を100体出したらメモリがすごいことになった…」
「同じ画像を何回も読み込んでる気がする…」

こんな疑問はありませんか?

ゲームで敵やブロックを大量に出すと、1体ずつが画像やモデルを持ってしまい、同じデータがメモリ上に何百個もコピーされることがあります。フライウェイトパターンは、その「みんな同じ部分」を1つにまとめて共有する考え方です。

この記事を読み終えると、あなたはフライウェイトパターンの意味・実装方法・ゲームでの使いどころ・初心者がハマりやすいミスをしっかり理解できると思いますので、ぜひ最後まで読んでいただけると嬉しいです。

フライウェイトパターンとは?

フライウェイトパターンとは、大量のオブジェクトで「共通するデータ」を1つにまとめて共有し、メモリを節約するパターンです。名前の「フライウェイト(flyweight)」はボクシングの階級と同じで、軽量という意味です。

ポイントは、オブジェクトのデータを次の2つに分けることです。

  • 共有できるデータ(内因的状態)… 画像、モデル、最大HP、攻撃力など。どの個体でも同じ
  • 個体ごとに違うデータ(外因的状態)… 座標、現在HP、向きなど。1体ずつ違う

この「同じ部分」だけを1つにして、全員でそれを参照します。デザインパターン全体の位置づけはデザインパターンをゲーム開発で活用する方法の記事もどうぞ。

なぜフライウェイトが必要?

理由は、同じデータのコピーがメモリを食い潰すからです。まず、やってしまいがちな書き方を見てください。

#include <string>

// 悪い例:1体ずつが画像データを丸ごと持っている
class Enemy {
    std::string textureName; // 全員同じ "slime.png"
    int maxHp;               // 全員同じ 100
    int attack;              // 全員同じ 10
    float x, y;              // ここだけ個体差がある
};

これだと、スライムを500体出したら "slime.png" という情報が500個コピーされます。実際の画像データまで各自が持っていたら、それだけでメモリが吹き飛びます。違うのは座標だけなのに、同じ物を500回持っているのが問題です。

フライウェイトパターンの実装

では、共有する部分を切り出してみます。まず「全員で同じ」データをまとめたクラスを作ります。

#include <iostream>
#include <string>
#include <memory>
#include <unordered_map>
#include <vector>

// 共有される側(フライウェイト本体):全個体で同じデータ
class EnemyType {
public:
    EnemyType(std::string name, std::string texture, int maxHp, int attack)
        : name_(std::move(name)), texture_(std::move(texture)),
          maxHp_(maxHp), attack_(attack) {}

    const std::string& GetName() const { return name_; }
    int GetMaxHp() const  { return maxHp_; }
    int GetAttack() const { return attack_; }

private:
    std::string name_;
    std::string texture_; // 本来は重い画像データ
    int maxHp_;
    int attack_;
};

次に、この共有データを1種類につき1つだけ作って配る係を用意します。すでにあれば使い回し、なければ作る、という管理役です。

// 管理役:同じ種類なら同じ実体を返す
class EnemyTypeFactory {
public:
    std::shared_ptr<EnemyType> Get(const std::string& name) {
        auto it = pool_.find(name);
        if (it != pool_.end()) {
            return it->second; // すでにあるので使い回す
        }
        // ここでは例としてスライムだけ用意
        auto type = std::make_shared<EnemyType>(name, name + ".png", 100, 10);
        pool_[name] = type;
        return type;
    }

private:
    std::unordered_map<std::string, std::shared_ptr<EnemyType>> pool_;
};

最後に、個体側は座標と現在HPだけを持ち、共有データは参照するだけにします。

// 個体側:軽い。共有データは持たずに参照するだけ
class Enemy {
public:
    Enemy(std::shared_ptr<EnemyType> type, float x, float y)
        : type_(std::move(type)), x_(x), y_(y), hp_(type_->GetMaxHp()) {}

    void Draw() const {
        std::cout << type_->GetName() << " (" << x_ << ", " << y_
                  << ") HP:" << hp_ << "n";
    }

private:
    std::shared_ptr<EnemyType> type_; // 共有(軽い)
    float x_, y_;                     // 個体ごと
    int hp_;                          // 個体ごと
};

int main() {
    EnemyTypeFactory factory;
    std::vector<Enemy> enemies;

    // スライムを3体出す(共有データは1つだけ作られる)
    for (int i = 0; i < 3; ++i) {
        enemies.emplace_back(factory.Get("slime"), i * 50.0f, 100.0f);
    }

    for (const auto& e : enemies) e.Draw();
    return 0;
}
slime (0, 100) HP:100
slime (50, 100) HP:100
slime (100, 100) HP:100

3体出しても、EnemyType の実体は1つだけです。これが500体でも1000体でも共有データは1つのまま。個体が増えても、増えるのは座標とHPだけになります。shared_ptr の使い方はスマートポインタの記事もあわせてどうぞ。

【重要】私が実際にフライウェイトで困った体験談

個人開発で弾幕シューティングを作っていたとき、弾を1000発出したらメモリがどんどん増えて、しばらく撃ち続けるとカクつき始めるという現象にハマりました。

調べてみたら、弾クラスがテクスチャ名の文字列を1発ずつ持っていたのが原因の一つ。共有部分を BulletType に切り出して参照させたら、弾1発あたりのサイズがぐっと小さくなり、動きも安定しました。

ただし、そのとき共有データにうっかり「現在HP」を入れてしまい、1体殴ったら画面中の敵のHPが一斉に減るという大惨事も起こしました。共有していいのは「全員で同じ物」だけ、と体で覚えた出来事です。

フライウェイト使用時のよくある失敗例と対処法

①個体ごとに違うデータを共有側に入れる

体験談のとおりです。現在HP・座標・向きは個体ごとに違うので、共有側に入れると全員が連動してしまいます。「全個体で同じか?」を必ず自問してから共有側に置きましょう。

②共有データを書き換えてしまう

共有データは全員が見ているので、1か所書き換えると全員に影響します。取得関数を const にして、そもそも書き換えられないようにするのが安全です(constの記事もどうぞ)。

③種類が少ないのに導入して複雑になる

敵が数体しか出ないゲームなら、フライウェイトを入れても効果より複雑さが勝ちます。「同じ物を大量に出す」ときに初めて価値が出るパターンです。

注意点

  • 共有していいのは「全個体で同じデータ」だけ
  • 共有データは読み取り専用にしておくと事故が減る
  • 大量に出すときだけ使う。少数なら普通に持たせてよい

まとめ

  • フライウェイトは共通データを1つにまとめて共有するパターン
  • データを「全員同じ」と「個体ごと」に分けるのが肝
  • 管理役が「あれば使い回し、なければ作る」で1つだけ持つ
  • 個体ごとのデータを共有すると全員が連動する事故になる

フライウェイトは、敵や弾を大量に出すゲームでこそ効いてくるパターンです。まずは「このデータ、全員同じでは?」と疑うところから始めてみてください。

関連記事