c# - Unity Object Pooling: IndexOutOfRangeException when releasing multiple objects simultaneously - Stack Overflow

I'm encountering an IndexOutOfRangeException in my Unity project when multiple objects are release

I'm encountering an IndexOutOfRangeException in my Unity project when multiple objects are released from the object pool at the same time.

For context, I'm working on a Unity project where tanks fire shells using an object pooling system to manage performance. When tanks are close together, they fire shells rapidly, causing objects in the pool to be activated and released in quick succession. This rapid interaction seems to trigger the exception.

Here's my object pooling implementation:

using System.Collections;
using System.Collections.Generic;
using TMPro;
using Unity.VisualScripting;
using UnityEditor.EditorTools;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    private int poolSize;
    //drag the object to pool in the game menu to this slot
    public GameObject ObjectToPool;
    public static GameManager manager;
    public GameObject[] pools;
    public short poolIndex;
    public GameObject activePlayer;
    public Camera activeCamera;
    void Awake()
    {
        //if the static instance of the class does not exist, create one
        if(manager==null){
            manager=this;
        }

        //tag is hardcoded but i will change that later
        poolSize=3*GetPooledObjectUserCount("Tank");
        poolIndex=0;
        InitializePool();
    }

    private int GetPooledObjectUserCount(string tag){
        return GameObject.FindGameObjectsWithTag(tag).Length;
    }
    
    private void InitializePool(){
        pools=new GameObject[poolSize];
        GameObject tmp;
        for(short i=0; i<poolSize; i++){
            tmp=Instantiate(ObjectToPool);
            tmp.SetActive(false);
            tmp.AddComponent<PooledObject>().PoolIndex = i;
            pools[i]=tmp;
        }
    }

    public GameObject GetObjectFromPool(){
        GameObject gameObject=pools[poolIndex];
        poolIndex++;
        return gameObject;
    }

    public void ReleaseObject(GameObject gameObject){
        gameObject.SetActive(false);
        GameObject latestActive=pools[poolIndex-1];

        //getting release and lastest active object index in pool by getting their component
        PooledObject releaseComponent=gameObject.GetComponent<PooledObject>();
        PooledObject activeComponent=latestActive.GetComponent<PooledObject>();
        int releaseIndex=releaseComponent.PoolIndex;
        int activeIndex=activeComponent.PoolIndex;

        //swap their index component value before swapping them in the array
        releaseComponent.PoolIndex=activeIndex;
        activeComponent.PoolIndex=releaseIndex;

        //switch their place in the pool
        pools[activeIndex]=gameObject;
        pools[releaseIndex]=latestActive;
        poolIndex--;
    }
}

And here is the implementation for the PooledObject class that is attached as a component to every pooled shell because I didn't want to declared a variable in shell.cs script to keep track of its pool index:


using UnityEngine;  

public class PooledObject : MonoBehaviour  
{  
    public int PoolIndex;  
}  

Lastly, the implementation for the shell.cs script that is attached to the shells:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Shell : MonoBehaviour
{
    public float speed=40;
    public Rigidbody rb;
    public GameObject explosion;
    public Transform shellTransform;

    // Start is called before the first frame update
    void Awake()
    {
        rb=gameObject.GetComponent<Rigidbody>();
        shellTransform=gameObject.GetComponent<Transform>();
    }

    // Update is called once per frame

    void OnCollisionEnter(Collision collision) 
    {
        GameManager.manager.ReleaseObject(gameObject);
        GameObject shockwave=Instantiate(explosion, shellTransform.position, shellTransform.rotation);
        Destroy(shockwave, 0.5f);
    }
    void Update()
    {
        shellTransform.forward=rb.velocity.normalized;
    }
}

The error message:

Exception caught: Index was outside the bounds of the array.
StackTrace:   at GameManager.ReleaseObject (UnityEngine.GameObject gameObject) [0x0000a] in /Users/tripham/Desktop/Unity/AI Projects/AI Tank prototype/Assets/Scripts/GameManager.cs:56 
UnityEngine.Debug:LogError (object)
GameManager:ReleaseObject (UnityEngine.GameObject) (at Assets/Scripts/GameManager.cs:73)
Shell:OnCollisionEnter (UnityEngine.Collision) (at Assets/Scripts/Shell.cs:23)
UnityEngine.Physics:OnSceneContact (UnityEngine.PhysicsScene,intptr,int) (at /Users/bokken/build/output/unity/unity/Modules/Physics/ScriptBindings/PhysicsContact.bindings.cs:49)

I have noticed that this pooling implementation have no problem even with a large number of caller(Tanks in the scene). The poolIndex when the error arises is always 0 also, so I have ruled out the possibility of poolIndex exceeding the poolSize. The error only arises when the tanks are grouped very close to each others, hence the shells are being set active and released very rapidly due to the shell hitting each other and the tanks in proximity.

