8 hours ago
Hi Qriva!
Your analysis is correct, but this is a bug that was fixed quite long ago. Which Obi version are you using? DestroyIfUnused() should look like this:
In our git the PR adding checks for pending colliders/rigidbodies/forceZones is dated 19 Jun 2025, so it should be included in the latest version. There's been other minor changes to ObiColliderWorld.cs in the production branch since then, I'm leaving the full file here:
let me know how it goes,
kind regards
Your analysis is correct, but this is a bug that was fixed quite long ago. Which Obi version are you using? DestroyIfUnused() should look like this:
Code:
private void DestroyIfUnused()
{
// when there is no data and no implementations, the world gets destroyed.
// don't check materialHandles.Count == 0, as these are scriptable objects and may outlive the world.
if (collidersToCreate.Count == 0 &&
rigidbodiesToCreate.Count == 0 &&
forceZonesToCreate.Count == 0 &&
colliderHandles.Count == 0 &&
rigidbodyHandles.Count == 0 &&
forceZoneHandles.Count == 0 &&
implementations.Count == 0)
Destroy();
}In our git the PR adding checks for pending colliders/rigidbodies/forceZones is dated 19 Jun 2025, so it should be included in the latest version. There's been other minor changes to ObiColliderWorld.cs in the production branch since then, I'm leaving the full file here:
Code:
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace Obi
{
public class ObiResourceHandle<T> where T : class
{
public T owner = null; /**< reference to the owner instance*/
public int index = -1; /**< index of this resource in the collision world.*/
private int referenceCount = 0; /**< amount of references to this handle. Can be used to clean up any associated resources after it reaches zero.*/
public bool isValid
{
get { return index >= 0; }
}
public void Invalidate()
{
index = -1;
referenceCount = 0;
}
public void Reference()
{
referenceCount++;
}
public bool Dereference()
{
return --referenceCount == 0;
}
public ObiResourceHandle(int index = -1)
{
this.index = index;
owner = null;
}
}
public class ObiColliderHandle : ObiResourceHandle<ObiColliderBase>
{
public ObiColliderHandle(int index = -1) : base(index) { }
}
public class ObiForceZoneHandle : ObiResourceHandle<ObiForceZone>
{
public ObiForceZoneHandle(int index = -1) : base(index) { }
}
public class ObiCollisionMaterialHandle : ObiResourceHandle<ObiCollisionMaterial>
{
public ObiCollisionMaterialHandle(int index = -1) : base(index) { }
}
public class ObiRigidbodyHandle : ObiResourceHandle<ObiRigidbodyBase>
{
public ObiRigidbodyHandle(int index = -1) : base(index) { }
}
public class ObiColliderWorld
{
[NonSerialized] public List<IColliderWorldImpl> implementations;
[NonSerialized] public List<ObiColliderHandle> colliderHandles; // list of collider handles, used by ObiCollider components to retrieve them.
[NonSerialized] public ObiNativeColliderShapeList colliderShapes; // list of collider shapes.
[NonSerialized] public ObiNativeAabbList colliderAabbs; // list of collider bounds.
[NonSerialized] public ObiNativeAffineTransformList colliderTransforms; // list of collider transforms.
[NonSerialized] public List<ObiForceZoneHandle> forceZoneHandles; // list of collider handles, used by ObiForceZone components to retrieve them.
[NonSerialized] public ObiNativeForceZoneList forceZones; // list of collider force zones.
[NonSerialized] public List<ObiCollisionMaterialHandle> materialHandles; // list of material handles, used by ObiCollisionMaterial components to retrieve them.
[NonSerialized] public ObiNativeCollisionMaterialList collisionMaterials; // list of collision materials.
[NonSerialized] public List<ObiRigidbodyHandle> rigidbodyHandles; // list of rigidbody handles, used by ObiRigidbody components to retrieve them.
[NonSerialized] public ObiNativeRigidbodyList rigidbodies; // list of rigidbodies.
[NonSerialized] public ObiTriangleMeshContainer triangleMeshContainer;
[NonSerialized] public ObiEdgeMeshContainer edgeMeshContainer;
[NonSerialized] public ObiDistanceFieldContainer distanceFieldContainer;
[NonSerialized] public ObiHeightFieldContainer heightFieldContainer;
private List<ObiColliderHandle> collidersToCreate;
private List<ObiColliderHandle> collidersToDestroy;
private List<ObiForceZoneHandle> forceZonesToCreate;
private List<ObiForceZoneHandle> forceZonesToDestroy;
private List<ObiRigidbodyHandle> rigidbodiesToCreate;
private List<ObiRigidbodyHandle> rigidbodiesToDestroy;
public int collidersToUpdateCount { private set; get; } = 0; // amount of colliders that need to be updated. These are always grouped at the start of the collider arrays (handles, shapes, etc).
private bool dirty = false;
private static ObiColliderWorld instance;
public static ObiColliderWorld GetInstance()
{
if (instance == null)
{
instance = new ObiColliderWorld();
instance.Initialize();
}
return instance;
}
private void Initialize()
{
// Allocate all lists:
if (implementations == null)
implementations = new List<IColliderWorldImpl>();
if (colliderHandles == null)
colliderHandles = new List<ObiColliderHandle>();
if (colliderShapes == null)
colliderShapes = new ObiNativeColliderShapeList();
if (colliderAabbs == null)
colliderAabbs = new ObiNativeAabbList();
if (colliderTransforms == null)
colliderTransforms = new ObiNativeAffineTransformList();
if (forceZoneHandles == null)
forceZoneHandles = new List<ObiForceZoneHandle>();
if (forceZones == null)
forceZones = new ObiNativeForceZoneList();
if (materialHandles == null)
materialHandles = new List<ObiCollisionMaterialHandle>();
if (collisionMaterials == null)
collisionMaterials = new ObiNativeCollisionMaterialList();
if (rigidbodyHandles == null)
rigidbodyHandles = new List<ObiRigidbodyHandle>();
if (rigidbodies == null)
rigidbodies = new ObiNativeRigidbodyList();
if (triangleMeshContainer == null)
triangleMeshContainer = new ObiTriangleMeshContainer();
if (edgeMeshContainer == null)
edgeMeshContainer = new ObiEdgeMeshContainer();
if (distanceFieldContainer == null)
distanceFieldContainer = new ObiDistanceFieldContainer();
if (heightFieldContainer == null)
heightFieldContainer = new ObiHeightFieldContainer();
if (collidersToCreate == null)
collidersToCreate = new List<ObiColliderHandle>();
if (collidersToDestroy == null)
collidersToDestroy = new List<ObiColliderHandle>();
if (forceZonesToCreate == null)
forceZonesToCreate = new List<ObiForceZoneHandle>();
if (forceZonesToDestroy == null)
forceZonesToDestroy = new List<ObiForceZoneHandle>();
if (rigidbodiesToCreate == null)
rigidbodiesToCreate = new List<ObiRigidbodyHandle>();
if (rigidbodiesToDestroy == null)
rigidbodiesToDestroy = new List<ObiRigidbodyHandle>();
}
private void Destroy()
{
dirty = false;
for (int i = 0; i < implementations.Count; ++i)
{
implementations[i].SetColliders(colliderShapes, colliderAabbs, colliderTransforms);
implementations[i].UpdateWorld(0);
}
// Invalidate all handles:
if (colliderHandles != null)
foreach (var handle in colliderHandles)
handle.Invalidate();
if (rigidbodyHandles != null)
foreach (var handle in rigidbodyHandles)
handle.Invalidate();
if (materialHandles != null)
foreach (var handle in materialHandles)
handle.Invalidate();
if (forceZoneHandles != null)
foreach (var handle in forceZoneHandles)
handle.Invalidate();
// Dispose of all lists:
implementations = null;
colliderHandles = null;
rigidbodyHandles = null;
materialHandles = null;
forceZoneHandles = null;
collidersToCreate = null;
collidersToDestroy = null;
forceZonesToCreate = null;
forceZonesToDestroy = null;
rigidbodiesToCreate = null;
rigidbodiesToDestroy = null;
colliderShapes?.Dispose();
colliderAabbs?.Dispose();
colliderTransforms?.Dispose();
forceZones?.Dispose();
collisionMaterials?.Dispose();
rigidbodies?.Dispose();
triangleMeshContainer?.Dispose();
edgeMeshContainer?.Dispose();
distanceFieldContainer?.Dispose();
heightFieldContainer?.Dispose();
instance = null;
}
private void DestroyIfUnused()
{
// when there is no data and no implementations, the world gets destroyed.
// don't check materialHandles.Count == 0, as these are scriptable objects and may outlive the world.
if (collidersToCreate.Count == 0 &&
rigidbodiesToCreate.Count == 0 &&
forceZonesToCreate.Count == 0 &&
colliderHandles.Count == 0 &&
rigidbodyHandles.Count == 0 &&
forceZoneHandles.Count == 0 &&
implementations.Count == 0)
Destroy();
}
public void RegisterImplementation(IColliderWorldImpl impl)
{
if (!implementations.Contains(impl))
implementations.Add(impl);
}
public void UnregisterImplementation(IColliderWorldImpl impl)
{
implementations.Remove(impl);
DestroyIfUnused();
}
public ObiColliderHandle CreateCollider()
{
var handle = new ObiColliderHandle();
// in-editor, we create data right away since the simulation is not running.
if (!Application.isPlaying)
CreateColliderData(handle);
else
collidersToCreate.Add(handle);
return handle;
}
public ObiForceZoneHandle CreateForceZone()
{
var handle = new ObiForceZoneHandle();
// in-editor, we create data right away since the simulation is not running.
if (!Application.isPlaying)
CreateForceZoneData(handle);
else
forceZonesToCreate.Add(handle);
return handle;
}
public ObiRigidbodyHandle CreateRigidbody()
{
var handle = new ObiRigidbodyHandle();
// in-editor, we create data right away since the simulation is not running.
if (!Application.isPlaying)
CreateRigidbodyData(handle);
else
rigidbodiesToCreate.Add(handle);
return handle;
}
public ObiCollisionMaterialHandle CreateCollisionMaterial()
{
var handle = new ObiCollisionMaterialHandle(materialHandles.Count);
materialHandles.Add(handle);
collisionMaterials.Add(new CollisionMaterial());
return handle;
}
public ObiTriangleMeshHandle GetOrCreateTriangleMesh(Mesh mesh)
{
return triangleMeshContainer.GetOrCreateTriangleMesh(mesh);
}
public void DestroyTriangleMesh(ObiTriangleMeshHandle meshHandle)
{
triangleMeshContainer.DestroyTriangleMesh(meshHandle);
}
public ObiEdgeMeshHandle GetOrCreateEdgeMesh(EdgeCollider2D collider)
{
return edgeMeshContainer.GetOrCreateEdgeMesh(collider);
}
public void DestroyEdgeMesh(ObiEdgeMeshHandle meshHandle)
{
edgeMeshContainer.DestroyEdgeMesh(meshHandle);
}
public ObiDistanceFieldHandle GetOrCreateDistanceField(ObiDistanceField df)
{
return distanceFieldContainer.GetOrCreateDistanceField(df);
}
public void DestroyDistanceField(ObiDistanceFieldHandle dfHandle)
{
distanceFieldContainer.DestroyDistanceField(dfHandle);
}
public ObiHeightFieldHandle GetOrCreateHeightField(TerrainData hf)
{
return heightFieldContainer.GetOrCreateHeightField(hf);
}
public void DestroyHeightField(ObiHeightFieldHandle hfHandle)
{
heightFieldContainer.DestroyHeightField(hfHandle);
}
public void DestroyCollider(ObiColliderHandle handle)
{
// Destroy data right away if no simulation is running.
if (!Application.isPlaying || implementations.Count == 0)
DestroyColliderData(handle);
else
{
// In case the handle is in the creation queue, just remove it.
if (!collidersToCreate.Remove(handle))
collidersToDestroy.Add(handle);
}
}
public void DestroyForceZone(ObiForceZoneHandle handle)
{
// Destroy data right away if no simulation is running.
if (!Application.isPlaying || implementations.Count == 0)
DestroyForceZoneData(handle);
else
{
// In case the handle is in the creation queue, just remove it.
if (!forceZonesToCreate.Remove(handle))
forceZonesToDestroy.Add(handle);
}
}
public void DestroyRigidbody(ObiRigidbodyHandle handle)
{
// Destroy data right away if no simulation is running.
if (!Application.isPlaying || implementations.Count == 0)
DestroyRigidbodyData(handle);
else
{
// In case the handle is in the creation queue, just remove it.
if (!rigidbodiesToCreate.Remove(handle))
rigidbodiesToDestroy.Add(handle);
}
}
public void DestroyCollisionMaterial(ObiCollisionMaterialHandle handle)
{
if (collisionMaterials != null && handle != null && handle.isValid && handle.index < materialHandles.Count)
{
int index = handle.index;
int lastIndex = materialHandles.Count - 1;
// swap all collider info:
materialHandles.Swap(index, lastIndex);
collisionMaterials.Swap(index, lastIndex);
// update the index of the handle we swapped with:
materialHandles[index].index = index;
// invalidate our handle:
// (after updating the swapped one!
// in case there's just one handle in the array,
// we need to write -1 after 0)
handle.Invalidate();
// remove last index:
materialHandles.RemoveAt(lastIndex);
collisionMaterials.count--;
DestroyIfUnused();
}
}
private void DestroyColliderData (ObiColliderHandle handle)
{
if (colliderShapes != null && handle != null && handle.isValid && handle.index < colliderHandles.Count)
{
int index = handle.index;
int lastIndex = colliderHandles.Count - 1;
// swap all collider info:
colliderHandles.Swap(index, lastIndex);
colliderShapes.Swap(index, lastIndex);
colliderAabbs.Swap(index, lastIndex);
colliderTransforms.Swap(index, lastIndex);
// update the index of the handle we swapped with:
colliderHandles[index].index = index;
// invalidate our handle:
// (after updating the swapped one!
// in case there's just one handle in the array,
// we need to write -1 after 0)
handle.Invalidate();
// remove last index:
colliderHandles.RemoveAt(lastIndex);
colliderShapes.count--;
colliderAabbs.count--;
colliderTransforms.count--;
// force all colliders to update next frame, as the index of the data they reference
// (eg the mesh in a MeshCollider) may have changed as a result of deleting this collider's data.
collidersToUpdateCount = colliderHandles.Count;
DestroyIfUnused();
}
}
private void DestroyForceZoneData(ObiForceZoneHandle handle)
{
if (forceZones != null && handle != null && handle.isValid && handle.index < forceZoneHandles.Count)
{
int index = handle.index;
int lastIndex = forceZoneHandles.Count - 1;
// swap all force zone info:
forceZoneHandles.Swap(index, lastIndex);
forceZones.Swap(index, lastIndex);
// update the index of the handle we swapped with:
forceZoneHandles[index].index = index;
// invalidate our handle:
// (after updating the swapped one!
// in case there's just one handle in the array,
// we need to write -1 after 0)
handle.Invalidate();
// remove last index:
forceZoneHandles.RemoveAt(lastIndex);
forceZones.count--;
DestroyIfUnused();
}
}
private void DestroyRigidbodyData(ObiRigidbodyHandle handle)
{
if (rigidbodies != null && handle != null && handle.isValid && handle.index < rigidbodyHandles.Count)
{
int index = handle.index;
int lastIndex = rigidbodyHandles.Count - 1;
// swap all collider info:
rigidbodyHandles.Swap(index, lastIndex);
rigidbodies.Swap(index, lastIndex);
// update the index of the handle we swapped with:
rigidbodyHandles[index].index = index;
// invalidate our handle:
// (after updating the swapped one!
// in case there's just one handle in the array,
// we need to write -1 after 0)
handle.Invalidate();
// remove last index:
rigidbodyHandles.RemoveAt(lastIndex);
rigidbodies.count--;
DestroyIfUnused();
}
}
private void CreateColliderData(ObiColliderHandle handle)
{
handle.index = colliderHandles.Count;
colliderHandles.Add(handle);
colliderShapes.Add(new ColliderShape { materialIndex = -1, rigidbodyIndex = -1, dataIndex = -1 });
colliderAabbs.Add(new Aabb());
colliderTransforms.Add(new AffineTransform());
MarkColliderAsNeedingUpdate(handle);
}
private void CreateForceZoneData(ObiForceZoneHandle handle)
{
handle.index = forceZoneHandles.Count;
forceZoneHandles.Add(handle);
forceZones.Add(new ForceZone());
}
private void CreateRigidbodyData(ObiRigidbodyHandle handle)
{
handle.index = rigidbodyHandles.Count;
rigidbodyHandles.Add(handle);
rigidbodies.Add(new ColliderRigidbody());
}
public bool DoesColliderRequireToBeUpdated(ObiColliderHandle handle)
{
if (handle != null && handle.isValid &&
handle.index < colliderHandles.Count && handle.index < collidersToUpdateCount)
return true;
return false;
}
public void MarkColliderAsNeedingUpdate(ObiColliderHandle handle)
{
if (colliderShapes != null && handle != null && handle.isValid &&
handle.index < colliderHandles.Count && handle.index >= collidersToUpdateCount &&
collidersToUpdateCount < colliderHandles.Count)
{
int index = handle.index;
int lastIndex = collidersToUpdateCount;
// swap all collider info:
colliderHandles.Swap(index, lastIndex);
colliderShapes.Swap(index, lastIndex);
colliderAabbs.Swap(index, lastIndex);
colliderTransforms.Swap(index, lastIndex);
// update handles:
colliderHandles[lastIndex].index = lastIndex;
colliderHandles[index].index = index;
collidersToUpdateCount++;
}
}
public void MarkColliderAsNotNeedingUpdate(ObiColliderHandle handle)
{
if (colliderShapes != null && handle != null && handle.isValid &&
handle.index < colliderHandles.Count && handle.index < collidersToUpdateCount)
{
int index = handle.index;
int lastIndex = collidersToUpdateCount-1;
// swap all collider info:
colliderHandles.Swap(index, lastIndex);
colliderShapes.Swap(index, lastIndex);
colliderAabbs.Swap(index, lastIndex);
colliderTransforms.Swap(index, lastIndex);
// update handles:
colliderHandles[lastIndex].index = lastIndex;
colliderHandles[index].index = index;
collidersToUpdateCount--;
}
}
public void FlushHandleBuffers()
{
// First process destruction, then process creation.
// In case we create a handle and then destroy it,
// we should enqueue it for destruction only if it's not in the creation queue.
// If it is, just remove if from the creation queue.
if (collidersToDestroy != null)
{
foreach (var handle in collidersToDestroy)
DestroyColliderData(handle);
collidersToDestroy?.Clear();
}
if (forceZonesToDestroy != null)
{
foreach (var handle in forceZonesToDestroy)
DestroyForceZoneData(handle);
forceZonesToDestroy?.Clear();
}
if (rigidbodiesToDestroy != null)
{
foreach (var handle in rigidbodiesToDestroy)
DestroyRigidbodyData(handle);
rigidbodiesToDestroy?.Clear();
}
if (collidersToCreate != null)
{
foreach (var handle in collidersToCreate)
CreateColliderData(handle);
collidersToCreate?.Clear();
}
if (forceZonesToCreate != null)
{
foreach (var handle in forceZonesToCreate)
CreateForceZoneData(handle);
forceZonesToCreate?.Clear();
}
if (rigidbodiesToCreate != null)
{
foreach (var handle in rigidbodiesToCreate)
CreateRigidbodyData(handle);
rigidbodiesToCreate?.Clear();
}
}
public void UpdateWorld(float deltaTime, bool updateDynamics = true)
{
if (!dirty)
return;
dirty = false;
// ensure all objects have valid handles.
// May destroy the world if it's empty,
// so we next check that handle/implementations are not null.
FlushHandleBuffers();
// update all colliders:
if (colliderHandles != null)
for (int i = 0; i < collidersToUpdateCount; ++i)
colliderHandles[i].owner.UpdateIfNeeded();
// update all force zones:
if (forceZoneHandles != null)
for (int i = 0; i < forceZoneHandles.Count; ++i)
forceZoneHandles[i].owner.UpdateIfNeeded();
// update rigidbodies:
if (rigidbodyHandles != null && updateDynamics)
for (int i = 0; i < rigidbodyHandles.Count; ++i)
rigidbodyHandles[i].owner.UpdateIfNeeded(deltaTime);
// update implementations:
if (implementations != null)
for (int i = 0; i < implementations.Count; ++i)
{
if (implementations[i].referenceCount > 0)
{
// set arrays:
implementations[i].SetColliders(colliderShapes, colliderAabbs, colliderTransforms);
implementations[i].SetForceZones(forceZones);
implementations[i].SetRigidbodies(rigidbodies);
implementations[i].SetCollisionMaterials(collisionMaterials);
implementations[i].SetTriangleMeshData(triangleMeshContainer.headers, triangleMeshContainer.bihNodes, triangleMeshContainer.triangles, triangleMeshContainer.vertices);
implementations[i].SetEdgeMeshData(edgeMeshContainer.headers, edgeMeshContainer.bihNodes, edgeMeshContainer.edges, edgeMeshContainer.vertices);
implementations[i].SetDistanceFieldData(distanceFieldContainer.headers, distanceFieldContainer.dfNodes);
implementations[i].SetHeightFieldData(heightFieldContainer.headers, heightFieldContainer.samples);
// update world implementation:
if (updateDynamics)
implementations[i].UpdateWorld(deltaTime);
}
}
}
public void SetDirty()
{
dirty = true;
}
public void UpdateCollisionMaterials()
{
if (implementations != null)
for (int i = 0; i < implementations.Count; ++i)
{
if (implementations[i].referenceCount > 0)
{
implementations[i].SetCollisionMaterials(collisionMaterials);
}
}
}
public void UpdateRigidbodyVelocities(ObiSolver solver)
{
if (solver != null && solver.initialized)
{
int count = Mathf.Min(rigidbodyHandles.Count, solver.rigidbodyLinearDeltas.count);
for (int i = 0; i < count; ++i)
rigidbodyHandles[i].owner.UpdateVelocities(solver.rigidbodyLinearDeltas[i], solver.rigidbodyAngularDeltas[i]);
}
solver.rigidbodyLinearDeltas.WipeToZero();
solver.rigidbodyAngularDeltas.WipeToZero();
solver.rigidbodyLinearDeltas.Upload();
solver.rigidbodyAngularDeltas.Upload();
}
}
}let me know how it goes,
kind regards

