#Is pooling still a thing in Unity ECS (Entities 1.0+) in 2025? Especially for VFX and hybrid entitie

1 messages · Page 1 of 1 (latest)

rocky flame
#

Hey folks, quick question about ECS (Entities 1.0+):

I've read that pooling isn't really needed anymore in ECS, since Instantiate / DestroyEntity is super fast (no Garbage Collector).
But I'm wondering:

**1. ** For more complex entities (with Bones, LinkedEntityGroup, ComponentObject like ParticleSystem, AudioSource, etc.),
does pooling still make sense in 2025?

And if yes...

2. How do you properly “disable” pooled entities now?

The old Disabled tag doesn’t work anymore.

I can exclude them from my systems, but they still render (and maybe simulate?)

Should I manually remove components like MaterialMeshInfo, physics, etc.? Or move them offscreen?

Would love to hear what people are doing in practice 🙏

bronze trench
#

Entity, along with struct-components, are just pieces of data within some preallocated fixed-size native arrays. To speak it simply, instantiating an entity is just like putting a mark on an array index, and removing that mark to destroy it.

#

Pooling should be used for objects. Its actual name is "object pooling", never "data pooling".

#

So if you depends on the class-components, you're working with hybrid ECS approach, where a part of your system is still represented by objects, then you should use pooling for those objects to reduce the amount of GC runs.

#

does pooling still make sense in 2025
To sum it up, pooling never makes sense for pure data. The technique only makes sense for things managed by the GC.

#

The real problem are on the "companion components" (ParticleSystem, AudioSource, etc). They're governed by Entities Hybrid's internal working. However it doesn't take into account object pooling. So whenever you instantiate or destroy an entity, those companion components will trigger the GC.

#

As a fact, those companion components are attached to some companion GameObjects on a hidden scene. Unfortunately, we don't have control over them.

#

So, my approach is managing the GameObject part on my own instead of using companion components.

#

Then the entity creation will be splitted into 2 parts:

  1. I create the entities. I remember to mark them as "NOT ready for simulation".
  2. I instantiate corresponding GameObjects via my object pooling manager, then assign each GO to its corresponding entity. Finally, I mark the entities as "READY for simulation".

2a. To help accessing the GO's components in a simpler way, I use UnityOnjectRef<T>.

public struct SpriteRendererRef : IComponentData
{
    public UnityObjectRef<SpriteRenderer> reference;
}

2b. At step 2, I do a go.GetComponent<SpriteRenderer>() then assign it to the SpriteRendererRef component on the entity.

#

Because the GameObjects are now managed by my object pooling manager, I can pool them whenever some entities are destroyed.

rocky flame
#

Thank you for all your answers ! 🙂 I'll check that 🙂

jagged cargo
#

For me, I’m actually more concerned about Particle Systems that get baked into entities.
But in practice, when running on a mobile device, I don’t really notice any performance impact from repeatedly instantiating and destroying them.

Instead, what I do notice is a stutter the first time an entity is spawned.

bronze trench
jagged cargo
#

This is our video:
https://www.youtube.com/watch?v=aE_n3TdzirM&t=176s

It features an estimated few hundred small effects triggered when monsters die, along with 3 to 5 more complex effects.

The video was recorded on an iPhone 12.
With post-processing disabled, the game still runs smoothly on the realme X50 Pro 5G — the lowest-end device we've tested so far.

jagged cargo
thick juniper
granite hazel
thick juniper
granite hazel
thick juniper
granite hazel
rain pike
#

Issue with enablable is more you'd have to add it to every job in your project, but then your entity would still exist in 3rd party things like physics etc.
If you're pooling you usually put on a Disabled component when it's in the pool to stop the entity being used, but this is just a structural change anyway!

thick juniper
#

i will just run tests tomorrow

thick juniper
#

this is what i got https://gist.github.com/hastgt/bd53929e3abffbbc96c439059a6bbe70

results:

Test Run  
ID: 2  
Tests run: 20/20 passed  
Start: 2025-07-13 15:08:12Z  
End:   2025-07-13 15:08:16Z  
Duration: 4.907s  

Test Suite: DOTS  
Total defined: 9 439  
Executed here: 20/20 passed  
Duration: 4.907s  

Assembly: DOTSAutoFarmer.Tests.dll  
Duration: 4.887s  

Fixture: EntitiesSpawnDestroyPerformanceTests  
Total cases: 20  
Duration: 4.886s  

