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_<someside>(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 <cocos.mapcolliders.RectMapCollider>`

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 <cocos.mapcolliders.RectMapWithPropsCollider>`


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 <cocos.mapcolliders.TmxObjectMapCollider>`


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_<variant>`` suits the requirements, 
      suffices to write::
      
        mapcollider.on_bump_handler = mapcollider.on_bump_<desired variant>

      or passing a selector at instantiation time::
      
        mapcollider = MapCollider(<desired variant>)

    - for custom behavior define ``on_bump_handler`` in a subclass and 
      instantiate it.
