Skip to content

Commit 058a359

Browse files
committed
reshaping MapColliders, los-cocos#252
1 parent 6daa721 commit 058a359

File tree

6 files changed

+549
-222
lines changed

6 files changed

+549
-222
lines changed

cocos/mapcolliders.py

Lines changed: 399 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,399 @@
1+
# ----------------------------------------------------------------------------
2+
# cocos2d
3+
# Copyright (c) 2008-2012 Daniel Moisset, Ricardo Quesada, Rayentray Tappa,
4+
# Lucio Torre
5+
# Copyright (c) 2009-2015 Richard Jones, Claudio Canepa
6+
# All rights reserved.
7+
#
8+
# Redistribution and use in source and binary forms, with or without
9+
# modification, are permitted provided that the following conditions are met:
10+
#
11+
# * Redistributions of source code must retain the above copyright
12+
# notice, this list of conditions and the following disclaimer.
13+
# * Redistributions in binary form must reproduce the above copyright
14+
# notice, this list of conditions and the following disclaimer in
15+
# the documentation and/or other materials provided with the
16+
# distribution.
17+
# * Neither the name of cocos2d nor the names of its
18+
# contributors may be used to endorse or promote products
19+
# derived from this software without specific prior written
20+
# permission.
21+
#
22+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
25+
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
26+
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
27+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
28+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31+
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
32+
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33+
# POSSIBILITY OF SUCH DAMAGE.
34+
# ----------------------------------------------------------------------------
35+
"""Support for handling collisions between an actor and a container of objects"""
36+
37+
from __future__ import division, print_function, unicode_literals
38+
39+
__docformat__ = 'restructuredtext'
40+
41+
42+
class RectMapCollider(object):
43+
"""Helper to handle collisions between an actor and objects in a RectMapLayer
44+
45+
Arguments:
46+
velocity_on_bump (str) : one of ``"bounce"``, ``"stick"``, ``"slide"``.
47+
selects which of the predefined on_bump handlers will be used
48+
Attributes:
49+
on_bump_handler : method to change velocity when a collision was detected
50+
bumped_x (bool) : True if collide_map detected collision in the x-axis
51+
bumped_y (bool) : True if collide_map detected collision in the y-axis
52+
53+
The code that updates actor position and velocity would call
54+
method :meth:`collide_map` to account for collisions
55+
56+
There are basically two ways to include this functionality into an
57+
actor class
58+
59+
- as a component, essentially passing (mapcollider, maplayer) in
60+
the actor's __init__
61+
- mixin style, by using RectMapCollider or a subclass as a secondary
62+
base class for actor.
63+
64+
Component way is more decoupled, Mixin style is more powerful because
65+
the collision code will have access to the entire actor trough his 'self'.
66+
67+
To have a working instance the behavior of velocity in a collision must be
68+
defined, and that's the job of method `on_bump_handler`
69+
70+
- if one of the stock on_bump_<variant> suits the requirements, suffices
71+
`mapcollider.on_bump_handler = mapcollider.on_bump_<desired variant>`
72+
or passing a selector at instantiation time
73+
`mapcollider = MapCollider(<desired variant>)`
74+
75+
- for custom behavior define on_bump_handler in a subclass and instantiate it.
76+
77+
"""
78+
def __init__(self, velocity_on_bump=None):
79+
if velocity_on_bump is not None:
80+
self.on_bump_handler = getattr(self, 'on_bump_' + velocity_on_bump)
81+
82+
# collide_<side>: do something when actor collides 'obj' from side <side>
83+
84+
def collide_bottom(self, obj):
85+
"""placeholder, called when collision with obj's bottom side detected"""
86+
pass
87+
88+
def collide_left(self, obj):
89+
"""placeholder, called when collision with obj's left side detected"""
90+
pass
91+
92+
def collide_right(self, obj):
93+
"""placeholder, called when collision with obj's right side detected"""
94+
pass
95+
96+
def collide_top(self, obj):
97+
"""placeholder, called when collision with obj's top side detected"""
98+
pass
99+
100+
# on_bump_<bump_style>: stock velocity changers when collision happened
101+
102+
def on_bump_bounce(self, vx, vy):
103+
"""Bounces when a wall is touched.
104+
105+
Example use case: bouncing projectiles.
106+
"""
107+
if self.bumped_x:
108+
vx = -vx
109+
if self.bumped_y:
110+
vy = -vy
111+
return vx, vy
112+
113+
def on_bump_stick(self, vx, vy):
114+
"""Stops all movement when any wall is touched.
115+
116+
Example use case: sticky bomb, hook weapon projectile.
117+
"""
118+
if self.bumped_x or self.bumped_y:
119+
vx = vy = 0.0
120+
return vx, vy
121+
122+
def on_bump_slide(self, vx, vy):
123+
"""Blocks movement only in the axis that touched a wall.
124+
125+
Example use case: player in a platformer game.
126+
"""
127+
if self.bumped_x:
128+
vx = 0.0
129+
if self.bumped_y:
130+
vy = 0.0
131+
return vx, vy
132+
133+
# placeholder
134+
def on_bump_handler(self, vx, vy):
135+
"""Returns velocity after all collisions considered by collide_map
136+
137+
Arguments:
138+
vx (float) : velocity in x-axis before collision
139+
vy (float) : velocity in y-axis before collision
140+
141+
Returns:
142+
(vx, vx) : velocity after all collisions considered in collide_map
143+
144+
This is a placeholder, either define a custom one or replace with one
145+
of the stock on_bump_<bump_style> methods
146+
"""
147+
raise ValueError(self.__class__.__name__ +
148+
'.on_bump_handler must be set to a real handler before calling.')
149+
# real code modifies vx, vy and
150+
return vx, vy
151+
152+
def collide_map(self, maplayer, last, new, vx, vy):
153+
"""Constrains a movement ``last`` -> ``new`` by considering collisions
154+
155+
Arguments:
156+
maplayer (RectMapLayer) : layer with solid objects to collide with.
157+
last (Rect) : actor rect before step.
158+
new (Rect): tentative rect after the stepm will be adjusted.
159+
vx (float) : velocity in x-axis used to calculate 'last' -> 'new'
160+
vy (float) : velocity in y-axis used to calculate 'last' -> 'new'
161+
162+
Returns:
163+
(vx, vy) (float, float) : the possibly modified (vx, vy).
164+
165+
Assumes:
166+
'last' does not collide with any object.
167+
168+
The dt involved in 'last' -> 'new' is small enough that no object
169+
can entirely fit between 'last' and 'new'.
170+
171+
Side effects:
172+
``new`` eventually modified to not be into forbidden area.
173+
For each collision with one object's side detected, the method
174+
``self.collide_<side>(obj)`` is called.
175+
176+
if rect ``new`` does not overlap any object in maplayer, the method
177+
- does not modify ``new``.
178+
- returns unchanged (vx, vy).
179+
- no method ``self.collide_<side>`` is called.
180+
- ``self.bumped_x`` and ``self.bumped_y`` both will be ``False``.
181+
182+
if rect ``new`` does overlaps any object in maplayer, the method:
183+
- modifies ``new`` to be the nearest rect to the original ``new``
184+
rect that it is still outside any maplayer object.
185+
- returns a modified (vx, vy) as specified by self.on_bump_handler.
186+
- after return self.bumped_x (resp self.bumped_y) will be True if
187+
an horizontal (resp vertical) collision happened.
188+
- if the movement from ``last`` to the original ``new`` was stopped
189+
by side <side> of object <obj>, then self.collide_<side>(obj) will be called.
190+
191+
Implementation details
192+
193+
Adjusts ``new`` in two passes against each object in maplayer.
194+
195+
In pass one, ``new`` is collision tested against each object in maplayer:
196+
- if collides only in one axis, ``new`` is adjusted as close as possible but not overlapping object
197+
- if not overlapping, nothing is done
198+
- if collision detected on both axis, let second pass handle it
199+
200+
In pass two, ``new`` is collision tested against the objects with double collisions in pass one:
201+
- if a collision is detected, adjust ``new`` as close as possible but not overlapping object,
202+
ie. use the smallest displacement on either X or Y axis. If they are both equal, move on
203+
both axis.
204+
"""
205+
self.bumped_x = False
206+
self.bumped_y = False
207+
objects = maplayer.get_in_region(*(new.bottomleft + new.topright))
208+
# first pass, adjust for collisions in only one axis
209+
collide_later = set()
210+
for obj in objects:
211+
# the if is not superfluous in the loop because 'new' can change
212+
if obj is None or obj.tile is None or not obj.intersects(new):
213+
continue
214+
dx_correction, dy_correction = self.detect_collision(obj, last, new)
215+
if dx_correction == 0.0 or dy_correction == 0.0:
216+
self.resolve_collision(obj, new, dx_correction, dy_correction)
217+
else:
218+
collide_later.add(obj)
219+
220+
# second pass, for objs that initially collided in both axis
221+
for obj in collide_later:
222+
if obj.intersects(new):
223+
dx_correction, dy_correction = self.detect_collision(obj, last, new)
224+
if abs(dx_correction) < abs(dy_correction):
225+
# do correction only on X (below)
226+
dy_correction = 0.0
227+
elif abs(dy_correction) < abs(dx_correction):
228+
# do correction only on Y (below)
229+
dx_correction = 0.0
230+
self.resolve_collision(obj, new, dx_correction, dy_correction)
231+
232+
vx, vy = self.on_bump_handler(vx, vy)
233+
234+
return vx, vy
235+
236+
def detect_collision(self, obj, last, new):
237+
"""returns minimal correction in each axis to not collide with obj
238+
239+
Arguments:
240+
obj : object in a MapLayer
241+
last (Rect) : starting rect for the actor step
242+
new (Rect) : tentative actor's rect after step
243+
244+
Decides if there is a collision with obj when moving ``last`` -> ``new``
245+
and then returns the minimal correctioin in each axis as to not collide.
246+
247+
It can be overridden to be more selective about when a collision exists
248+
(see the matching method in :class:`RectMapWithPropsCollider` for example).
249+
"""
250+
dx_correction = dy_correction = 0.0
251+
if last.bottom >= obj.top > obj.bottom:
252+
dy_correction = obj.top - new.bottom
253+
elif last.top <= obj.bottom < new.top:
254+
dy_correction = obj.bottom - new.top
255+
if last.right <= obj.left < new.right:
256+
dx_correction = obj.left - new.right
257+
elif last.left >= obj.right > new.left:
258+
dx_correction = obj.right - new.left
259+
return dx_correction, dy_correction
260+
261+
def resolve_collision(self, obj, new, dx_correction, dy_correction):
262+
"""Corrects ``new`` to just avoid collision with obj, does side effects.
263+
264+
Arguments:
265+
obj (obj) : the object colliding with ``new``.
266+
new (Rect) : tentative actor position before considering
267+
collision with ``obj``.
268+
dx_correction (float) : smallest correction needed on
269+
``new`` x position not to collide ``obj``.
270+
dy_correction (float) : smallest correction needed on
271+
``new`` y position not to collide ``obj``.
272+
273+
The correction is applied to ``new`` position.
274+
275+
If a collision along the x-axis (respectively y-axis) was detected,
276+
the flag ``self.bumped_x`` (resp y) is set.
277+
278+
If the movement towards the original ``new`` was stopped by side <side>
279+
of object <obj>, then ``self.collide_<side>(obj)`` will be called.
280+
"""
281+
if dx_correction != 0.0:
282+
# Correction on X axis
283+
self.bumped_x = True
284+
new.left += dx_correction
285+
if dx_correction > 0.0:
286+
self.collide_left(obj)
287+
else:
288+
self.collide_right(obj)
289+
if dy_correction != 0.0:
290+
# Correction on Y axis
291+
self.bumped_y = True
292+
new.top += dy_correction
293+
if dy_correction > 0.0:
294+
self.collide_bottom(obj)
295+
else:
296+
self.collide_top(obj)
297+
298+
299+
class RectMapWithPropsCollider(RectMapCollider):
300+
"""Helper to handle collisions between an actor and objects in a RectMapLayer
301+
302+
Same as RectMapCollider except that collision detection is more fine grained.
303+
Collision happens only on objects sides with prop(<side>) set.
304+
305+
Look at :class:`RectMapCollider` for details
306+
"""
307+
308+
def detect_collision(self, obj, last, new):
309+
"""Returns minimal correction in each axis to not collide with obj
310+
311+
Collision happens only on objects sides with prop <side> set.
312+
"""
313+
# shorthand for getting a prop from obj
314+
g = obj.get
315+
dx_correction = dy_correction = 0.0
316+
if g('top') and last.bottom >= obj.top > obj.bottom:
317+
dy_correction = obj.top - new.bottom
318+
elif g('bottom') and last.top <= obj.bottom < new.top:
319+
dy_correction = obj.bottom - new.top
320+
if g('left') and last.right <= obj.left < new.right:
321+
dx_correction = obj.left - new.right
322+
elif g('right') and last.left >= obj.right > new.left:
323+
dx_correction = obj.right - new.left
324+
return dx_correction, dy_correction
325+
326+
327+
class TmxObjectMapCollider(RectMapCollider):
328+
"""Helper to handle collisions between an actor and objects in a TmxObjectLayer
329+
330+
Same as RectMapCollider except maplayer is expected to be a :class:`TmxObjectLayer`, so
331+
the objects to collide are TmxObject instances.
332+
333+
Look at :class:`RectMapCollider` for details
334+
"""
335+
def collide_map(self, maplayer, last, new, vx, vy):
336+
"""Constrains a movement ``last`` -> ``new`` by considering collisions
337+
338+
Arguments:
339+
maplayer (RectMapLayer) : layer with solid objects to collide with.
340+
last (Rect) : actor rect before step.
341+
new (Rect): tentative rect after the stepm will be adjusted.
342+
vx (float) : velocity in x-axis used to calculate 'last' -> 'new'
343+
vy (float) : velocity in y-axis used to calculate 'last' -> 'new'
344+
345+
Returns:
346+
vx, vy (float, float) : the possibly modified (vx, vy).
347+
348+
See :meth:`RectMapCollider.collide_map` for side effects and details
349+
"""
350+
self.bumped_x = False
351+
self.bumped_y = False
352+
objects = maplayer.get_in_region(*(new.bottomleft + new.topright))
353+
# first pass, adjust for collisions in only one axis
354+
collide_later = set()
355+
for obj in objects:
356+
# the if is not superfluous in the loop because 'new' can change
357+
if not obj.intersects(new):
358+
continue
359+
dx_correction, dy_correction = self.detect_collision(obj, last, new)
360+
if dx_correction == 0.0 or dy_correction == 0.0:
361+
self.resolve_collision(obj, new, dx_correction, dy_correction)
362+
else:
363+
collide_later.add(obj)
364+
365+
# second pass, for objs that initially collided in both axis
366+
for obj in collide_later:
367+
if obj.intersects(new):
368+
dx_correction, dy_correction = self.detect_collision(obj, last, new)
369+
if abs(dx_correction) < abs(dy_correction):
370+
# do correction only on X (below)
371+
dy_correction = 0.0
372+
elif abs(dy_correction) < abs(dx_correction):
373+
# do correction only on Y (below)
374+
dx_correction = 0.0
375+
self.resolve_collision(obj, new, dx_correction, dy_correction)
376+
377+
vx, vy = self.on_bump_handler(vx, vy)
378+
379+
return vx, vy
380+
381+
382+
def make_collision_handler(collider, maplayer):
383+
"""Returns ``f = collider.collide_map(maplayer, ...)``
384+
385+
Returns:
386+
f : ``(last, new, vx, vy)`` -> ``(vx, vy)``
387+
388+
Utility function to create a collision handler by combining
389+
390+
Arguments:
391+
maplayer : tells the objects to collide with.
392+
collider : tells how velocity changes on collision and resolves
393+
actual collisions.
394+
"""
395+
396+
def collision_handler(last, new, vx, vy):
397+
return collider.collide_map(maplayer, last, new, vx, vy)
398+
399+
return collision_handler

0 commit comments

Comments
 (0)