1. EntityManager_SpawnDestroy_System (Performance) – 5 cases, 1.337s  
   • (10)      0.388s  
   • (100)     0.229s  
   • (1 000)   0.228s  
   • (10 000)  0.228s  
   • (100 000) 0.246s  

2. JobParallelForChunkIndexInQuery_SpawnDestroy_System (Performance) – 5 cases, 1.158s  
   • (10)      0.215s  
   • (100)     0.239s  
   • (1 000)   0.238s  
   • (10 000)  0.229s  
   • (100 000) 0.222s  

3. JobParallelForEntityIndexInChunk_SpawnDestroy_System (Performance) – 5 cases, 1.157s  
   • (10)      0.210s  
   • (100)     0.229s  
   • (1 000)   0.234s  
   • (10 000)  0.233s  
   • (100 000) 0.235s  

4. PrePool_ToggleEnableable_System (Performance) – 5 cases, 1.229s  
   • (10)      0.223s  
   • (100)     0.235s  
   • (1 000)   0.242s  
   • (10 000)  0.251s  
   • (100 000) 0.262s  
bronze trench
thick juniper
#

i run the same tests, but in play mode for a more accurate real-world test
this is for 100k entities:

ToggleEnableSystem  
Calls: 1   GC Alloc: 70.1 KB   Time: 1.01 ms   Self: 0.05 ms  

SpawnDestroyEntityManagerSystem  
Calls: 1   GC Alloc: 0 B      Time: 1.26 ms   Self: 1.24 ms  

SpawnDestroyEcbParallelChunkIndexInQuerySystem  
Calls: 1   GC Alloc: 336.6 KB Time: 73.12 ms  Self: 0.08 ms  

SpawnDestroyEcbParallelEntityIndexInChunkSystem  
Calls: 1   GC Alloc: 336.6 KB Time: 75.00 ms  Self: 0.10 ms 

the difference between ecb in tests and real world is crazy

bronze trench
#

That difference was caused by the fact that ECB don't know how to batch most (or every) commands. In general it executes commands one by one.

bronze trench
#

I suggest reducing the entity count, but adding more components and a couple of SetComponent calls to the test.

granite hazel
thick juniper
granite hazel
#

actually, looking at your systems - they do it already

#

Do I understand correctly that spawning and destroying 10 entities took 0.388s and same for 100000 took 0.246s?

thick juniper
#

yes

#

i tested THE SAME EXACT code in play mode and have COMPLETLY different results (you can look above)

granite hazel
#

Something is wrong with this testing then. Maybe lacks some warm up on world or smth.
Kinda makes no sense spawning less takes more

thick juniper
rain pike
#

i'm not actually that surprised about the EM test results, what i find weird is the ECB test

#

i don't think batch creating on EM is a fair comparison though, it's rarely possible to just create that many entities of same type at runtime

#

you should probably do a for loop creating entities independently to compare against ecb

rain pike
#

@thick juniper
state.RequireForUpdate<BeginSimulationEntityCommandBufferSystem.Singleton>();
is reason why your editor tests are screwed

#

but you never added BeginSimulationEntityCommandBufferSystem to your world

#

so the system just didn't run

#

