Search Unity

Object Pooling w/ gameObject.setActive() vs. Instantiate()

Discussion in 'Scripting' started by Yukichu, Jul 18, 2013.

  1. Yukichu

    Yukichu

    Joined:
    Apr 2, 2013
    Posts:
    420
    Creating a bullet hell shooter. Projectiles are various prefabs. Decided I should pool the projectile objects.

    I created a C# stack full of projectiles (50 to start) and when the collider hits something, I disable the projectile gameobject via gameObject.setActive(false)

    When I pull the projectile off the stack, I set it active again: gameObject.setActive(true)

    I immediately started running into performance issues running this in the gameview of Unity Editor on a laptop with HD3000 graphics (a beast, I know.) I had no real issues before. I thought it was changing the image / collider size of the projectile when I pulled it off the stack, but it actually appears to be the gameObject.setActive() that is slowing things down.

    I changed it to disable the collider and move the object to some point in space you can't see, and viola.

    So, anyone care to explain why having an object pool with setActive() actually being slower than Instantiate? It makes me wonder, should you, for anything you need to deactivate though still use (object pooling, hidden GUI objects, hidden anything for that matter) should I not be using setActive? Is it that much of a hog to deactivate a component?

    Thoughts or insight would be appreciated.
     
  2. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    Im not sure why you are comparing SetActive to Instantiate.

    They are not related in any way... you instantiate something, then you set it to active/inactive.

    As for your problem, I suspect its more about how your pooling works. Might need to show us your code to see if thats the problem, however...

    1. What collection type are you using for your pooling? Hashtable, Dictionary, Array, List?

    2. How are you actually doing the pooling? Constantly maintaining a full list of pooled objects, iterating through each looking for inactive, or adding/removing as they are used?
     
  3. jc_lvngstn

    jc_lvngstn

    Joined:
    Jul 19, 2006
    Posts:
    1,508
    Sounds like he using a dedicated stack for each type of gameobject, with the one he is talking about being for projectiles.

    Preloads it with 50 instantiated but inactive gameobjects, and when you need one you pull it off the stack and set it to active. Then, when it collides with something, you disable it and put it back on the stack to be used later. Is that correct Yukichu? Honestly, this sounds like a nice way to handle pooling, if you don't mind a dedicated stack for each unique type of gameobject. I kinda like it.

    Can you take some measurements, to see how long it takes for you to enable and disable the gameobjects?

    JamesLeeNZ...I think he is comparing them because if enable/disabling them is slower then just instantiating them...it would be odd.
     
  4. Yukichu

    Yukichu

    Joined:
    Apr 2, 2013
    Posts:
    420
    I was comparing them for how 'expensive' they were, as when I was just instantiating clones and destroying them as needed, I never had a hiccup in the Unity editor. I wanted to pool things as a 'build it right' sort of thing, as I knew there would be more and more projectiles eventually.

    So, I'm using C# Stack Collection with 2DToolKit and was convinced it was me changing the sprite and/or collider size, but as soon as I changed it to disable the collider and move the object to Vector3.zero instead of disabling the whole object, everything was fine.

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5. using System;
    6.  
    7. public class ProjectilePool : MonoBehaviour {
    8.  
    9.     public static ProjectilePool Instance { get; private set; }
    10.     public bool m_bDebugMessages = false;
    11.     public int m_iProjectilePoolSize = 50;
    12.     public int m_iProjectileGrowthSize = 20;
    13.     public Projectile m_scrProjectileTemplate;
    14.     public int m_iProjectileGrowthRate { get; private set; }
    15.     private Stack<Projectile> m_stackProjectiles;
    16.     private Transform m_transform;
    17.    
    18.     void Awake() { doAwake(); }
    19.     private void doAwake()
    20.     {
    21.         Instance = this;
    22.         m_transform = transform;
    23.         initProjectilePool(m_iProjectilePoolSize, m_iProjectileGrowthSize);
    24.     }  
    25.    
    26.     private void initProjectilePool(int _initSize, int _iGrowthRate)
    27.     {
    28.         m_iProjectileGrowthRate = _iGrowthRate;
    29.         if (m_iProjectileGrowthRate <= 0)
    30.         {
    31.             m_iProjectileGrowthRate = 10;
    32.         }
    33.  
    34.         m_stackProjectiles = new Stack<Projectile>();
    35.  
    36.         for (int i = 0; i < _initSize; i++)
    37.         {
    38.             Projectile _projTemp = Instantiate(m_scrProjectileTemplate, m_transform.position, m_transform.rotation) as Projectile;
    39.             // Set any values.
    40.             _projTemp.transform.parent = m_transform;
    41.             m_stackProjectiles.Push(_projTemp);
    42.         }
    43.        
    44.         if (m_bDebugMessages)
    45.             Debug.Log("Initialized Projectile Pool.  Pool Count: " + m_stackProjectiles.Count);    
    46.     }
    47.  
    48.     public Projectile GetFreeProjectile(Projectile _template)
    49.     {
    50.         try
    51.         {
    52.             m_stackProjectiles.Peek();
    53.         }
    54.         catch (InvalidOperationException ex)
    55.         {
    56.             // Stack is empty. Log that we're growing
    57.             if (m_bDebugMessages)
    58.                 Debug.Log("Projectile pool growing in size: " + ex.Message);
    59.  
    60.             for (int i = 0; i < m_iProjectileGrowthRate; i++)
    61.             {
    62.                 Projectile _projTemp = Instantiate(m_scrProjectileTemplate, m_transform.position, m_transform.rotation) as Projectile;
    63.                 // Set any values.
    64.                 _projTemp.transform.parent = m_transform;
    65.                 _projTemp.gameObject.SetActive(true);
    66.                 m_stackProjectiles.Push(_projTemp);
    67.             }
    68.         }
    69.        
    70.         if (m_bDebugMessages)
    71.             Debug.Log("Getting Projectile from pool, index: " + (m_stackProjectiles.Count));
    72.        
    73.         Projectile _projClone = m_stackProjectiles.Pop();
    74.         _projClone.collider.enabled = true;
    75.         //_projClone.gameObject.SetActive(true);
    76.        
    77.         if (_template != null)
    78.         {
    79.             switch (_template.m_iColliderType) {
    80.             default:
    81.             case Constants.ColliderType.BoxCol:
    82.             {
    83.                 BoxCollider box1 = _projClone.collider as BoxCollider;
    84.                 BoxCollider box2 = _template.collider as BoxCollider;;
    85.                 box1.center = box2.center;
    86.                 box1.extents = box2.extents;
    87.             }
    88.             break;
    89.             }
    90.             tk2dSprite _tempSprite = _template.GetComponent<tk2dSprite>();
    91.             _projClone.m_sprite.SetSprite(_tempSprite.spriteId);
    92.             _projClone.m_sprite.color = _tempSprite.color;
    93.             _projClone.m_sprite.scale = _tempSprite.scale;
    94.             _projClone.m_ID = _template.m_ID;
    95.             _projClone.m_fIndependentRotation = 0;
    96.             _projClone.m_bIsMobileProjectile = false;
    97.             _projClone.m_scrMobileOrigin = null;
    98.             _projClone.m_scrPlayerOrigin = null;
    99.         }
    100.        
    101.         return _projClone;
    102.     }
    103.    
    104.     // something to note...  activating and deactivating an object is almost as 'expensive'
    105.     // as instantiating the thing, resulting in a not-so-helpful object pool
    106.     public void PutFreeProjectile(Projectile _projClone)
    107.     {
    108.         // if there are any variables you want to clean up before
    109.         // pushing the bubble back on the stack, do it here.
    110.         // That way the Get always gets a clean copy.
    111.         _projClone.collider.enabled = false;
    112.         _projClone.rigidbody.velocity = Vector3.zero;
    113.         _projClone.transform.position = Vector3.zero;
    114.         //_projClone.gameObject.SetActive(false);
    115.         _projClone.m_fIndependentRotation = 0;
    116.         _projClone.m_fKnockBack = 0;
    117.         _projClone.m_bIsMobileProjectile = false;
    118.         _projClone.m_scrMobileOrigin = null;
    119.         _projClone.m_scrPlayerOrigin = null;
    120.         m_stackProjectiles.Push(_projClone);
    121.         if (m_bDebugMessages)
    122.             Debug.Log("Put Projectile back into pool, index: " + m_stackProjectiles.Count);
    123.     }
    124. }
    125.  
     
  5. Yukichu

    Yukichu

    Joined:
    Apr 2, 2013
    Posts:
    420
    You got it right jc_lvngstn.

    I was trying to measure it and couldn't figure out how you actually measure it. I was trying to create a bunch via a loop in an coroutine, but would all run at once and the start/end time was always the same.
     
  6. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    Im not too familiar with stack performance... I assume its all good (trying to find comparison in google just returns stack overflow links comparing all the other collection types).

    The code looks good though. You can access the windows stop watch if you want to get a timed result.

    Ill have to check what I was doing for my pooling (ie setactive or disabling).
     
  7. Yukichu

    Yukichu

    Joined:
    Apr 2, 2013
    Posts:
    420
  8. jc_lvngstn

    jc_lvngstn

    Joined:
    Jul 19, 2006
    Posts:
    1,508
    Yeah, stack collection performance...I would think this would be very fast, the least of your concerns. Enabling/disabling gameobjects to me would be the biggest culprit. I was also thinking about how expensive colliders can be, but you are using box colliders and that should be gravy.

    Interested to see your results, though.
     
  9. Yukichu

    Yukichu

    Joined:
    Apr 2, 2013
    Posts:
    420
    I feel like a mad scientist now. Here are my results:

    Setup is a laptop with:
    HD3000 graphics / Core i5-2520M 2.50 GHz / 4 GB RAM / Windows 7

    Prior to running the game, CPU is about 2-8% (Chrome, Unity, MonoDevelop, etc.) and 1 GB RAM free which means, I should not be constrained on resources while running the test

    Test is:
    Create 1000 projectiles, all visible on screen with various velocity/position. Game is using isometric camera, no velocity on Y axis. Projectiles exist for 1 second then disappear.

    Scenario 1: Use object pool stack as shown earlier and disable collider, enable isKinimatic, and move to neutral location on screen when projectile is done being used.

    Initial Projectile Pool Size is 120 objects, growth is 20 objects. 1000 projectiles means pool must grow 44 times to accomodate size.
    Test 1: 0.369885 seconds
    Test 2: 0.634261 seconds
    Test 3: 0.65 seconds (I stopped use the calculator, too many numbers to type)
    Test 4+: 0.4 to 0.7 seconds

    After Pool has grown to 1000 objects, so all objects are already instantiated
    Test 1: 0.03477 seconds
    Test 2+: Multiple tests with pool already grown shows similar results of .033 to .040 seconds

    Scenario 2: Use object pool stack as shown earlier and disable/enable complete object via gameObject.setActive()
    Note that the enable/disable on collider/isKinimatic/velocity/position are no longer done. this relies completely on setActive() to enable/disable object.

    Initial Projectile Pool Size is 120 objects, growth is 20 objects. 1000 projectiles means pool must grow 44 times to accomodate size.
    Test 1: 0.7 seconds
    Test 2: 0.71 seconds
    Test 3+: around 0.7 seconds consistently

    After Pool has grown to 1000 objects, so all objects are already instantiated
    Test 1+: 0.065 to 0.08 seconds, rather consistently

    Scenario 3: Instantiate 1000 objects from prefab, and use Destroy(gameObject, 1) to destroy them 1 second later
    Test 1+: 0.21 seconds, very consistently
    Note: Unknown effect on garbage collector / game performance when GC invokes

    Conclusion
    1. Object Pooling has significant advantages when the pool has already been created. Everyone knows this already.
    2. setActive() is actually slower than disabling the collider, set isKinimatic, set velocity, set position to neutral (non-rendered) location. It seems to take about double the time for 1000 already instantiated objects. I think this is pretty sad actually and baffles me, as I would think it would be the opposite. All I can guess at this point is that setActive() is just disabling so many other things (renderer, checking for children, removing from scene and whatever else, who knows) that it's just costing more performance. Honestly, it just seems weird.

    Anyhow, this was my fun test. Do with that data as you will.

    Edit: I added isKinimatic to the enable/disable of the pooled objects, since I had read that the physics engine would still be checking them even if off-screen. It was added into my tests above, but isn't in the aforementioned code.
     
    Last edited: Jul 19, 2013
    giggioz and MikeITS like this.
  10. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    Oddly enough - I thought the point of deprecating SetActiveRecursively in favor of SetActive is that it didn't run through child objects and simply deactivated the caller.

    I wonder if the effect is the same if you do it on objects without physics.
     
  11. hippocoder

    hippocoder

    Digital Ape

    Joined:
    Apr 11, 2010
    Posts:
    29,723
    I think it might be rebuilding/refreshing the collider each time you SetActive. You'll have to check the profiler though. Generally this is ok with pooling because you're not actually spawning many things each frame, but if you're in a situation where there's 100+ things having to be set active in one frame, then that's going to be a problem, and it's probably better to use code to collide and avoid the physics engine.
     
  12. jc_lvngstn

    jc_lvngstn

    Joined:
    Jul 19, 2006
    Posts:
    1,508
    Very interesting...I'd curious how pooling assets address this, if they do.
     
  13. Fattie

    Fattie

    Joined:
    Jul 5, 2012
    Posts:
    476
    "I feel like a mad scientist now. Here are my results:"

    spectacular analysis, Yukichu - thanks for this after all these years!
     
  14. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    I'd be cautious about using these results. Much of the engine, including the physics engine, has been rewritten. The analysis might still be valid, but I would rerun all the benchmarks to be sure.
     
  15. HonoraryBob

    HonoraryBob

    Joined:
    May 26, 2011
    Posts:
    1,214
    Anyone know what the current "best practice" would be for pooling with Unity 5? I've found that SetActive is still abysmally, shockingly slow for some reason, to the point that it isn't a feasible option. So what's the best option? .
     
  16. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    Its probably still better than using destroy
     
  17. HonoraryBob

    HonoraryBob

    Joined:
    May 26, 2011
    Posts:
    1,214
    Sure, but is it best to keep the pool of objects out of view and then move them into their in-game positions when needed, or enable /disable their mesh renderers, or some other method? And what about enabling things like building interiors when the player moves into a building? This is where SetActive failed the test for me, because it takes up to several seconds just to activate a container GO containing the objects inside a very small building (about 10 meters in length with only a sparse set of interior objects).
     
  18. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    Moving in/out of a building is not what pooling is useful for.

    Pooling is often used for small/simple objects that are re-used often. Like projectiles. In inferno I dont bother trying to pool vehicles as the rate of create/destroy is relatively low, as opposed to projectiles/effects which are constantly being used.

    It comes down to figuring out the best performance. If SetActive is too expensive, I suspect you're using pooling wrong.
     
    Kiwasi likes this.
  19. HonoraryBob

    HonoraryBob

    Joined:
    May 26, 2011
    Posts:
    1,214

    If the buildings have mostly the same type of objects in each one (same types of interior walls, furniture, etc), then a pooling system might be useful. But I was using the building interior case mainly as an example of a case in which SetActive didn't work very well for me, and also an example of a similar problem (how to bring objects into a scene dynamically only when needed, which is what pooling also does).
     
  20. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    I doubt I would use pooling for that scenario, but then I havnt attempted to do that.

    Dynamic loading of scene stuff is generally not the best from what I have found. I would be more likely to do large mesh combines and work that way.
     
  21. SteveJ

    SteveJ

    Joined:
    Mar 26, 2010
    Posts:
    3,085
    Meanwhile, fairly nice piece of code from @Yukichu ... #StealingThat.
     
  22. HonoraryBob

    HonoraryBob

    Joined:
    May 26, 2011
    Posts:
    1,214

    My building interior takes several seconds to activate the first time the player moves into the building, but then about one second for subsequent times; and very little time to deactivate when the player moves out of the building. One possibility is that the large GO hierarchy for the interior might be causing a delay? Each object typically has several GOs apiece, creating a large structure with many layers.
     
  23. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    Couldnt give you a definite answer.

    If SetActive doesnt work for you, dont use it, use enabled or whatever is best. There's no hard and fast rules here about how you should handle activation of [whatever]. I tend to use pooling for things that have a short life. An effect might last 5 seconds, a projectile 10 seconds, and ill reuse them hundreds of times in a short amount of time.

    A building interior doesnt really fit that description for me. I might spend a minute or so in one room. If I was flying through them, I would change my solution to whatever worked the best. Some kind of Occlusion Culling? move the room somewhere else/ disable meshes/whatever is fastest.

    Of course the downside is this requires time starting at a profiler then experimenting with different solutions.
     
  24. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,616
    The primary goal is to avoid Destroy unless absolutely required. You said yourself creation can take a few seconds. If you can re-use rather than destroy and re-create do it. Is that pooling... by some definitions I guess so.
     
  25. HonoraryBob

    HonoraryBob

    Joined:
    May 26, 2011
    Posts:
    1,214
    It's SetActive() which can take several seconds. I would imagine instantiating a new object would take longer.