[UE5] Object Pooling Basics: Reusing Three Bullets Instead of Destroying Them

Created: 2026-07-20Last updated: 2026-09-07

Turn spawn-and-destroy into rest-and-reuse. Reuses three bullets in Blueprint and diagrams the in-use check, clearing timers, restarting Projectile Movement, and handling an empty pool.

Spawn a bullet with Spawn Actor each shot and remove it with Destroy Actor when it finishes flying. That is fine to start with, but for rapid-fire weapons and heavy enemy fire, the creating and cleaning up itself can become a cost.

That is where object pooling helps. Keep prepared bullets resting and wake one when you need it. A bullet that finished flying is not destroyed; it waits for its next turn.

This article builds a small experiment firing three bullets automatically. Confirm that the same Actors remain even while the bullets appear to be gone.

The cycle of taking a bullet out and returning it to stock when finished

What You'll Learn

  • The difference between spawn-and-destroy and rest-and-reuse
  • The in-use check, and why timers and movement are reset
  • Repeatedly firing three bullets in Blueprint
  • Deciding what to do when the pool is empty, and measuring pooling's effect

Sponsored

Turn "destroy" into "rest"

A pool is where things you reuse are managed together. Think of loaner balls. Rather than making a new ball each time you play, you use a free one and return it when done.

In UE, that ball is an Actor , the unit you place in the game world. "Returning" does not mean moving it to some warehouse. It means stopping its display and motion and restoring it to a reusable state.

Comparing spawn-and-destroy with resting and reusing the same bullet

Spawn Actor does more than place an Actor. It allocates memory, registers components, and, during play, runs initialization such as BeginPlay . Calling Destroy Actor when finished ends the Actor's activity, and a later garbage collection (GC) reclaims the unneeded memory. GC is the mechanism cleaning up what is no longer used.

Pooling reduces how often the same bullet is created and destroyed. On the other hand, resting bullets stay in memory, and you pay for creating them all up front and for the logic managing free slots.

Point of comparisonSpawn / Destroy every timeReusing from a pool
Firing a bulletCreates a new ActorWakes a free Actor
Removing a bulletDestroys the ActorStops motion and hides it
Preparing for the next shotLeft to initialization on creationYou restore the previous state
Resting ActorsNone held in stockKept in memory

Pooling does not stop the game's GC. Rendering and collision for bullets in flight also remain. Build the ordinary way first and consider this when spawn and destroy cost becomes a problem.

Hands-On: fire three bullets automatically

We fire a sphere from a launcher every 0.5 seconds and rest it after 2 seconds. Only three spheres exist. While all are in use, firing is skipped, and it resumes when one returns.

The first experiment uses no collision, building up to spheres flying straight. Confirm the reuse mechanism first so you can add wall collision and damage later.

The launcher reusing three spheres and skipping the next shot when all are in use

Prepare the bullet and the manager

Prepare a project with the Blueprint Third Person template . Right-click in the Content Browser, choose "Blueprint Class" with Actor as the parent, and create these two.

BlueprintRole
BP_PooledBulletOne bullet, holding logic to wake and rest itself
BP_BulletPoolThe manager of three bullets, asking a free one to fire

Open BP_PooledBullet , add a Sphere Collision from "Add", and name it Sphere . Drag it above DefaultSceneRoot in Components to make it the root. As its child, add a Static Mesh named BulletMesh and then a Projectile Movement .

Sphere as the root, with the visible BulletMesh and the movement component prepared
TargetSettings
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 off
Projectile MovementInitial Speed 0 , Max Speed 1200 , Projectile Gravity Scale 0 , Auto Activate off, Should Bounce off
Class DefaultsInitial Life Span 0 , Actor Tick's Start with Tick Enabled off

If the engine Sphere is missing from the mesh picker, turn on "Show Engine Content" in the picker's settings. The default white sphere color is fine.

The root is the part serving as the basis for the whole Actor's position and orientation. Here Projectile Movement moves Sphere and the child BulletMesh moves with it. Movable means it can change position during play.

We do not use the Actor's own Event Tick . Projectile Movement moves the bullet, so turning Actor Tick off still leaves that component's motion to manage separately.

Record whether it is in use

Add these variables from BP_PooledBullet 's "My Blueprint" and compile.

Variable nameTypeDefaultRole
bInUseBooleanfalsetrue while this bullet is in use
ReturnHandleTimer HandleUnsetThe information needed to stop the return timer

The b in bInUse is a prefix marking a true/false variable. Leave Private off so the manager can read it.

The point is separating being visible from being in use . Even when an effect temporarily hides a bullet, an in-use bullet is not handed to another shot.

Build the logic that rests a bullet

Build the return path for a finished bullet first. Create a RestInPool function from the "+" on BP_PooledBullet 's "Functions". No inputs or outputs are needed.

