【Unreal Engine】オブジェクトプーリング入門:3個の弾を、消さずに使い回す

作成: 2026-07-20最終更新: 2026-09-07

弾を作って消す処理を、休ませて再利用する処理へ。Blueprintで3個の弾を使い回し、空きの判定、タイマーの解除、Projectile Movementの再起動、在庫切れの扱いを図解します。

弾を撃つたびに Spawn Actor で作り、飛び終わったら Destroy Actor で消す。まずはこの形で十分ですが、連射する武器や大量の敵弾では、作って片付ける処理そのものが負荷になることがあります。

そこで使えるのが オブジェクトプーリング です。先に用意した弾を休ませておき、必要なときに起こして使います。飛び終わった弾は消さず、次の出番に備えます。

この記事では、3個の弾を自動で撃つ小さな実験を作ります。弾が消えて見える間も、同じActorが残っていることを確かめてみましょう。

弾を取り出し、使い終わったら在庫へ戻す循環

この記事でわかること

  • 作って消す方式と、休ませて使い回す方式の違い
  • 使用中の判定と、タイマー・移動をリセットする理由
  • Blueprintで3個の弾を繰り返し発射する手順
  • 在庫切れの決め方と、プール化の効果の測り方

Sponsored

「消す」を「休ませる」へ変える

プール(Pool) は、使い回すものをまとめて管理する場所です。貸し出し用のボールを想像すると分かりやすいでしょう。遊ぶたびに新品を作るのではなく、空いているボールを使い、終わったら返します。

UEでは、そのボールが、ゲーム世界に置くものの単位である Actor に当たります。「返す」といっても、どこかの倉庫へ移動させる必要はありません。表示や動きを止めて、もう一度使える状態に戻すことを指します。

作って破棄する方式と、同じ弾を休ませて使い回す方式

Spawn Actor は、Actorを置くだけの処理ではありません。必要なメモリを確保し、コンポーネントを登録し、ゲーム中なら BeginPlay などの初期化も進めます。使い終わって Destroy Actor を呼ぶとActorの活動が終わり、後の ガベージコレクション(GC) で不要なメモリが回収されます。GCは、使わなくなったものを片付ける仕組みです。

プール化すると、同じ弾を何度も生成・破棄する回数を減らせます。一方、待機中の弾もメモリに残り、最初にまとめて作る負荷と、空きを管理する処理が必要です。

比べる点毎回Spawn / Destroyプールで再利用
弾を出す新しいActorを作る空いているActorを起こす
弾を消すActorを破棄する動きを止めて非表示にする
次の発射に向けた準備新規作成時の初期化に任せる前回の状態を自分で戻す
待機中のActor在庫として持たないメモリに保持する

プール化しても、ゲーム全体のGCが止まるわけではありません。 また、動いている弾の描画や当たり判定も残ります。まずは普通に作り、生成・破棄の負荷が問題になったときに検討する仕組みです。

実践:3個の弾を自動で撃つ

今回は、発射台から0.5秒ごとに球を出し、2秒後に休ませます。用意する球は3個だけです。全部を使っている間は発射を見送り、どれかが戻ったら再開します。

最初の実験では当たり判定を使わず、球がまっすぐ飛ぶところまで作ります。壁への衝突やダメージ処理を後から足せるように、再利用の仕組みを先に確認しましょう。

発射台が3個の球を使い回し、全部使用中なら次の発射を見送る

弾と、管理役を用意する

Blueprintの Third Personテンプレート でプロジェクトを用意します。コンテンツブラウザを右クリックし、「Blueprint Class」から親クラス「Actor」を選んで、次の2つを作成してください。

Blueprint役割
BP_PooledBullet1個の弾。自分を起こす処理と休ませる処理を持つ
BP_BulletPool弾3個の管理役。空いている1個に発射を頼む

BP_PooledBullet を開き、「Add」から Sphere Collision を追加して Sphere と命名します。Components欄で DefaultSceneRoot の上へドラッグし、ルートにします。その子として Static Mesh を追加して BulletMesh と命名し、さらに Projectile Movement を追加してください。

