-
Notifications
You must be signed in to change notification settings - Fork 191
Description
Circumstances:
- During
Space.step()
, a shape is removed (such as frompost_solve
callback). The shape removal is automatically delayed until the step finishes. - After the step ends, the shape removal occurs and triggers a
separate
callback. - Inside the
separate
callback, an object is removed.
This leads to an error:
File "C:\...\site-packages\pymunk\space.py", line 590, in step
for obj in self._remove_later:
RuntimeError: Set changed size during iteration
What happened was that while the space was iterating over the set of objects to be removed, there was a call to .remove()
in separate
. Since the space is locked during this, a new object is put into the set of objects to be removed, but this is illegal as the set is being iterated. This bug is minor, and I can't really think of a practical situation where this might possibly be encountered.
In Space.step()
:
Lines 588 to 592 in ce59f9a
self.add(*self._add_later) | |
self._add_later.clear() | |
for obj in self._remove_later: | |
self.remove(obj) | |
self._remove_later.clear() |
I'd recommend that any objects removed from these special separate
calls also be removed immediately after since they can be.
Slightly related code improvement
It seems that _add_later
and _remove_later
are set
s instead of list
s which doesn't seem like a good choice. Not only does this forget the actual order and is slightly slower, it also allows duplicates (space.remove(body1, body1)
) in a step but not outside a step. So, they should be list
s instead.
Recommended fix, assuming above code improvement is applied
self.add(*self._add_later)
self._add_later.clear()
# A separate callback may remove objects
while self._remove_later:
remove_later = self._remove_later
self._remove_later = []
self.remove(*remove_later)
With python >=3.8
, a walrus operator may be used for the loop condition.
Minimal reproducible example
import pymunk as pm
space = pm.Space()
space.gravity = 0, -100
body1 = pm.Body()
shape1 = pm.Circle(body1, 20)
shape1.density = 5
shape1.collision_type = 111
floor1 = pm.Segment(space.static_body, (-100,-30), (100,-30), 1)
body2 = pm.Body()
body2.position = 500,0
shape2 = pm.Circle(body2, 20)
shape2.density = 5
shape2.collision_type = 222
floor2 = pm.Segment(space.static_body, (400,-30), (600,-30), 1)
space.add(body1, shape1, body2, shape2, floor1, floor2)
def separate(arbiter: pm.Arbiter, space: pm.Space, data):
print('SEPARATE')
print('REMOVE 2, remove any object')
space.remove(floor2)
def post_solve(arbiter: pm.Arbiter, space: pm.Space, data):
print('POST-SOLVE')
if i == 30:
print('REMOVE 1, causes separate during step')
space.remove(shape1)
# space.remove(shape1, shape1, shape1, shape1)
space.add_wildcard_collision_handler(111).separate = separate
space.add_wildcard_collision_handler(222).post_solve = post_solve
for i in range(60):
print('step START', i)
space.step(1/60)
print('step END', i)