- Home /
Problem with Object Pooling
My question got a little messy over time. For future reference (if anybody ever needs it) here is a short summary:
The symptom: The bullets get instantiated as they should when I start the scene. The bullets fly into the direction they are shot and after leaving the screen (and therefore also leaving a trigger) they get disabled and moved back to the object pool. If a bullet hits an enemy it also gets pooled right away. If that exact bullet is retrieved however it only flies for about half the screen before disappearing. They apparently get pooled for no good reason. I checked wether they collide with anything (they don't) or wether they are not actually pooled but destroyed for some reason (also a no). So they get pooled after a few seconds even though I didn't call the Pool() method. There is no timer involved or anything that would cause any delayed pooling. Anyways, these bullets that disappear get moved to the pool again. If they get retrieved a third time they disappear again and so on and so forth. After a few (seamingly random) times of disappearing the bullets behave normally again until they hit an enemy.
The problem: When a bullet hit an enemy the single bullet was (don't know why) added to the array of pooled objects multiple times. So the bullets that have already been shot were still inside the array (at least one entry) and have been reused even though they were already retrieved.
The solution: As @DerDude87 pointed out correctly, I should have checked wether the object has already been added to the array of pooled objects before adding it. Thanks to everyone, thanks DerDude87, for helping me find the solution. Turns out it was something as simple as this to fix my problem.
I think you are instantiating your objects in hierarchy and when you execute this condition if (objectPrefabs[i].name == obj.name) and never comes true because when we instantiate object in hierarchy then name changes, lets suppose we instantiate gameObject with the name Object and in hierarchy name will display Object(Clone). So if its your problem then rename gameObject where you instantiate.
if (objectPrefabs[i].name == obj.name)// never true
{
obj.transform.parent = containerObject.transform;
obj.SetActive(false);
pooledObjects[i].Add(obj);
return;
}
So after this your object destroy immediately and not added to list here
if ((i + 1) == objectPrefabs.Length)
{
Destroy(obj);
break;
}
I'm not seeing anything that handles when the bullets DO NOT hit an enemy, so those bullets will fly forever. The bullets that do hit the enemy seem to get disabled then added, but what is with the whole parent thing?
You are also using a jagged array, where a List might be more adaptable.
Regarding your Get method: might the pool be empty? In case it is, your get method does not seem to be returning anything.
@SohailBukhari thanks for your answer, I made sure that that wouldn't be happening by instantiating them beforehand and then setting their name to match the original prefab's name. I mean the script is working more than well as long as I do not hit any enemies.
@RobAnthem those bullets will eventually exit a box collider (OnTriggerExit2D()) that is a little bit bigger than the camera FOV. When that happens the bullets get pooled without any problems.
@DerDude87 the pool isn't empty as I instantiate enough instances of the bullets beforehand. but I was meaning to change the Get() $$anonymous$$ethod to return a new instance if the pool is empty. But first I got to sort out this other mess...
Thanks for your help guys, but it seems to be somethig else. The bullets disappear after a few seconds if they were pooled after hitting an enemy. This will happen a few times after they hit said enemy and at some point the bullets go back to behaving normally.
I agree with punisher, this system is a little odd. Why wouldn't you do something like this?
public class ObjectPool : $$anonymous$$onoBehaviour
{
public static ObjectPool instance;
public List<GameObject> pooledObjects;
public GameObject prefabObject;
void Awake()
{
instance = this;
pooledObjects = new List<GameObject>();
}
public void AddToPool(GameObject bullet)
{
pooledObjects.Add(bullet);
bullet.SetActive(false);
}
public GameObject GetObject()
{
GameObject go;
if (pooledObjects.Count > 0)
{
go = pooledObjects[pooledObjects.Count -1];
pooledObjects.Remove(go);
}
else
{
go = Instantiate(prefabObject);
}
return go;
}
}
then your trigger can do this.
void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("EnemyTag") || other.CompareTag("OtherEnemyTag"))
{
ObjectPool.instance.AddToPool(this.gameObject);
}
}
And your firing method
public void Fire()
{
GameObject bullet = ObjectPool.GetObject();
bullet.transform.position = transform.position;
bullet.forward = transform.forward;
}
@RobAnthem While this is great for pooling only one type of object, I want to be able to store different kinds of object through the same script.
Answer by NoBrainer-David · May 13, 2017 at 03:35 PM
One thing you could do is make sure each object can only be in the pool list once. Having one object referenced multiple times in the pool list could very well cause the behavior you are describing.
Maybe check in the inspector and see if the list grows larger as time goes by.
Then, to fix this issue, you could make sure to not add the object twice with:
if(!pooledObjects[i].Contains(obj))
pooledObjects[i].Add(obj);
This could happen when your projectile hits multiple colliders in the same frame. Setting the object inactive will only take effect on the next physics-frame. You cannot rely on OnTriggerEnter to be called only once.
Thanks ;)
Also thank you for pulling up my comment to be the answer!
Answer by Piyush_Pandey · May 12, 2017 at 11:43 AM
I dont know if you attached a 2d collider on the bullet or 3D but here are some info that might be useful:
I guess u already know that for any kind of OnTriggerEnter/OnCollisionEnter function {2D or 3D} u need colliders attached to both of them and rigidbody to any one of them.
3D collision:
1) Both should have 3D colliders. The one having the trigger/collision script should have isTrigger enabled.
2D collision:
1) Both should have 2D collider. The one having the trigger/collision script should have isTrigger enabled.
2) The one having rigidbody should have simulated as checked. I u dont want to use gravity, use BodyType as kinematic.
3D+2D collision
1) No inbuilt method in unity. Reason is that for 3D unity uses PhysX and for 2D it uses Box2D
2) Workaround: Attach an empty 3D box collider to the 2D object you want to detect.If its not possible to attach directly to the gameObject, attach it as a child.
@Piyush_Pandey Thankfs for your detailed answer. Sorry, I should have stated that I am working in 2D. I do have everything set up to work correctly. And it all worked beautifully until I used pooling ins$$anonymous$$d of Destroy()
an Instantiate()
.
EDIT: another thing that is worth noting (that I think I also stated above) is that this only happens if the Pool()
function is called from OnTriggerEnter2D()
on the bullet object but not if it is called by OnTriggerExit2D()
by the "invisible box" (2D box collider) around the screen....
Answer by ThePunisher · May 12, 2017 at 11:42 PM
There's two things that very confusing about how you are pooling objects. A pooling script should be pretty straight forward to be honest. The only things it has to do are: disable objects, store them, and retrieve next pooled object or create one if there's none available.
Confusing thing one: Your pool method is storing objects only if it finds their name to be equal to some other object's name in the objectPrefabs array/list. I'm not sure why you're having to do this since it shouldn't be necessary for object pooling. What happens if the name doesn't equal anything in the objectPrefabs? Are you potentially pooling other types of objects in combination with bullets? You're also storing the bullets under some container parent but never actually moving the object to the container. If you're trying to move the bullets to some location you could just set the position of the bullet to the position of the container (unless the container moves around).
Confusing thing two: When you are trying to pull objects from the object pool you again compare names. This is even more confusing as it looks like a different container than the one you used when pooling the object. Also, not all code paths return an object, which would obviously cause strange behavior. You need to make sure your object pool can always return an object, whether it is an already pooled object or one it has to instantiate.
I bet the problem is related to a mismatch/discrepancy between the name comparison of when you pool the object vs when you retrieve the pooled object. To help you debug that I would add some logging to all possible paths of both for loops.
Edit: Apparently you posted something while I was writing this up. With the behavior you described it sounds like the bullets are not removed from the pool container, and are therefore reused prematurely when you try and fire a new bullet. This is exactly what happens in my example if you comment out the line that removes the object from the list when it is retrieved.
Here's an example of a simple object pooling script for reference. The other two scripts are just to illustrate the point.
To setup this example follow these steps:
Import all 3 scripts (ObjectPoolingScript, Gun, and Bullet)
Create new scene.
Add ObjectPoolingScript and Gun to main camera.
Create a sphere game object and attach Bullet script to it.
Turn sphere with script into prefab by dragging it into your project view.
Set the ObjectPoolingScript's Prefab member in the inspector to reference the Bullet prefab by dragging the prefab onto it.
Run scene and click left mouse button to spawn pooled objects.
ObjectPoolingScript.cs:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class ObjectPoolingScript : $$anonymous$$onoBehaviour
{
public GameObject Prefab;
private List<GameObject> m_pooledObjects = new List<GameObject>();
public void PoolObject(GameObject obj)
{
obj.SetActiveRecursively(false);
obj.transform.position = transform.position;
m_pooledObjects.Add(obj);
}
public GameObject GetPooledObject()
{
if (m_pooledObjects.Count > 0)
{
GameObject result = m_pooledObjects[0];
result.SetActiveRecursively(true);
m_pooledObjects.Remove(result);
return result;
}
else
{
return (GameObject)GameObject.Instantiate(Prefab);
}
}
}
Gun.cs:
using UnityEngine;
using System.Collections;
public class Gun : $$anonymous$$onoBehaviour
{
public float BulletsPerSecond = 2.0f;
private float m_elapsedTime = 0.0f;
private ObjectPoolingScript m_bulletPool = null;
private void Awake()
{
m_bulletPool = GetComponent<ObjectPoolingScript>();
}
private void FireBullet(Vector3 position, Vector3 direction, Quaternion facing)
{
GameObject obj = m_bulletPool.GetPooledObject();
obj.transform.rotation = facing;
Bullet bullet = obj.GetComponent<Bullet>();
bullet.Project(position, direction, m_bulletPool);
}
private void Update()
{
m_elapsedTime += BulletsPerSecond * Time.deltaTime;
if (Input.Get$$anonymous$$ouseButton(0) && (m_elapsedTime >= 1.0f))
{
m_elapsedTime = 0.0f;
Vector3 direction = Vector3.forward + (Vector3.left * Random.RandomRange(-0.2f, 0.2f)) + (Vector3.up * Random.RandomRange(-0.2f, 0.2f));
FireBullet(transform.position, direction, Quaternion.identity);
}
}
}
Bullet.cs:
using UnityEngine;
using System.Collections;
public class Bullet : $$anonymous$$onoBehaviour
{
public float Speed = 1.0f;
public float LifeSpan = 5.0f;
private Vector3 m_direction;
private float m_elapsedTime = 0.0f;
private ObjectPoolingScript m_pool = null;
public void Project(Vector3 startPosition, Vector3 direction, ObjectPoolingScript pool)
{
m_elapsedTime = 0.0f;
m_direction = direction;
m_pool = pool;
transform.position = startPosition;
}
private void Update()
{
transform.position += m_direction * (Speed * Time.deltaTime);
m_elapsedTime += Time.deltaTime;
if (m_elapsedTime >= LifeSpan)
{
//recycle.
m_pool.PoolObject(this.gameObject);
}
}
}
Thanks for your very detailed answer, @ThePunisher . While I used to do what you did, I actually tried to pool all objects that I reuse with my one script. That is why I first create an array of objects that I want to store (which I can then specify in the editor) and a List of arrays to store the pooled objects (each array contains one type of object, the script can match them by getting the index from the first array):
public GameObject[] poolPrefabs;
public List<GameObject>[] pooledObjects;
I then fill the pool (for testing, later I will do it dynamically) with a sufficient number of bullets (50% or more than I need).
To match the objects to the right array in the list, I need to check the names and compare them, as I do not know of a more simple method of doing this.
EDIT: Also, I did add debugging to nearly all lines the last few days and the strange thing was that I couldn't make out anything strange except that the bullets get pooled even though they do not collide with anything.
Gotcha, thanks for clarifying that. Did you ever figure out what was causing the bullets to pool earlier than they were supposed to?
Edit: Nvm, I see the answer now. I changed his comment to an answer. if you get a chance just mark @DerDude87 as the answer, please.
Your answer
Follow this Question
Related Questions
Multiple Cars not working 1 Answer
Distribute terrain in zones 3 Answers
Is there any way to run a class script from visual studio? 0 Answers
Object Pooling 1 Answer