I've considered the logic and I really can't find any edge cases. Which leads me to suspect it might be because of race-condition from threads that cause multiple callers of ReleaseObject() to access poolIndex at the same time.`

I'm encountering an IndexOutOfRangeException in my Unity project when multiple objects are released from the object pool at the same time.

For context, I'm working on a Unity project where tanks fire shells using an object pooling system to manage performance. When tanks are close together, they fire shells rapidly, causing objects in the pool to be activated and released in quick succession. This rapid interaction seems to trigger the exception.

Here's my object pooling implementation:

using System.Collections;
using System.Collections.Generic;
using TMPro;
using Unity.VisualScripting;
using UnityEditor.EditorTools;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    private int poolSize;
    //drag the object to pool in the game menu to this slot
    public GameObject ObjectToPool;
    public static GameManager manager;
    public GameObject[] pools;
    public short poolIndex;
    public GameObject activePlayer;
    public Camera activeCamera;
    void Awake()
    {
        //if the static instance of the class does not exist, create one
        if(manager==null){
            manager=this;
        }

        //tag is hardcoded but i will change that later
        poolSize=3*GetPooledObjectUserCount("Tank");
        poolIndex=0;
        InitializePool();
    }

    private int GetPooledObjectUserCount(string tag){
        return GameObject.FindGameObjectsWithTag(tag).Length;
    }
    
    private void InitializePool(){
        pools=new GameObject[poolSize];
        GameObject tmp;
        for(short i=0; i<poolSize; i++){
            tmp=Instantiate(ObjectToPool);
            tmp.SetActive(false);
            tmp.AddComponent<PooledObject>().PoolIndex = i;
            pools[i]=tmp;
        }
    }

    public GameObject GetObjectFromPool(){
        GameObject gameObject=pools[poolIndex];
        poolIndex++;
        return gameObject;
    }

    public void ReleaseObject(GameObject gameObject){
        gameObject.SetActive(false);
        GameObject latestActive=pools[poolIndex-1];

        //getting release and lastest active object index in pool by getting their component
        PooledObject releaseComponent=gameObject.GetComponent<PooledObject>();
        PooledObject activeComponent=latestActive.GetComponent<PooledObject>();
        int releaseIndex=releaseComponent.PoolIndex;
        int activeIndex=activeComponent.PoolIndex;

        //swap their index component value before swapping them in the array
        releaseComponent.PoolIndex=activeIndex;
        activeComponent.PoolIndex=releaseIndex;

        //switch their place in the pool
        pools[activeIndex]=gameObject;
        pools[releaseIndex]=latestActive;
        poolIndex--;
    }
}

And here is the implementation for the PooledObject class that is attached as a component to every pooled shell because I didn't want to declared a variable in shell.cs script to keep track of its pool index:


using UnityEngine;  

public class PooledObject : MonoBehaviour  
{  
    public int PoolIndex;  
}  

Lastly, the implementation for the shell.cs script that is attached to the shells:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Shell : MonoBehaviour
{
    public float speed=40;
    public Rigidbody rb;
    public GameObject explosion;
    public Transform shellTransform;

    // Start is called before the first frame update
    void Awake()
    {
        rb=gameObject.GetComponent<Rigidbody>();
        shellTransform=gameObject.GetComponent<Transform>();
    }

    // Update is called once per frame

    void OnCollisionEnter(Collision collision) 
    {
        GameManager.manager.ReleaseObject(gameObject);
        GameObject shockwave=Instantiate(explosion, shellTransform.position, shellTransform.rotation);
        Destroy(shockwave, 0.5f);
    }
    void Update()
    {
        shellTransform.forward=rb.velocity.normalized;
    }
}

The error message:

Exception caught: Index was outside the bounds of the array.
StackTrace:   at GameManager.ReleaseObject (UnityEngine.GameObject gameObject) [0x0000a] in /Users/tripham/Desktop/Unity/AI Projects/AI Tank prototype/Assets/Scripts/GameManager.cs:56 
UnityEngine.Debug:LogError (object)
GameManager:ReleaseObject (UnityEngine.GameObject) (at Assets/Scripts/GameManager.cs:73)
Shell:OnCollisionEnter (UnityEngine.Collision) (at Assets/Scripts/Shell.cs:23)
UnityEngine.Physics:OnSceneContact (UnityEngine.PhysicsScene,intptr,int) (at /Users/bokken/build/output/unity/unity/Modules/Physics/ScriptBindings/PhysicsContact.bindings.cs:49)

I have noticed that this pooling implementation have no problem even with a large number of caller(Tanks in the scene). The poolIndex when the error arises is always 0 also, so I have ruled out the possibility of poolIndex exceeding the poolSize. The error only arises when the tanks are grouped very close to each others, hence the shells are being set active and released very rapidly due to the shell hitting each other and the tanks in proximity.

I've considered the logic and I really can't find any edge cases. Which leads me to suspect it might be because of race-condition from threads that cause multiple callers of ReleaseObject() to access poolIndex at the same time.`

Share Improve this question asked Nov 21, 2024 at 5:59 user28406589user28406589 1 2
  • I don't know how you handle released game objects, but if you saw poolIndex was dropped to 0, it means that ReleaseObject is called more times than GetObjectFromPool (IOW a game object is released multiple times). – shingo Commented Nov 21, 2024 at 7:58
  • @shingo Yes this seems to be the case, I printed out some Debug.Log() calls in both GetObjectFromPool() and ReleaseObject() and as the number of callers increase, at some point, the number of release calls actually exceeds the number of get calls, which makes me think this is because the shells somehow registered 2 or more collisions before the first collision have the chance to set the shell inactive. Shortly after, the IndexOutOfRangeException occurred as expected from a corrupt poolIndex. – user28406589 Commented Nov 21, 2024 at 18:03
Add a comment  | 

1 Answer 1

Reset to default 0

Inside the function "GetObjectFromPool" you increase the poolIndex without making a check whether is past the length of your initialized array. You need to check if the index is past and reset it to 0

  public GameObject GetObjectFromPool(){
    GameObject gameObject=pools[poolIndex];
    if (poolIndex + 1 >= pools.length)
        poolIndex=0;
    else
        poolIndex++;

    return gameObject;
}

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1742311133a4419918.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信