(also you don't actually use this)

#

results then match what i'd expect

thick juniper
thick juniper
rain pike
#

what looks weird about it?

#

it's exactly what i'd expect

thick juniper
#

u have two ecbs
one is taking 40ms
second 3ms

rain pike
#

10x the entities

thick juniper
#

and toggle is 0.02???

rain pike
#

yes?

#

btw you also made a mistake and ran all your tests twice

thick juniper
#

i mean if u put these in play mode, you’re gonna have different numbers

rain pike
#

by using World.Update()

rain pike
thick juniper
rain pike
#

oh wait yeah you never added it to a group

thick juniper
#

difference is big

#

i dont believe it takes 0.02 ms for 100k entities to enable/disable component

rain pike
#

oh your ToggleEnableSystem is wrong

#

you use WithAll

#

not WithPresent

#

so it's only running once

thick juniper
#

yes, i fixed it but still the same result

#

didnt update the gist tho

rain pike
#

well yeah double 0.02 is still very fast ^^

thick juniper
#

what did u get in play mode?
are you saying it’s the same 0.02???

#

can this be because of ur 9950x3d

rain pike
#

your playmode is like 1ms?

#

that seems fine

#

you've run the same editor test over and over, the cpu is predicting very well by the end

thick juniper
rain pike
#

as enzi found out, editor performance tests get a magnitude faster over time

#

due to having all the data in cache and perfect prediction

thick juniper
#

but still 0.02????????

rain pike
#

runtime

#

though this one looks a bit screwey let me check

#

wait toggle systme has no entities created

#

ah because you do it in the test

thick juniper
#

yes, i pre pool them

#

just drop this:

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Jobs;
using Unity.Jobs.LowLevel.Unsafe;


public struct SpawnDestroyComponent : IComponentData {}
public struct ToggleTag : IComponentData, IEnableableComponent {}
public struct JustTag : IComponentData, IEnableableComponent {}
public partial struct PrePoolEntities : ISystem
{
    public void OnCreate(ref SystemState state)
    {
        
    }
    
    public void OnUpdate(ref SystemState state)
    {
        state.Enabled = false;
        
        EntityArchetype archetypeSpawnDestroy = state.EntityManager.CreateArchetype(ComponentType.ReadWrite<SpawnDestroyComponent>());
        
        EntityArchetype archetypeToggle = state.EntityManager.CreateArchetype(ComponentType.ReadWrite<ToggleTag>(),
            ComponentType.ReadWrite<JustTag>());
        state.EntityManager.CreateEntity(archetypeToggle, 100_000);
    }
}

[BurstCompile]
[UpdateInGroup(typeof(SimulationSystemGroup))]
public partial struct ToggleEnableSystem : ISystem
{
    private EntityQuery _toggleQuery;

    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        _toggleQuery = SystemAPI.QueryBuilder()
            .WithPresentRW<ToggleTag>()
            .Build();
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        state.Enabled = false;

        JobHandle disableJob = new ToggleEnableJob
            {
                Enable = false
            }
            .ScheduleParallel(_toggleQuery, state.Dependency);

        new ToggleEnableJob
        {
            Enable = true
        }.ScheduleParallel(_toggleQuery, disableJob);

        disableJob.Complete();
    }

    [BurstCompile]
    private partial struct ToggleEnableJob : IJobEntity
    {
        public bool Enable;
        private void Execute(EnabledRefRW<ToggleTag> tag)
        {
            tag.ValueRW = Enable;
        }
    }
}
#

(prepool is not burst because i didnt care enough)

rain pike
#

there you go

#

my actual runtime results

#

wait

thick juniper
#

0.81 vs 0.02

rain pike
#

no i'm getting exception

#

that's why i'm making garbage

#

messed up job type

thick juniper
# rain pike

how can i make the same view?
i have to click on each test to see the results

rain pike
#

this is just profiler

thick juniper
#

oh

rain pike
#

oh wait srorry

#

you highlighted different message

#

just run all your tests in 1 go

#

and open performance window

#

ok actual runtime

thick juniper
#

now this is what i also get

thick juniper
rain pike
#

back to 0.19ms

rain pike
#

under general

#

next to your test runner

thick juniper
#

nice, ty

rain pike
#

anyway i think enable is an irrelevant test as it's not a solution

#

and i think you're missing the 2 tests that matter

#

creating entities 1 at a time with entitymanager
toggling with disable component

thick juniper
#

that's gonna be hella expensive

#

wasn't my main pont to pool entities with enable component

rain pike
#

its going to be as expensive as the ecb test

#

you can't pool entities with enable component

#

they still exist in everything

thick juniper
#

well that sucks, until unity does something with ienablecomponent, i guess

#

but you can still pool them with entities that don’t have any logic tied to 3rd parties, right

rain pike
#

you'd still have to add your enable component in every job

#

and now every job is slower

#

if you're pooling, just use disable component

#

that's the point of it

#

but yeah pooling makes no sense unless you have managed objects to reuse (meshes is actually a good example)

thick juniper
#

removing and adding component

rain pike
#

hence pooling makes no sense in pure entities, it's pretty much as fast to create/destroy than to pool

#

to quote joachim back in 2020

As a starting point in DOTS don’t do any pooling. Instantiation & destruction when done in batch is fast.

Adding / removing Disabled component is a reasonable option to hide a bunch of entities of you want to maintian their state. You want to use EntityQuery base add /remove component to do this efficiently. (In batch as opposed to one entity at a time which is multiple magnitudes slower)

thick juniper
#

would be nice for unity to implement something like, kinda annoying at this point