Sphereをルートに、見た目のBulletMeshと移動用コンポーネントを用意する
対象設定
SphereSphere Radius = 20、Collision Presets = NoCollision、Mobility = Movable
BulletMeshStatic Mesh = Sphere/Engine/BasicShapes/Sphere)、Scale = (0.4, 0.4, 0.4)、Collision Presets = NoCollision、Simulate Physics = オフ
Projectile MovementInitial Speed = 0、Max Speed = 1200、Projectile Gravity Scale = 0、Auto Activate = オフ、Should Bounce = オフ
Class DefaultsInitial Life Span = 0、Actor TickのStart with Tick Enabled = オフ

メッシュ選択欄でエンジンのSphereが見つからなければ、選択画面の設定から「Show Engine Content」をオンにします。色は既定の白い球のままで構いません。

ルート は、Actor全体の位置や向きの基準になる部品です。ここではProjectile MovementがSphereを動かし、子のBulletMeshも一緒に移動します。Movableは、ゲーム中に位置を変えられる設定です。

Actor自身の Event Tick は使いません。弾を動かすのはProjectile Movementなので、Actor Tickをオフにしても、そのコンポーネントの動きは別に管理する必要があります。

使用中かどうかを記録する

BP_PooledBullet の「My Blueprint」から次の変数を追加し、Compileします。

変数名初期値役割
bInUseBooleanfalseこの弾を使用中ならtrue
ReturnHandleTimer Handle未設定自動返却のタイマーを止めるための情報

bInUseb は、true/falseを入れる変数だと分かるように付けた接頭辞です。Privateはオフのままにし、管理役から読めるようにします。

見えているかどうかと、使用中かどうかを分ける のがポイントです。演出で一時的に弾を隠しても、使用中の弾を別の発射へ回さずに済みます。

弾を休ませる処理を作る

先に、使い終わった弾の戻し先を作ります。BP_PooledBullet の「Functions」の「+」から RestInPool 関数を作成してください。入力・出力は不要です。

まず Clear and Invalidate Timer by Handle を置き、ReturnHandle のGetをHandleへつなぎます。これは「あと何秒で返す」という予約を取り消す処理です。

RestInPoolの入口でReturnHandleのタイマーを解除する

タイマーを残したまま再利用すると、前回の予約によって、新しく撃った弾まで途中で消えることがあります。最初の呼び出しではまだタイマーがなくても、このノードを通して構いません。

ここでのTargetは「処理する相手」、Selfは「このBlueprint自身」を指します。弾全体を隠すときはSelf、移動を止めるときはProjectile Movementを指定します。

続いて、白い実行線を次の順につなぎます。Projectile Movement はComponents欄からグラフへドラッグし、参照を作って各Targetへ接続します。

Projectile Movementの速度をゼロにしてDeactivateする
  1. Stop Movement Immediately:TargetをProjectile Movementにし、速度をゼロにする。
  2. Deactivate:同じProjectile Movementを停止する。
  3. Set Actor Hidden In Game:TargetはSelf、New Hiddenをオンにして弾を隠す。
  4. Set bInUse:falseにして、再び空きとして使えるようにする。
移動停止の続きで非表示にし、bInUseをfalseへ戻す
BP_PooledBullet / RestInPool(入力・出力なし)
  → Clear and Invalidate Timer by Handle(ReturnHandle)
  → Stop Movement Immediately(Projectile Movement)
  → Deactivate(Projectile Movement)
  → Set Actor Hidden In Game(Self、true)
  → Set bInUse(false)

RestInPool は、最初に弾を用意した直後にも呼びます。表示と動きを止めるだけなので、すでに休んでいる弾へ呼んでも、同じ待機状態に戻せます。

今回は最初から NoCollision です。実際の攻撃用に当たり判定を足す場合は、休ませる際に Set Actor Enable Collision もオフにします。非表示にするだけでは、当たり判定は止まりません。

Sponsored

弾を起こし、2秒後に返す

