stride3d/stride · error · InvalidOperationException

The collision object has been destroyed.

Error message

The collision object has been destroyed.

What it means

ContactChanged awaits a channel that fires when the contact points of a given Collision change. The Simulation keeps a registry of live collisions (collisions dict); if the passed Collision is not in it, it was never registered or has already been removed (destroyed), so awaiting its changes is meaningless. The library throws InvalidOperationException to signal use of a stale/foreign Collision handle.

Solutions

  1. Check simulation.Collisions (or track collision lifetime) and only call ContactChanged while the collision is still alive.
  2. Restructure async code so the awaiter is cancelled when the collision ends instead of awaiting a destroyed collision.
  3. Verify the Collision came from the same Simulation instance that ContactChanged is called on.

Example fix

// before
var awaiter = simulation.ContactChanged(cachedCollision);
// after
if (simulation.Collisions.Contains(cachedCollision))
{
    var awaiter = simulation.ContactChanged(cachedCollision);
}
else
{
    cachedCollision = null; // collision was destroyed; skip
}
Defensive patterns

Strategy: validation

Validate before calling

bool canAwait = simulation.Collisions.Contains(collision); // only call ContactChanged when true

Type guard

bool IsAlive(Collision c) => c != null && simulation.Collisions.Contains(c);

Try / catch

try { awaiter = simulation.ContactChanged(coll); } catch (InvalidOperationException) { /* collision destroyed; abandon wait */ }

Prevention

When it happens

Trigger: Calling simulation.ContactChanged(coll) with a Collision that was already removed from the simulation (its entity/component destroyed and EndContact/cleanup ran), or with a Collision belonging to a different Simulation instance.

Common situations: Caching a Collision from a collision-start event and using it later in an async micro-thread after the physics pair separated and was destroyed; running ContactChanged during teardown before cleanup.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14). Data as JSON: /api/errors/a50bf7dc18d247fa. Report an issue: GitHub.

Appendix: source

Thrown at sources/engine/Stride.Physics/Simulation.cs:518

                        Distance = point.m_distance1,
                        Normal = point.m_normalWorldOnB,
                        PositionOnA = point.m_positionWorldOnA,
                        PositionOnB = point.m_positionWorldOnB,
                        AppliedImpulse = point.m_appliedImpulse,
                        AppliedImpulseLateral1 = point.m_appliedImpulseLateral1,
                        AppliedImpulseLateral2 = point.m_appliedImpulseLateral2
                    });
                }
            }

            return buffer;
        }


        internal ChannelMicroThreadAwaiter<HashSet<ContactPoint>> ContactChanged(Collision coll)
        {
            if (collisions.ContainsKey(coll) == false)
                throw new InvalidOperationException("The collision object has been destroyed.");

            // Forces this frame's contact to be retrieved and stored so that we can compare it for changes
            LatestContactPointsFor(coll);

            if (contactChangedChannels.TryGetValue(coll, out var tuple))
                return tuple.Channel.Receive();

            var channel = channelsPool.Count == 0 ? new Channel<HashSet<ContactPoint>>{ Preference = ChannelPreference.PreferSender } : channelsPool.Pop();
            contactChangedChannels[coll] = (channel, null);
            return channel.Receive();
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            //if (mSoftRigidDynamicsWorld != null) mSoftRigidDynamicsWorld.Dispose();

View on GitHub (pinned to 96fad776d2)