|
| 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