次は逆の処理です。同じBlueprintに LaunchFromPool 関数を作り、入力を2つ追加します。

入力名意味
LaunchLocationVector発射する位置
LaunchRotationRotator発射する向き

位置を戻してから、動きを再開する

最初に Set bInUse をtrueにし、続いて Set Actor Location And Rotation を置きます。入力のLaunchLocationをNew Location、LaunchRotationをNew Rotationへ接続してください。TargetはSelf、Sweepはオフ、Teleportはオンにします。

使用中にしてから入力の位置と向きへ弾を戻す

前回飛び終わった場所から、新しい発射位置へ戻す処理です。Sweepを使わないため、その間を移動しながら衝突を調べることはありません。

次に Set Updated Component を置きます。TargetにはProjectile Movement、New Updated ComponentにはSphereの参照をつなぎます。

Projectile Movementの移動対象にルートのSphereを指定し直す

Updated Component は、この移動コンポーネントが動かす相手です。今回はルートのSphereを指定します。特に、後から衝突を追加して弾が停止した場合、この相手の指定が解除されることがあります。再利用時に指定し直しておけば、「一発目は飛ぶのに、二発目は動かない」という原因を一つ除けます。

速度を入れ直す

Projectile Movementの参照から Set Velocity を追加します。新しい速度は、Get Actor Forward Vector(TargetはSelf)の出力に 1200 を掛けて作ります。VectorとFloatの乗算になるように接続してください。

正面方向に1200を掛けてVelocityへ入れ、Activateする

Forward Vector は、そのActorの正面を表す長さ1のベクトルです。方向に1200を掛けると、毎秒1200cmの速度になります。直前に発射する向きへ回転させたので、毎回その方向へ飛びます。

Set Updated Component の実行出力を Set Velocity へ、さらに Activate へつなぎます。ActivateのTargetもProjectile Movement、Resetはオンです。位置・相手・速度を用意してから移動を再開する順番になります。

表示し、返却を予約する

Activateの後ろに Set Actor Hidden In Game(Self、New Hidden = オフ)をつなぎます。続いて Set Timer by Function Name を置き、次のように設定してください。

項目
ObjectSelf
Function NameRestInPool
Time2.0
Loopingオフ
弾を表示し、2秒後にRestInPoolを呼ぶタイマーのハンドルを保存する

先に作った ReturnHandle のSetを置き、Return Valueと白い実行線をつなぎます。

タイマーハンドル は、仕掛けたタイマーを後から指定するための情報です。弾を早めに返すときも、RestInPoolがこのハンドルを使って予約を取り消せます。

Function Name は文字列なので、RestInPool と正確に入力します。関数名を変更したときは、この欄も直してください。この例では引数なしの関数をタイマーから呼びます。

ここまでが1本の LaunchFromPool 関数です。図ごとに別の関数を作らず、前の図の実行出力から次の図へつなぎます。

BP_PooledBullet / LaunchFromPool(LaunchLocation、LaunchRotation)
  → Set bInUse(true)
  → Set Actor Location And Rotation(Self、入力の位置と向き)
  → Set Updated Component(Projectile Movement、Sphere)
  → Set Velocity(Projectile Movement、Get Actor Forward Vector × 1200)
  → Activate(Projectile Movement、Reset = true)
  → Set Actor Hidden In Game(Self、false)
  → Set Timer by Function Name(Self、RestInPool、2.0秒、繰り返しなし)
  → Set ReturnHandle(タイマーのReturn Value)

Initial Life Span は0のままにします。Life Spanは時間が来るとActorを破棄する設定なので、今回の「消さずに返す」という目的には合いません。

3個を用意し、空きを探す

ここからは管理役の BP_BulletPool を編集します。「My Blueprint」で次の変数を作ってCompileしてください。

変数名初期値
PoolBP_PooledBulletのObject Referenceの配列
PoolSizeInteger3

参照 は、すでに作ったActorを後から指定するためのものです。Poolには弾そのものをコピーするのではなく、3個の弾への参照を順に入れます。型をBP_PooledBulletのObject Referenceにし、型欄の右側でコンテナをArrayへ切り替えてください。配列の基本は配列・Map・Setの記事で確認できます。

