Unity对象池同时释放多物体时触发IndexOutOfRangeException
坦克炮弹对象池触发IndexOutOfRangeException问题
在Unity项目中采用对象池管理坦克发射的炮弹以优化性能,当坦克近距离聚集时,炮弹会被快速激活与释放,此时触发了IndexOutOfRangeException。
相关代码实现
GameManager(对象池核心逻辑)
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--; } }
PooledObject类
using UnityEngine; public class PooledObject : MonoBehaviour { public int PoolIndex; }
Shell脚本
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; } }
错误信息
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)
排查情况
- 大量坦克单独调用无异常,错误触发时
poolIndex始终为0,排除超出池大小的可能 - 推测是同一帧内多个炮弹同时触发碰撞回调,并发访问
poolIndex导致其被多次递减,最终在ReleaseObject中访问pools[poolIndex-1]时出现负数索引越界
解决方案
1. 增加并发访问保护
Unity的物理回调可能在同一帧内被多个对象触发,导致ReleaseObject被并行执行。通过添加锁机制,确保对poolIndex和数组的操作是原子性的:
private readonly object _poolLock = new object(); public GameObject GetObjectFromPool(){ lock(_poolLock){ if(poolIndex >= poolSize) return null; // 额外增加边界判断,防止取超出池大小的对象 GameObject gameObject=pools[poolIndex]; poolIndex++; return gameObject; } } public void ReleaseObject(GameObject gameObject){ lock(_poolLock){ if(poolIndex <= 0) return; // 避免poolIndex为0时继续递减 gameObject.SetActive(false); GameObject latestActive=pools[poolIndex-1]; PooledObject releaseComponent=gameObject.GetComponent<PooledObject>(); PooledObject activeComponent=latestActive.GetComponent<PooledObject>(); int releaseIndex=releaseComponent.PoolIndex; int activeIndex=activeComponent.PoolIndex; releaseComponent.PoolIndex=activeIndex; activeComponent.PoolIndex=releaseIndex; pools[activeIndex]=gameObject; pools[releaseIndex]=latestActive; poolIndex--; } }
2. 改用Queue/Stack简化对象池实现
手动维护数组索引容易出错,改用Queue管理闲置对象、HashSet管理活跃对象,更简洁且避免索引越界问题:
public class GameManager : MonoBehaviour { private int poolSize; public GameObject ObjectToPool; public static GameManager manager; private Queue<GameObject> _idlePool = new Queue<GameObject>(); private HashSet<GameObject> _activeObjects = new HashSet<GameObject>(); void Awake() { if(manager==null){ manager=this; } poolSize=3*GetPooledObjectUserCount("Tank"); InitializePool(); } private int GetPooledObjectUserCount(string tag){ return GameObject.FindGameObjectsWithTag(tag).Length; } private void InitializePool(){ for(int i=0; i<poolSize; i++){ GameObject tmp=Instantiate(ObjectToPool); tmp.SetActive(false); _idlePool.Enqueue(tmp); } } public GameObject GetObjectFromPool(){ if(_idlePool.Count == 0) return null; // 无闲置对象时返回null或扩展池 GameObject obj = _idlePool.Dequeue(); obj.SetActive(true); _activeObjects.Add(obj); return obj; } public void ReleaseObject(GameObject gameObject){ if(!_activeObjects.Contains(gameObject)) return; // 避免重复释放 gameObject.SetActive(false); _activeObjects.Remove(gameObject); _idlePool.Enqueue(gameObject); } }
这种实现无需手动维护索引,完全规避了数组越界的风险,同时逻辑更清晰。
3. 额外防护:在Shell中增加释放检查
避免同一炮弹被多次释放(比如碰撞触发多次回调),在Shell脚本中添加标记:
private bool _isReleased = false; void OnCollisionEnter(Collision collision) { if(_isReleased) return; _isReleased = true; GameManager.manager.ReleaseObject(gameObject); GameObject shockwave=Instantiate(explosion, shellTransform.position, shellTransform.rotation); Destroy(shockwave, 0.5f); }
内容的提问来源于stack exchange,提问作者user28406589
相关产品推荐
相关产品推荐

