You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 05:07:21