BeginPlayで在庫を作る

Event Graphの Event BeginPlay から For Loop をつなぎます。First Indexは0、Last Indexは PoolSize - 1 です。今回は0・1・2の3回、Loop Bodyの処理が動きます。PoolSizeは1以上にしてください。

BeginPlayでPoolSize回だけBP_PooledBulletを生成する

Loop Bodyから Spawn Actor from Class を呼び、ClassにBP_PooledBulletを指定します。Get Actor Transform(Self)をSpawn Transformへ接続し、Collision Handling Overrideは Always Spawn, Ignore Collisions を選びます。

SpawnのReturn Valueからドラッグして RestInPool を呼び、生成した弾を休ませます。その後、Pool配列への Add を置き、Itemにも同じReturn Valueをつなぎます。

生成した弾を休ませ、その参照をPool配列に追加する
BP_BulletPool / Event BeginPlay
  → For Loop(0 ~ PoolSize - 1)
      Loop Body → Spawn Actor from Class(BP_PooledBullet)
                 → RestInPool(Target = 生成した弾)
                 → Add(Target Array = Pool、Item = 生成した弾)

「生成 → 待機 → 配列へ登録」が1個分です。BeginPlay内のループで繰り返すため、発射を始める前に3個の在庫が揃います。

使用中でない1個だけを発射する

BP_BulletPoolに FireOne 関数を作ります。入力・出力はありません。PoolのGetを For Each Loop のArrayへつなぎ、関数の入口から実行線を接続します。

Array Elementからドラッグして Get bInUse を作り、BranchのConditionへつなぎます。Loop BodyからBranchへ実行線を引いてください。

Pool配列から1個ずつbInUseを読み、使用中でない弾を探す

BranchのTrue側はつなぎません。使用中の弾では何もせず、ループが次の要素へ進みます。False側で、そのArray Elementの LaunchFromPool を呼びます。

この呼び出しのLaunchLocationには Get Actor Location(Self)、LaunchRotationには Get Actor Rotation(Self)をつなぎます。Selfは 管理役のBP_BulletPool なので、管理役を置いた位置と向きから発射されます。

空いている弾を管理役の位置と向きから発射し、Return Nodeで関数を終える

最後に Return Node を置き、LaunchFromPoolの実行出力をつなぎます。出力値は不要です。1個を発射できたら、関数ごと終了する ためのノードです。これがないと、空いている弾を一度に全部撃ってしまいます。

For Each LoopのCompleted側は、全て調べても空きがなかったときの経路です。Print StringPool empty と表示し、処理を終えます。追加のSpawnは置きません。

最後まで空きが見つからなければCompletedからPool emptyを表示する
BP_BulletPool / FireOne(入力・出力なし)
  → For Each Loop(Pool)
      Loop Body → Branch(Array ElementのbInUse)
        True  → 何もしない。次の要素へ
        False → LaunchFromPool(Target = Array Element)
                LaunchLocation = 管理役の位置
                LaunchRotation = 管理役の向き
              → Return Node(関数を終える)
      Completed → Print String(Pool empty)

発射した弾は、LaunchFromPoolの冒頭でbInUseがtrueになります。次にFireOneを呼んだときは、その弾を飛ばして別の空きを探せます。返却時も配列から削除する必要はありません。配列には全在庫を残し、bInUseだけで空きを区別する 作りです。

繰り返し撃って確かめる

自動発射のタイマーを付けます。BP_BulletPoolのEvent Graphへ戻り、在庫作成に使ったFor Loopの Completed から Set Timer by Function Name をつないでください。

在庫作成のCompletedから0.5秒間隔のFireOneタイマーを始める

ObjectはSelf、Function Nameは FireOne、Timeは 0.5、Loopingはオンです。Loop BodyではなくCompletedにつなぐことで、3個を作り終えてからタイマーを開始します。今回はPlay中ずっと試射するので、このタイマーのReturn Valueは保存しなくて構いません。

