今回は、ゲーム開発でよく出てくる「フライウェイトパターンの使い方」について解説していきます。
「フライウェイトパターンって何?」
「敵を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つだけ持つ
- 個体ごとのデータを共有すると全員が連動する事故になる
フライウェイトは、敵や弾を大量に出すゲームでこそ効いてくるパターンです。まずは「このデータ、全員同じでは?」と疑うところから始めてみてください。