First place Clear and Invalidate Timer by Handle and connect a Get of ReturnHandle to Handle. That cancels the "return in N seconds" reservation.

Clearing ReturnHandle's timer at RestInPool's entry

Reusing with a timer still running can cut short a newly fired bullet because of the previous reservation. Passing through this node on the first call, when no timer exists yet, is fine.

Target here means "what to act on" and Self means "this Blueprint itself". Use Self when hiding the whole bullet and Projectile Movement when stopping motion.

Then wire white exec in this order. Drag Projectile Movement from Components into the graph to create a reference and connect it to each Target.

Zeroing Projectile Movement's velocity and deactivating it
  1. Stop Movement Immediately with Target Projectile Movement, zeroing velocity.
  2. Deactivate on the same Projectile Movement.
  3. Set Actor Hidden In Game with Target Self and New Hidden on, hiding the bullet.
  4. Set bInUse to false so it is usable as free again.
Continuing from stopping motion to hiding it and returning bInUse to false
BP_PooledBullet / RestInPool (no inputs or outputs)
  → 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 is also called right after a bullet is first created. It only stops display and motion, so calling it on an already resting bullet returns it to the same waiting state.

We start at NoCollision here. When adding collision for real attacks, also turn Set Actor Enable Collision off when resting. Hiding alone does not stop collision.

Sponsored

Wake a bullet and return it after 2 seconds

Now the reverse. Create a LaunchFromPool function on the same Blueprint with two inputs.

Input nameTypeMeaning
LaunchLocationVectorWhere to fire from
LaunchRotationRotatorWhich way to fire

Restore the position, then resume motion

First set Set bInUse to true, then place Set Actor Location And Rotation . Connect the LaunchLocation input to New Location and LaunchRotation to New Rotation. Target is Self, Sweep off, Teleport on.

Marking it in use and returning the bullet to the input position and orientation

That returns the bullet from where it finished flying to the new firing position. Without Sweep, it does not test collisions along the way.

Next place Set Updated Component with Target Projectile Movement and New Updated Component connected to the Sphere reference.

Re-specifying the root Sphere as Projectile Movement's moved component

Updated Component is what this movement component moves. We specify the root Sphere. In particular, when collision is added later and the bullet stops, that assignment can be cleared. Re-specifying it on reuse removes one cause of "the first shot flies but the second does not move".

Set the velocity again

Add Set Velocity from the Projectile Movement reference. Build the new velocity by multiplying Get Actor Forward Vector (Target Self) 's output by 1200 . Connect them as a Vector times Float multiplication.

Multiplying the forward direction by 1200 into Velocity and activating it

Forward Vector is a length-1 vector representing that Actor's forward. Multiplying the direction by 1200 gives 1200 cm per second. Since we rotated it to the firing direction just before, it flies that way every time.

Connect Set Updated Component's exec output to Set Velocity and then to Activate . Activate's Target is also Projectile Movement with Reset on. The order is: prepare the position, the moved component, and the velocity, then resume motion.

Show it and reserve the return

After Activate, connect Set Actor Hidden In Game (Self, New Hidden off). Then place Set Timer by Function Name and configure it as follows.

EntryValue
ObjectSelf
Function NameRestInPool
Time2.0
LoopingOff
Showing the bullet and storing the handle of the timer calling RestInPool after 2 seconds

Place a Set of the ReturnHandle you created and connect Return Value and the white exec wire.

A timer handle is the information for referring to a set timer later. When returning a bullet early, RestInPool uses that handle to cancel the reservation.

Function Name is a string, so type RestInPool exactly. If you rename the function, fix this field too. This example calls an argument-less function from the timer.

That is one LaunchFromPool function. Do not create separate functions per diagram; continue from the previous diagram's exec output.