両方のBlueprintをCompileして保存してから、エディタ内のPlay(PIE)で確かめます。

  1. BP_BulletPoolをレベルの開けた場所に1個置きます。床から100cmほど上げ、Scaleは (1, 1, 1) にします。
  2. RotationのYawで発射方向を決めます。弾は管理役のローカルX方向へ進むので、Transformの赤いX軸を開けた方へ向けてください。
  3. Playし、少し離れた横から球の動きを見ます。必要なら F8 でキャラクターの操作を離れ、エディタのカメラで見渡します。
  4. PIE実行中のWorld Outlinerで BP_PooledBullet を検索し、弾が3個のままか確認します。これはエディタ内のPlayで行う確認です。

0.5秒ごとに発射を試み、各弾は2秒で非表示になります。3個とも使用中なら Pool empty が出て、その回は撃ちません。どれかが戻ると、また同じ弾を発射します。

続いて、Playを止めてBP_BulletPoolを開き、PoolSizeの初期値を 6 に変更してCompileします。もう一度試すと在庫に余裕ができ、通常の0.5秒間隔の試射では空きを見つけやすくなります。3個の場合と比べると、在庫不足による発射の間隔 が分かります。

さらに向きを変えて再生し直すと、球も新しい方向へ飛びます。休ませた弾の速度を、発射時に入れ直しているためです。

症状確認する場所
最初から球が見えているSpawn直後に、生成した弾をTargetとしてRestInPoolを呼んだか
1回で3個とも飛ぶFireOneでLaunchFromPoolの直後にReturn Nodeがあるか
一度飛ぶと戻ってこないタイマーのFunction NameがRestInPoolと一致し、Timeが2.0になっているか
戻ったはずなのにPool emptyが続くRestInPoolの最後でbInUseをfalseへ戻しているか
球が見えるだけで動かないSphereがルートか。Set Updated Componentの相手、Velocity、ActivateのTargetを確認
球の数が増え続けるFireOneにSpawnを追加していないか。在庫を作るのはBeginPlayだけ

ここで確認するのは、速さの改善より先に 同じActorを使い回せていること です。球3個の実験だけで、性能差が出る必要はありません。

Sponsored

在庫切れと、プールの大きさを決める

用意した全てを使用中で、空きがなくなることを 枯渇 と呼びます。そのときの動きは、ゲーム側で決めます。

在庫切れで発射を省略する、追加する、古い弾を置き換える選択肢
方針画面で起きること考えておくこと
今回は出さない弾や演出が1回省略される装飾なら省略できても、プレイヤーの攻撃を欠落させてよいかは別
新しく1個作る要求どおりに出るその瞬間はSpawnの負荷があり、在庫とメモリが増える。上限も決める
使用中の古い1個を戻す古い弾が消え、新しい弾になる途中の攻撃や当たり判定も消える。ダメージ数字など、置き換えてよいものに使う

今回の実験は、違いを見やすくするため「今回は出さない」を選びました。完成したゲームへそのまま採用するかは、武器や演出の仕様で判断してください。

在庫数の最初の目安は、 1秒間に出す数 × 使っている秒数 です。毎秒10発を撃ち、最大3秒残すなら約30個。連射の重なりや処理タイミングを考え、そこへ余裕を持たせます。複数の武器で同じプールを使う場合は、合計の発射数で考えます。

実際にプレイして、同時使用数の最大値や発射を見送った回数を記録すると、必要な在庫を決めやすくなります。大量に用意すれば安心というわけではなく、待機分のメモリと初期生成の時間も一緒に見ます。

ゲームに組み込み、効果を測る

当たったときも、破棄せず返す

試射ができたら、Spawn ActorとProjectile Movementの記事を参考にSphereの当たり判定を設定できます。その際は、当たった弾が Destroy Actor へ進んでいた箇所を、 RestInPool へ置き換えます。

ただし、休ませる・起こす処理も一緒に補います。RestInPoolでは当たり判定をオフにし、LaunchFromPoolでは 位置・速度・返却タイマーの準備が済んだ後 にオンへ戻します。SphereのNoCollision設定自体を攻撃用の設定へ変えないと、Actor側だけオンにしても衝突しません。

