Map Colliders ============= Overview -------- If a scene has an actor moving, we may update its position from frame to frame using the discrete approximation:: velocity = velocity + acceleration * dt position = position + velocity * dt That's fine when the actor does not find an obstacle, like a wall. If there is an obstacle, we usually want to - stop the position change so the actor touches the obstacle but does not overlap it. - do something sensible with the velocity (stop ?, bounce ?). - maybe do some action based on which obstacle (spikes ?, glass ?). To do this we can represent the actor and the obstacles as rects with sides parallel to the axis, and do the calculations. This is what `map colliders` do given: - the actor velocity, rect before and tentative rect after - a `map layer` that contains a number of obstacles it will - Properly update the velocity and the position. - Call appropriate methods when it detects the actor bumped into an obstacle. - Tell if any collision in the x and / or y axis happened. While some `mapcolliders` are meant to be used in tiled maps ( :class:`~.mapcolliders.RectMapCollider`, :class:`.RectMapWithPropsCollider`), others (:class:`.TmxObjectMapCollider`) can be used with no tiles at all. Properly update the velocity and the position ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The position and the velocity update can be written as:: vx, vy = actor.velocity # using the player controls, gravity and other acceleration influences # update the velocity vx = (keyboard[key.RIGHT] - keyboard[key.LEFT]) * actor.MOVE_SPEED vy += GRAVITY * dt if actor.on_ground and keyboard[key.SPACE]: vy = actor.JUMP_SPEED # with the updated velocity calculate the (tentative) displacement dx = vx * dt dy = vy * dt # get the player's current bounding rectangle last = actor.get_rect() # build the tentative displaced rect new = last.copy() new.x += dx new.y += dy # account for hitting obstacles, it will adjust new and vx, vy actor.velocity = mapcollider.collide_map(maplayer, last, new, vx, vy) # update on_ground status actor.on_ground = (new.y == last.y) # update player position; player position is anchored at the center of the image rect actor.position = new.center How velocity changes in a collision is handled by method :attr:`~.mapcolliders.RectMapCollider.on_bump_handler()`; it can be set to custom code or to one of the stock handlers: - :meth:`~.mapcolliders.RectMapCollider.on_bump_bounce()`: Bounces when a wall is touched. - :meth:`~.mapcolliders.RectMapCollider.on_bump_stick()`: Stops all movement when any wall is touched. (sticky bomb...). - :meth:`~.mapcolliders.RectMapCollider.on_bump_slide()`: Blocks movement only in the axis that touched a wall. (player...). For convenience, a stock handler can be specified at instantiation time with the ``velocity_on_bump`` parameter. Call appropriate methods when it detects actor bumped into an obstacle ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When ``mapcollider.collide_map`` detects that actor collides with obj 'someobj' from side 'someside', it will call ``mapcollider.collide_(someobj)``. There 'someside' is one of 'left', 'right', 'top' and 'bottom'. This provides an opportunity to do things based on which object the actor touched, like 'spike' ? -> init the spikes animation and kill the actor. Note that each ``mapcollider.collide_*`` can receive multiple calls in the same ``mapcollider.collide_map`` call. Tell if any collision in the x and / or y axis happened ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ After ``collide_map`` returns, the flags ``mapcollider.bumped_x`` and ``mapcollider.bumped_y`` tells if there has been any collisions along the respective axis. This can be handy to flip the actor's animation direction, by example from 'walk_left' to 'walk_right'. Variants --------- Currently they are three map colliders variants: RectMapCollider ^^^^^^^^^^^^^^^ Obstacles are rectangular tiles grouped into a :class:`.RectMapLayer`; all non empty cells are deemed solid. :class:`implementation ` RectMapWithPropsCollider ^^^^^^^^^^^^^^^^^^^^^^^^ Obstacles are rectangular tiles grouped into a :class:`.RectMapLayer`, cells use properties "left", "right", "top" and "bottom" to signal which side(s) block movements. A solid block would probably have all four sides set; a platform can set only the top so the player might jump up from underneath and pass through. For convenience these properties would typically be set on the tiles themselves, rather than on individual cells. Of course for the cell which is the entrance to a secret area you could override a wall's properties to set the side to False and allow ingress. See :ref:`cell_properties_label` for information about properties. :class:`implementation ` TmxObjectMapCollider ^^^^^^^^^^^^^^^^^^^^ Obstacles are :class:`.TmxObjects` grouped into a :class:`.TmxObjectLayer`, each object is meant to block movement. A common use case is to do moving platforms in a platform game. :class:`implementation ` How to use ----------------------- There are basically two ways to include this functionality into an actor class: - as a component, essentially passing (mapcollider, maplayer) in the actor's __init__ method. - mixin style, by using :class:`~.mapcolliders.RectMapCollider` or a subclass as a secondary base class for the actor. The component way is more decoupled while the Mixin style is more powerful because the collision code will have access to the entire actor through its ``self`` attribute. To have a working instance, the behavior of the velocity in a collision must be defined, and that's the job of the method ``on_bump_handler`` - if one of the stock ``on_bump_`` suits the requirements, suffices to write:: mapcollider.on_bump_handler = mapcollider.on_bump_ or passing a selector at instantiation time:: mapcollider = MapCollider() - for custom behavior define ``on_bump_handler`` in a subclass and instantiate it.