Skip to content

Commit bc90bf6

Browse files
committed
Add default CollisionHandler callback functions instead of None to better align API #272
1 parent de52ceb commit bc90bf6

File tree

7 files changed

+144
-82
lines changed

7 files changed

+144
-82
lines changed

CHANGELOG.rst

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ Changelog
77
Added Vec2d.length_squared, and depreacted Vec2d.get_length_sqrd()
88
Added Vec2d.get_distance_squared(), and deprecated Vec2d.get_dist_sqrd()
99
10+
Added default do_nothing and always_collide callback functions to the CollisionHandler, so that its clear how to reset and align with other callbacks.
11+
If in old code you did handler.begin = None, you should now instead to handler.begin = CollisionHandler.always_collide etc.
12+
1013
New feature: ShapeFilter.rejects_collision()
1114
New feature: Added Vec2d.polar_tuple
1215
Optimized Vec2d.angle and Vec2d.angle_degrees (note that the optimized versions treat 0 length vectors with x and/or y equal to -0 slightly differently.)

pymunk/collision_handler.py

+92-66
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
__docformat__ = "reStructuredText"
22

3-
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
3+
from typing import TYPE_CHECKING, Any, Callable, Dict
44

55
if TYPE_CHECKING:
66
from .space import Space
77

88
from ._chipmunk_cffi import ffi, lib
99
from .arbiter import Arbiter
1010

11-
_CollisionCallbackBool = Callable[[Arbiter, "Space", Any], bool]
12-
_CollisionCallbackNoReturn = Callable[[Arbiter, "Space", Any], None]
11+
_CollisionCallbackBool = Callable[[Arbiter, "Space", Dict[Any, Any]], bool]
12+
_CollisionCallbackNoReturn = Callable[[Arbiter, "Space", Dict[Any, Any]], None]
1313

1414

1515
class CollisionHandler(object):
@@ -42,25 +42,13 @@ def __init__(self, _handler: Any, space: "Space") -> None:
4242
self._handler.userData = self._userData
4343

4444
self._space = space
45-
self._begin: Optional[_CollisionCallbackBool] = None
46-
self._pre_solve: Optional[_CollisionCallbackBool] = None
47-
self._post_solve: Optional[_CollisionCallbackNoReturn] = None
48-
self._separate: Optional[_CollisionCallbackNoReturn] = None
45+
self._begin: _CollisionCallbackBool = CollisionHandler.always_collide
46+
self._pre_solve: _CollisionCallbackBool = CollisionHandler.always_collide
47+
self._post_solve: _CollisionCallbackNoReturn = CollisionHandler.do_nothing
48+
self._separate: _CollisionCallbackNoReturn = CollisionHandler.do_nothing
4949

5050
self._data: Dict[Any, Any] = {}
5151

52-
def _reset(self) -> None:
53-
def allways_collide(arb: Arbiter, space: "Space", data: Any) -> bool:
54-
return True
55-
56-
def do_nothing(arb: Arbiter, space: "Space", data: Any) -> None:
57-
return
58-
59-
self.begin = allways_collide
60-
self.pre_solve = allways_collide
61-
self.post_solve = do_nothing
62-
self.separate = do_nothing
63-
6452
@property
6553
def data(self) -> Dict[Any, Any]:
6654
"""Data property that get passed on into the
@@ -72,17 +60,9 @@ def data(self) -> Dict[Any, Any]:
7260
"""
7361
return self._data
7462