Hitイベントの最初でbInUseを調べ、trueのときだけダメージ処理と返却を進めるようにします。使用中でない弾への通知で、同じ攻撃を重ねて処理しないためです。Projectile Movementが衝突で停止した後も、LaunchFromPoolでUpdated Componentと速度を入れ直し、再び動かせることを確認してください。

同じ条件でSpawn版と比べる

性能を比べるときは、通常のSpawn / Destroy版と、プール版で 発射数、弾が残る時間、メッシュ、当たり判定 を揃えます。在庫切れで撃つ数が減っていれば、同じ負荷での比較になりません。計測中のPrint Stringも外します。

Standalone Game などエディタ操作の影響を減らせる実行方法で、同じ場面を動かして比べます。stat unit でGameの時間を見て、必要なら stat unitgraph で時間の変化を確認します。Gameは、ゲームの処理を進めるCPU側の時間です。

発射数や当たり判定をそろえたうえで、Spawn/Destroy版とプール版のGameの時間を比べる
見るもの確かめたいこと
Gameの時間発射が集中する場面の処理時間が減ったか
Unreal Insightsの記録重い区間で、生成・初期化・破棄・GCのどれに時間がかかっているか
初期化の時間とメモリ先に作った在庫が多すぎないか
ゲームの挙動発射抜けや返却漏れで、比較条件が変わっていないか

折れ線の山が減っただけで「GCが止まった」とは判断できません。詳しい原因はUnreal Insightsの記事で紹介している記録から調べます。基本の数字の読み方はstatコマンドの記事を参照してください。

差がほとんどなければ、元の生成・破棄が十分軽かった可能性があります。プールの管理を増やす利点があるか、測った結果から判断しましょう。

おまけ:先に知っておくと良いこと

再利用ではBeginPlayが毎回動くわけではありません。 HP、ダメージ量、撃った相手、エフェクトの状態などをBeginPlayだけで設定していると、前の発射の情報が残ります。使うたびに変わる値は、LaunchFromPoolへ渡すか、返却時に戻す処理へまとめます。ダメージ数字のプールなら、前回の数字・位置・アニメーションの進み具合が対象です。

Actorのプールは、そのWorldで管理します。 Worldは、その時点で動いているゲーム世界です。GameInstanceやそのSubsystemに参照を保存しても、通常のレベル切り替えで破棄されるActorをそのまま持ち越せるわけではありません。まずは今回のように専用Actorを置き、レベル開始時に在庫を用意する形で十分です。GameInstanceの記事では、持ち越すデータとActorの違いを扱っています。

大量の在庫は、作るタイミングも調整します。 BeginPlayでまとめて生成すれば、初期化の負荷がその時点に集まります。数が多い場合は、読み込み画面中に用意する、何フレームかに分けるなどの方法を検討します。

返却時に止めるものは、部品ごとに違います。 今回はProjectile MovementをDeactivateしましたが、AIの思考、繰り返す音、エフェクト、別のコンポーネントのTickまで自動で止まるわけではありません。複雑な敵を丸ごと再利用する前に、弾やダメージ数字のように状態が少ないものから試すと、戻し忘れを追いやすくなります。

まとめ

プーリングは、使い終わったActorを休ませ、次の出番で起こす仕組みです。今回の例では、配列に全在庫を残し、bInUseで空きを選びました。返却時にはタイマーと移動を止め、発射時には位置・向き・速度を入れ直しています。

最初に確かめたいのは、同じ弾が何度も使えて、在庫切れのときも決めたとおりに動くことです。そのうえで元のSpawn版と比べ、生成・破棄を減らす効果があるか測ってみてください。

Actorの出し消しより「まだ使わないメッシュまで読み込まれている」ことが気になるなら、次はソフト参照と非同期ロードへ進みます。

参考:ActorのライフサイクルProjectile Movement Componentタイマーの使い方

Unreal Engine このセクションのノート98