BP_PooledBullet / LaunchFromPool (LaunchLocation, LaunchRotation)
  → Set bInUse (true)
  → Set Actor Location And Rotation (Self, the input position and rotation)
  → 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 s, no looping)
  → Set ReturnHandle (the timer's Return Value)

Leave Initial Life Span at 0. Life Span destroys an Actor when its time is up, which does not fit our goal of returning without destroying.

Prepare three and find a free one

Now edit the manager, BP_BulletPool . Create these variables in "My Blueprint" and compile.

Variable nameTypeDefault
PoolArray of BP_PooledBullet Object ReferencesEmpty
PoolSizeInteger3

A reference specifies an already created Actor later. Pool holds references to three bullets in order rather than copies of the bullets. Set the type to a BP_PooledBullet Object Reference and switch the container to Array on the right of the type field. Array basics are covered in the Array, Map, and Set article.

Create the stock on BeginPlay

Connect For Loop from Event BeginPlay in the Event Graph. First Index is 0 and Last Index is PoolSize - 1 . Here Loop Body runs three times, for 0, 1, and 2. Keep PoolSize at 1 or more.

BeginPlay spawning BP_PooledBullet PoolSize times

From Loop Body, call Spawn Actor from Class with Class BP_PooledBullet. Connect Get Actor Transform (Self) to Spawn Transform and choose Always Spawn, Ignore Collisions for Collision Handling Override.

Drag from Spawn's Return Value to call RestInPool , resting the created bullet. Then place an Add on the Pool array with the same Return Value as Item.

Resting the created bullet and adding its reference to the Pool array
BP_BulletPool / Event BeginPlay
  → For Loop (0 to PoolSize - 1)
      Loop Body → Spawn Actor from Class (BP_PooledBullet)
                 → RestInPool (Target = the created bullet)
                 → Add (Target Array = Pool, Item = the created bullet)

"Create, rest, register in the array" is one bullet. Repeating it in the BeginPlay loop gives you three in stock before firing starts.

Fire only one bullet that is not in use

Create a FireOne function on BP_BulletPool with no inputs or outputs. Connect a Get of Pool to For Each Loop 's Array and wire exec from the function entry.

Drag from Array Element to create Get bInUse and connect it to a Branch's Condition. Wire exec from Loop Body to the Branch.

Reading bInUse for each Pool element to find a bullet that is not in use

Leave the Branch's True side unconnected. In-use bullets do nothing and the loop moves to the next element. On False, call that Array Element's LaunchFromPool .

Connect Get Actor Location (Self) to that call's LaunchLocation and Get Actor Rotation (Self) to LaunchRotation. Self is the manager BP_BulletPool , so bullets fire from where the manager sits and faces.

Firing a free bullet from the manager's position and orientation, ending the function with a Return Node

Finally place a Return Node and connect LaunchFromPool's exec output to it. No output value is needed. It ends the function once one bullet was fired . Without it, you would fire every free bullet at once.

For Each Loop's Completed is the path when nothing was free after checking everything. Print Pool empty with Print String and end. Do not add another Spawn.

Printing Pool empty from Completed when no free bullet was found
BP_BulletPool / FireOne (no inputs or outputs)
  → For Each Loop (Pool)
      Loop Body → Branch (Array Element's bInUse)
        True  → do nothing, continue to the next element
        False → LaunchFromPool (Target = Array Element)
                LaunchLocation = the manager's position
                LaunchRotation = the manager's rotation
              → Return Node (end the function)
      Completed → Print String (Pool empty)

A fired bullet has bInUse set true at the start of LaunchFromPool. The next FireOne call skips it and looks for another free one. There is no need to remove it from the array when it returns. The design keeps all stock in the array and distinguishes free slots only by bInUse .

Fire repeatedly and confirm

Add an auto-fire timer. Return to BP_BulletPool's Event Graph and connect Set Timer by Function Name to the Completed of the For Loop that built the stock.

Starting a 0.5-second FireOne timer from the stock creation's Completed

Object is Self, Function Name is FireOne , Time is 0.5 , and Looping is on. Connecting to Completed rather than Loop Body starts the timer after all three exist. Since we test-fire throughout Play, this timer's Return Value need not be stored.

Compile and save both Blueprints, then confirm with Play in Editor (PIE).

  1. Place one BP_BulletPool in an open area of the level, about 100 cm above the floor with Scale (1, 1, 1) .
  2. Set the firing direction with Rotation's Yaw. Bullets travel along the manager's local X, so point the Transform's red X axis toward open space.
  3. Play and watch the spheres from the side, a little away. Press F8 to leave character control and survey with the editor camera if needed.
  4. Search for BP_PooledBullet in the World Outliner during PIE and confirm there are still three bullets. Do this check in editor Play.

It attempts a shot every 0.5 seconds and each bullet hides after 2 seconds. With all three in use, Pool empty prints and no shot fires that round. When one returns, the same bullet fires again.

Next, stop Play, open BP_BulletPool, change PoolSize's default to 6 , and compile. Try again and the stock has slack, making free bullets easier to find at the normal 0.5-second cadence. Compared with three, that shows the firing gaps caused by insufficient stock .

Change the orientation and replay and the spheres fly the new way, because the rested bullet's velocity is set again on firing.

SymptomWhere to check
Spheres are visible from the startWhether RestInPool is called with the created bullet as Target right after Spawn
All three fire at onceWhether FireOne has a Return Node right after LaunchFromPool
They fly once and never come backWhether the timer's Function Name matches RestInPool and Time is 2.0
Pool empty continues though they returnedWhether RestInPool sets bInUse back to false at the end
Spheres appear but do not moveWhether Sphere is the root. Check Set Updated Component's target, Velocity, and Activate's Target
The sphere count keeps growingWhether you added a Spawn to FireOne. Only BeginPlay creates stock

What we verify here is that the same Actors are being reused , not a speed improvement. Three spheres do not need to show a performance difference.

Sponsored

Deciding empty-pool behavior and pool size

Having everything in use with nothing free is called exhaustion . What happens then is decided by the game.

The options on exhaustion: skip firing, add one, or replace an old bullet
PolicyWhat happens on screenWhat to consider
Do not fire this timeOne bullet or effect is skippedSkipping decoration is fine, but whether to drop a player's attack is another matter
Create one moreIt fires as requestedSpawn cost at that instant, and growing stock and memory. Also set an upper limit
Return an old in-use oneThe old bullet vanishes and becomes the new oneIts in-flight attack and collision vanish too. Use it for things like damage numbers

Our experiment chose "do not fire this time" so the difference is visible. Whether to ship that as is depends on your weapon and effect design.

A first estimate for stock size is shots per second times seconds alive . Firing 10 per second with a 3-second life gives about 30. Add slack for overlapping bursts and timing. When several weapons share a pool, use the combined fire rate.

Recording the maximum concurrent usage and the number of skipped shots during play makes the needed stock easier to decide. Preparing a huge amount is not automatically safe; also watch the resting memory and the initial creation time.

Build it into a game and measure the effect

Return without destroying on impact too

Once test firing works, configure Sphere's collision following the Spawn Actor and Projectile Movement article. Replace where a hit bullet went to Destroy Actor with RestInPool .

But also fill in the rest and wake logic. RestInPool turns collision off, and LaunchFromPool turns it back on after position, velocity, and the return timer are prepared . Unless you change Sphere's NoCollision setting to an attack configuration, turning it on at the Actor level alone will not collide.

Check bInUse at the start of the Hit event and continue to damage and return only when true, so notifications on a not-in-use bullet do not process the same attack twice. Also confirm that after Projectile Movement stops on impact, LaunchFromPool's Updated Component and velocity get it moving again.

Compare against the Spawn version under the same conditions

When comparing performance, align shot count, bullet lifetime, meshes, and collision between the normal Spawn / Destroy version and the pooled version. If exhaustion reduces the shots fired, the loads are not comparable. Remove Print Strings during measurement.

Run the same scene with an execution method that reduces editor influence, such as Standalone Game . Watch Game time with stat unit and, if needed, watch its variation with stat unitgraph . Game is CPU-side time advancing the game's logic.

Comparing Game time between the Spawn/Destroy version and the pooled version with shot counts and collision aligned
What to look atWhat you want to confirm
Game timeWhether processing time dropped in the scene where shots concentrate
Unreal Insights captureWhich of creation, initialization, destruction, or GC took time in heavy stretches
Initialization time and memoryWhether the up-front stock is too large
Game behaviorWhether dropped shots or missed returns changed the comparison conditions

A smaller peak in the graph alone does not prove "GC stopped". Investigate the detailed cause with the captures described in the Unreal Insights article. Basic number reading is in the stat commands article.

If there is almost no difference, the original spawn and destroy may have been light enough already. Judge from your measurements whether adding pool management is worth it.

Bonus: Good to Know Up Front

Reuse does not run BeginPlay each time. Configuring HP, damage, who fired, and effect state only in BeginPlay leaves the previous shot's information behind. Values that change per use go into LaunchFromPool's inputs or into reset logic on return. For a damage-number pool, that means the previous number, position, and animation progress.

Actor pools are managed within that World. A World is the game world running at that moment. Storing references in a GameInstance or its Subsystem does not carry over Actors destroyed by a normal level change. Placing a dedicated Actor and building stock at level start, as we did, is enough to begin. The GameInstance article covers the difference between carried data and Actors.

Adjust when you create large stock. Creating everything on BeginPlay concentrates initialization cost there. For large counts, consider preparing during a loading screen or spreading it across frames.

What to stop on return differs per component. We deactivated Projectile Movement, but AI thinking, looping audio, effects, and other components' Ticks do not stop automatically. Before reusing a whole complex enemy, start with low-state things like bullets and damage numbers so missed resets are easier to track.

Summary

Pooling rests a finished Actor and wakes it for its next turn. Our example kept all stock in an array and picked free ones with bInUse. Returning stops the timer and movement, and firing sets position, orientation, and velocity again.

What to confirm first is that the same bullet is reused repeatedly and that an empty pool behaves as you decided. Then compare against the original Spawn version and measure whether it reduces creation and destruction cost.

If what bothers you is not spawning and destroying Actors but "meshes loaded before they are needed", go to soft references and async loading next.

Reference: Actor lifecycle, Projectile Movement Component, Using timers.

Unreal Engine Notes in this section98