75-
def _set_begin(self, func: _CollisionCallbackBool) -> None:
76-
self._begin = func
77-
self._handler.beginFunc = lib.ext_cpCollisionBeginFunc
78-
79-
def _get_begin(self) -> Optional[_CollisionCallbackBool]:
80-
return self._begin
81-
82-
begin = property(
83-
_get_begin,
84-
_set_begin,
85-
doc="""Two shapes just started touching for the first time this step.
63+
@property
64+
def begin(self) -> _CollisionCallbackBool:
65+
"""Two shapes just started touching for the first time this step.
8666
8767
``func(arbiter, space, data) -> bool``
8868
@@ -91,20 +71,24 @@ def _get_begin(self) -> Optional[_CollisionCallbackBool]:
9171
false, the `pre_solve` and `post_solve` callbacks will never be run,
9272
but you will still recieve a separate event when the shapes stop
9373
overlapping.
94-
""",
95-
)
74+
"""
75+
return self._begin
9676

97-
def _set_pre_solve(self, func: _CollisionCallbackBool) -> None:
98-
self._pre_solve = func
99-
self._handler.preSolveFunc = lib.ext_cpCollisionPreSolveFunc
77+
@begin.setter
78+
def begin(self, func: _CollisionCallbackBool) -> None:
79+
assert (
80+
func is not None
81+
), "To reset the begin callback, set handler.begin = CollisionHandler.always_collide"
82+
self._begin = func
10083

101-
def _get_pre_solve(self) -> Optional[_CollisionCallbackBool]:
102-
return self._pre_solve
84+
if self._begin == CollisionHandler.always_collide:
85+
self._handler.beginFunc = ffi.addressof(lib, "AlwaysCollide")
86+
else:
87+
self._handler.beginFunc = lib.ext_cpCollisionBeginFunc
10388

104-
pre_solve = property(
105-
_get_pre_solve,
106-
_set_pre_solve,
107-
doc="""Two shapes are touching during this step.
89+
@property
90+
def pre_solve(self) -> _CollisionCallbackBool:
91+
"""Two shapes are touching during this step.
10892
10993
``func(arbiter, space, data) -> bool``
11094
@@ -113,48 +97,90 @@ def _get_pre_solve(self) -> Optional[_CollisionCallbackBool]:
11397
override collision values using Arbiter.friction, Arbiter.elasticity
11498
or Arbiter.surfaceVelocity to provide custom friction, elasticity,
11599
or surface velocity values. See Arbiter for more info.
116-
""",
117-
)
118-
119-
def _set_post_solve(self, func: _CollisionCallbackNoReturn) -> None:
100+
"""
101+
return self._pre_solve
120102

121-
self._post_solve = func
122-
self._handler.postSolveFunc = lib.ext_cpCollisionPostSolveFunc
103+
@pre_solve.setter
104+
def pre_solve(self, func: _CollisionCallbackBool) -> None:
105+
assert (
106+
func is not None
107+
), "To reset the pre_solve callback, set handler.pre_solve = CollisionHandler.always_collide"
108+
self._pre_solve = func
123109

124-
def _get_post_solve(self) -> Optional[_CollisionCallbackNoReturn]:
125-
return self._post_solve
110+
if self._pre_solve == CollisionHandler.always_collide:
111+
self._handler.preSolveFunc = ffi.addressof(lib, "AlwaysCollide")
112+
else:
113+
self._handler.preSolveFunc = lib.ext_cpCollisionPreSolveFunc
126114

127-
post_solve = property(
128-
_get_post_solve,
129-
_set_post_solve,
130-
doc="""Two shapes are touching and their collision response has been
115+
@property
116+
def post_solve(self) -> _CollisionCallbackNoReturn:
117+
"""Two shapes are touching and their collision response has been
131118
processed.
132119
133120
``func(arbiter, space, data)``
134121
135122
You can retrieve the collision impulse or kinetic energy at this
136123
time if you want to use it to calculate sound volumes or damage
137124
amounts. See Arbiter for more info.
138-
""",
139-
)
125+
"""
126+
return self._post_solve
140127

141-
def _set_separate(self, func: _CollisionCallbackNoReturn) -> None:
142-
self._separate = func
143-
self._handler.separateFunc = lib.ext_cpCollisionSeparateFunc
128+
@post_solve.setter
129+
def post_solve(self, func: _CollisionCallbackNoReturn) -> None:
130+
assert (
131+
func is not None
132+
), "To reset the post_solve callback, set handler.post_solve = CollisionHandler.do_nothing"
133+
self._post_solve = func
144134

145-
def _get_separate(self) -> Optional[_CollisionCallbackNoReturn]:
146-
return self._separate
135+
if self._post_solve == CollisionHandler.do_nothing:
136+
self._handler.postSolveFunc = ffi.addressof(lib, "DoNothing")
137+
else:
138+
self._handler.postSolveFunc = lib.ext_cpCollisionPostSolveFunc
147139

148-
separate = property(
149-
_get_separate,
150-
_set_separate,
151-
doc="""Two shapes have just stopped touching for the first time this
140+
self._handler.postSolveFunc = lib.ext_cpCollisionPostSolveFunc
141+
142+
@property
143+
def separate(self) -> _CollisionCallbackNoReturn:
144+
"""Two shapes have just stopped touching for the first time this
152145
step.
153146
154147
``func(arbiter, space, data)``
155148
156149
To ensure that begin()/separate() are always called in balanced
157150
pairs, it will also be called when removing a shape while its in
158151
contact with something or when de-allocating the space.
159-
""",
160-
)
152+
"""
153+
return self._separate
154+
155+
@separate.setter
156+
def separate(self, func: _CollisionCallbackNoReturn) -> None:
157+
assert (
158+
func is not None
159+
), "To reset the separate callback, set handler.separate = CollisionHandler.do_nothing"
160+
self._separate = func
161+
162+
if self._separate == CollisionHandler.do_nothing:
163+
self._handler.separateFunc = ffi.addressof(lib, "DoNothing")
164+
else:
165+
self._handler.separateFunc = lib.ext_cpCollisionSeparateFunc
166+
167+
@staticmethod
168+
def do_nothing(arbiter: Arbiter, space: "Space", data: Dict[Any, Any]) -> None:
169+
"""The default do nothing method used for the post_solve and seprate
170+
callbacks.
171+
172+
Note that its more efficient to set this method than to define your own
173+
do nothing method.
174+
"""
175+
return
176+
177+
@staticmethod
178+
def always_collide(arbiter: Arbiter, space: "Space", data: Dict[Any, Any]) -> bool:
179+
"""The default method used for the begin and pre_solve callbacks.
180+
181+
It will always return True, meaning the collision should not be ignored.
182+
183+
Note that its more efficient to set this method than to define your own
184+
return True method.
185+
"""
186+
return True

pymunk/tests/test_batch.py

+3
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ def test_get_arbiters(self) -> None:
201201
ints = memoryview(data.int_buf()).cast("P")
202202

203203
def check_arb_data(arb: pymunk.Arbiter) -> None:
204+
assert arb.shapes[0].body is not None
205+
assert arb.shapes[1].body is not None
206+
204207
a_id = arb.shapes[0].body.id
205208
b_id = arb.shapes[1].body.id
206209

pymunk/tests/test_shape.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,9 @@ def testPickle(self) -> None:
195195
self.assertEqual(c.surface_velocity, c2.surface_velocity)
196196
self.assertEqual(c.density, c2.density)
197197
self.assertEqual(c.mass, c2.mass)
198+
assert c.body is not None
198199
self.assertEqual(c.body.mass, c2.body.mass)
199-
200+
self.assertIsNotNone
200201
c = p.Circle(None, 1)
201202
c.density = 3
202203

pymunk/tests/test_space.py

+37-14
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def testProperties(self) -> None:
7373
s.step(0.1)
7474
self.assertEqual(s.current_time_step, 0.1)
7575

76-
self.assertTrue(s.static_body != None)
76+
self.assertTrue(s.static_body is not None)
7777
self.assertEqual(s.static_body.body_type, p.Body.STATIC)
7878

7979
self.assertEqual(s.threads, 1)
@@ -196,7 +196,7 @@ def testPointQueryNearestWithShapeFilter(self) -> None:
196196
s1.filter = f1
197197
hit = s.point_query_nearest((0, 0), 0, f2)
198198
self.assertEqual(
199-
hit != None,
199+
hit is not None,
200200
test["hit"],
201201
"Got {}!=None, expected {} for test: {}".format(hit, test["hit"], test),
202202
)
@@ -647,6 +647,29 @@ def separate(*_: Any) -> None:
647647
s.step(1)
648648
s.remove(c1)
649649

650+
def testCollisionHandlerDefaultCallbacks(self) -> None:
651+
s = p.Space()
652+
653+
b1 = p.Body(1, 1)
654+
c1 = p.Circle(b1, 10)
655+
b1.position = 9, 11
656+
657+
b2 = p.Body(body_type=p.Body.STATIC)
658+
c2 = p.Circle(b2, 10)
659+
b2.position = 0, 0
660+
661+
s.add(b1, c1, b2, c2)
662+
s.gravity = 0, -100
663+
664+
h = s.add_default_collision_handler()
665+
h.begin = h.always_collide
666+
h.pre_solve = h.always_collide
667+
h.post_solve = h.do_nothing
668+
h.separate = h.do_nothing
669+
670+
for _ in range(10):
671+
s.step(0.1)
672+
650673
@unittest.skip("Existing bug in Pymunk. TODO: Fix bug and enable test")
651674
def testRemoveInSeparate(self) -> None:
652675
s = p.Space()
@@ -1057,26 +1080,26 @@ def _testCopyMethod(self, copy_func: Callable[[Space], Space]) -> None:
10571080
# Assert collision handlers
10581081
h2 = s2.add_default_collision_handler()
10591082
self.assertIsNotNone(h2.begin)
1060-
self.assertIsNone(h2.pre_solve)
1061-
self.assertIsNone(h2.post_solve)
1062-
self.assertIsNone(h2.separate)
1083+
self.assertEqual(h2.pre_solve, p.CollisionHandler.always_collide)
1084+
self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing)
1085+
self.assertEqual(h2.separate, p.CollisionHandler.do_nothing)
10631086

10641087
h2 = s2.add_wildcard_collision_handler(1)
1065-
self.assertIsNone(h2.begin)
1088+
self.assertEqual(h2.begin, p.CollisionHandler.always_collide)
10661089
self.assertIsNotNone(h2.pre_solve)
1067-
self.assertIsNone(h2.post_solve)
1068-
self.assertIsNone(h2.separate)
1090+
self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing)
1091+
self.assertEqual(h2.separate, p.CollisionHandler.do_nothing)
10691092

10701093
h2 = s2.add_collision_handler(1, 2)
1071-
self.assertIsNone(h2.begin)
1072-
self.assertIsNone(h2.pre_solve)
1094+
self.assertEqual(h2.begin, p.CollisionHandler.always_collide)
1095+
self.assertEqual(h2.pre_solve, p.CollisionHandler.always_collide)
10731096
self.assertIsNotNone(h2.post_solve)
1074-
self.assertIsNone(h2.separate)
1097+
self.assertEqual(h2.separate, p.CollisionHandler.do_nothing)
10751098

10761099
h2 = s2.add_collision_handler(3, 4)
1077-
self.assertIsNone(h2.begin)
1078-
self.assertIsNone(h2.pre_solve)
1079-
self.assertIsNone(h2.post_solve)
1100+
self.assertEqual(h2.begin, p.CollisionHandler.always_collide)
1101+
self.assertEqual(h2.pre_solve, p.CollisionHandler.always_collide)
1102+
self.assertEqual(h2.post_solve, p.CollisionHandler.do_nothing)
10801103
self.assertIsNotNone(h2.separate)
10811104

10821105
def testPickleCachedArbiters(self) -> None:

pymunk_cffi/extensions.c

+3
Original file line numberDiff line numberDiff line change
@@ -488,3 +488,6 @@ void cpMessage(const char *condition, const char *file, int line, int isError, i
488488
snprintf(formattedMessage, sizeof(formattedMessage), "\tSource: %s:%d", file, line);
489489
ext_pyLog(formattedMessage);
490490
}
491+
492+
static cpBool AlwaysCollide(cpArbiter *arb, cpSpace *space, cpDataPointer data){return cpTrue;}
493+
static void DoNothing(cpArbiter *arb, cpSpace *space, cpDataPointer data){}

pymunk_cffi/extensions_cdef.h

+4-1
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,7 @@ cpContact *cpContactArrAlloc(int count);
102102

103103
cpFloat defaultSpringForce(cpDampedSpring *spring, cpFloat dist);
104104

105-
cpFloat defaultSpringTorque(cpDampedRotarySpring *spring, cpFloat relativeAngle);
105+
cpFloat defaultSpringTorque(cpDampedRotarySpring *spring, cpFloat relativeAngle);
106+
107+
static cpBool AlwaysCollide(cpArbiter *arb, cpSpace *space, cpDataPointer data);
108+
static void DoNothing(cpArbiter *arb, cpSpace *space, cpDataPointer data);

0 commit comments

Comments
 (0)