Source code for xsge.physics

# xSGE Physics Framework
# Copyright (C) 2014 Julian Marchant <onpon4@riseup.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
This module provides an easy-to-use framework for collision physics.
This is especially useful for platformers, though it can also be useful
for other types of games.
"""

from __future__ import division
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals

import sge


__all__ = ["Collider", "SolidLeft", "SolidRight", "SolidTop", "SolidBottom",
           "Solid", "SlopeTopLeft", "SlopeTopRight", "SlopeBottomLeft",
           "SlopeBottomRight"]


[docs]class Collider(sge.Object): """ Class for objects which have physics interactions. .. note:: This class depends on use of :meth:`Collider.move_x` and :meth:`Collider.move_y` to handle physics interactions. :meth:`event_update_position` uses these methods, so speed attributes will work properly, but changing :attr:`x` and :attr:`y` manually will not cause any physics to occur. """
[docs] def move_x(self, move): """Move the object horizontally, handling physics. Arguments: - ``move`` -- The amount to add to :attr:`x`. """ sticky = False old_x = self.x old_y = self.y old_bbox_left = self.bbox_left old_bbox_right = self.bbox_right old_bbox_top = self.bbox_top old_bbox_bottom = self.bbox_bottom self.x += move if move > 0: for slope in self.collision(SlopeTopRight, x=(old_x - 1)): y = slope.get_slope_y(old_bbox_left) if (slope.xsticky and old_bbox_bottom >= y and (not self.collision(slope, x=old_x) or old_bbox_bottom - 1 < y)): sticky = 1 break else: for slope in self.collision(SlopeBottomRight, x=(old_x - 1)): y = slope.get_slope_y(old_bbox_left) if (slope.xsticky and old_bbox_top <= y and (not self.collision(slope, x=old_x) or old_bbox_top + 1 > y)): sticky = 2 break for other in self.collision(SolidLeft): if not self.collision(other, x=old_x): self.bbox_right = min(self.bbox_right, other.bbox_left) self.event_collision_right(other) other.event_collision_left(self) for other in self.collision(SlopeTopLeft): oy = other.get_slope_y(old_bbox_right) y = other.get_slope_y(self.bbox_right) if self.bbox_bottom > y: if old_bbox_bottom <= oy: self.move_y(y - self.bbox_bottom) x = other.get_slope_x(self.bbox_bottom) self.bbox_right = min(self.bbox_right, x) self.event_collision_right(other) other.event_collision_left(self) elif not self.collision(other, x=old_x): self.bbox_right = min(self.bbox_right, other.bbox_left) self.event_collision_right(other) other.event_collision_left(self) for other in self.collision(SlopeBottomLeft): oy = other.get_slope_y(old_bbox_right) y = other.get_slope_y(self.bbox_right) if self.bbox_top < y: if old_bbox_top >= oy: self.move_y(y - self.bbox_top) x = other.get_slope_x(self.bbox_top) self.bbox_right = min(self.bbox_right, x) self.event_collision_right(other) other.event_collision_left(self) elif not self.collision(other, x=old_x): self.bbox_right = min(self.bbox_right, other.bbox_left) self.event_collision_right(other) other.event_collision_left(self) elif move < 0: for slope in self.collision(SlopeTopLeft, x=(old_x + 1)): y = slope.get_slope_y(old_bbox_right) if (slope.xsticky and old_bbox_bottom >= y and (not self.collision(slope, x=old_x) or old_bbox_bottom - 1 < y)): sticky = 1 break else: for slope in self.collision(SlopeBottomLeft, x=(old_x + 1)): y = slope.get_slope_y(old_bbox_right) if (slope.xsticky and old_bbox_top <= y and (not self.collision(slope, x=old_x) or old_bbox_top + 1 > y)): sticky = 2 break for other in self.collision(SolidRight): if not self.collision(other, x=old_x): self.bbox_left = max(self.bbox_left, other.bbox_right) self.event_collision_left(other) other.event_collision_right(self) for other in self.collision(SlopeTopRight): oy = other.get_slope_y(old_bbox_left) y = other.get_slope_y(self.bbox_left) if self.bbox_bottom > y: if old_bbox_bottom <= oy: self.move_y(y - self.bbox_bottom) x = other.get_slope_x(self.bbox_bottom) self.bbox_right = max(self.bbox_right, x) self.event_collision_left(other) other.event_collision_right(self) elif not self.collision(other, x=old_x): self.bbox_left = max(self.bbox_left, other.bbox_right) self.event_collision_left(other) other.event_collision_right(self) for other in self.collision(SlopeBottomRight): oy = other.get_slope_y(old_bbox_left) y = other.get_slope_y(self.bbox_left) if self.bbox_top < y: if old_bbox_top >= oy: self.move_y(y - self.bbox_top) x = other.get_slope_x(self.bbox_top) self.bbox_left = max(self.bbox_left, x) self.event_collision_left(other) other.event_collision_right(self) elif not self.collision(other, x=old_x): self.bbox_left = max(self.bbox_left, other.bbox_right) self.event_collision_left(other) other.event_collision_right(self) # Engage stickiness (same whether moving left or right) # 1 = sticking to the floor # 2 = sticking to the ceiling if sticky == 1: if (not self.get_bottom_touching_slope() and not self.get_bottom_touching_wall()): new_bbox_bottom = None for other in sge.game.current_room.objects: if (other.bbox_left >= self.bbox_right or other.bbox_right <= self.bbox_left): continue if isinstance(other, SolidTop): y = other.bbox_top elif isinstance(other, SlopeTopLeft): y = other.get_slope_y(self.bbox_right) elif isinstance(other, SlopeTopRight): y = other.get_slope_y(self.bbox_left) else: continue if (y >= self.bbox_bottom and (new_bbox_bottom is None or y < new_bbox_bottom)): new_bbox_bottom = y if new_bbox_bottom is not None: self.bbox_bottom = new_bbox_bottom elif sticky == 2: if (not self.get_top_touching_slope() and not self.get_top_touching_wall()): new_bbox_top = None for other in sge.game.current_room.objects: if (other.bbox_left >= self.bbox_right or other.bbox_right <= self.bbox_left): continue if isinstance(other, SolidBottom): y = other.bbox_bottom elif isinstance(other, SlopeBottomLeft): y = other.get_slope_y(self.bbox_right) elif isinstance(other, SlopeBottomRight): y = other.get_slope_y(self.bbox_left) else: continue if y <= self.bbox_top and (new_bbox_top is None or y > new_bbox_top): new_bbox_top = y if new_bbox_top is not None: self.bbox_top = new_bbox_top
[docs] def move_y(self, move): """Move the object vertically, handling physics. Arguments: - ``move`` -- The amount to add to :attr:`y`. """ sticky = False old_x = self.x old_y = self.y old_bbox_left = self.bbox_left old_bbox_right = self.bbox_right old_bbox_top = self.bbox_top old_bbox_bottom = self.bbox_bottom self.y += move if move > 0: for slope in self.collision(SlopeBottomLeft, y=(old_y - 1)): x = slope.get_slope_x(old_bbox_top) if (slope.ysticky and old_bbox_right >= x and (not self.collision(slope, y=old_y) or old_bbox_right - 1 < x)): sticky = 1 break else: for slope in self.collision(SlopeBottomRight, y=(old_y - 1)): x = slope.get_slope_x(old_bbox_top) if (slope.ysticky and old_bbox_left <= x and (not self.collision(slope, y=old_y) or old_bbox_left + 1 > x)): sticky = 2 break for other in self.collision(SolidTop): if not self.collision(other, y=old_y): self.bbox_bottom = min(self.bbox_bottom, other.bbox_top) self.event_collision_bottom(other) other.event_collision_top(self) for other in self.collision(SlopeTopLeft): ox = other.get_slope_x(old_bbox_bottom) x = other.get_slope_x(self.bbox_bottom) if self.bbox_right > x: if old_bbox_right <= ox: self.move_x(x - self.bbox_right) y = other.get_slope_y(self.bbox_right) self.bbox_bottom = min(self.bbox_bottom, y) self.event_collision_bottom(other) other.event_collision_top(self) elif not self.collision(other, y=old_y): self.bbox_bottom = min(self.bbox_bottom, other.bbox_top) self.event_collision_bottom(other) other.event_collision_top(self) for other in self.collision(SlopeTopRight): ox = other.get_slope_x(old_bbox_bottom) x = other.get_slope_x(self.bbox_bottom) if self.bbox_left < x: if old_bbox_left >= ox: self.move_x(x - self.bbox_left) y = other.get_slope_y(self.bbox_left) self.bbox_bottom = min(self.bbox_bottom, y) self.event_collision_bottom(other) other.event_collision_top(self) elif not self.collision(other, y=old_y): self.bbox_bottom = min(self.bbox_bottom, other.bbox_top) self.event_collision_bottom(other) other.event_collision_top(self) elif move < 0: for slope in self.collision(SlopeTopLeft, y=(old_y + 1)): x = slope.get_slope_x(old_bbox_bottom) if (slope.ysticky and old_bbox_right >= x and (not self.collision(slope, y=old_y) or old_bbox_right - 1 < x)): sticky = 1 break else: for slope in self.collision(SlopeTopRight, y=(old_y + 1)): x = slope.get_slope_x(old_bbox_bottom) if (slope.ysticky and old_bbox_left <= x and (not self.collision(slope, y=old_y) or old_bbox_left + 1 > x)): sticky = 2 break for other in self.collision(SolidBottom): if not self.collision(other, y=old_y): self.bbox_top = max(self.bbox_top, other.bbox_bottom) self.event_collision_top(other) other.event_collision_bottom(self) for other in self.collision(SlopeBottomLeft): ox = other.get_slope_x(old_bbox_top) x = other.get_slope_x(self.bbox_top) if self.bbox_right > x: if old_bbox_right <= ox: self.move_x(x - self.bbox_right) y = other.get_slope_y(self.bbox_right) self.bbox_top = max(self.bbox_top, y) self.event_collision_top(other) other.event_collision_bottom(self) elif not self.collision(other, y=old_y): self.bbox_top = max(self.bbox_top, other.bbox_bottom) self.event_collision_top(other) other.event_collision_bottom(self) for other in self.collision(SlopeBottomRight): ox = other.get_slope_x(old_bbox_top) x = other.get_slope_x(self.bbox_top) if self.bbox_left < x: if old_bbox_left >= ox: self.move_x(x - self.bbox_left) y = other.get_slope_y(self.bbox_left) self.bbox_top = max(self.bbox_top, y) self.event_collision_top(other) other.event_collision_bottom(self) elif not self.collision(other, y=old_y): self.bbox_top = max(self.bbox_top, other.bbox_bottom) self.event_collision_top(other) other.event_collision_bottom(self) # Engage stickiness (same whether moving left or right) # 1 = sticking to a wall on the right # 2 = sticking to a wall on the left if sticky == 1: if (not self.get_right_touching_slope() and not self.get_right_touching_wall()): new_bbox_right = None for other in sge.game.current_room.objects: if (other.bbox_top >= self.bbox_bottom or other.bbox_bottom <= self.bbox_top): continue if isinstance(other, SolidLeft): x = other.bbox_left elif isinstance(other, SlopeTopLeft): x = other.get_slope_x(self.bbox_bottom) elif isinstance(other, SlopeBottomLeft): x = other.get_slope_x(self.bbox_top) else: continue if x >= self.bbox_right and (new_bbox_right is None or x < new_bbox_right): new_bbox_right = x if new_bbox_right is not None: self.bbox_right = new_bbox_right elif sticky == 2: if (not self.get_left_touching_slope() and not self.get_left_touching_wall()): new_bbox_left = None for other in sge.game.current_room.objects: if (other.bbox_top >= self.bbox_bottom or other.bbox_bottom <= self.bbox_top): continue if isinstance(other, SolidRight): x = other.bbox_right elif isinstance(other, SlopeTopRight): x = other.get_slope_x(self.bbox_bottom) elif isinstance(other, SlopeBottomRight): x = other.get_slope_x(self.bbox_top) else: continue if x <= self.bbox_left and (new_bbox_left is None or x > new_bbox_left): new_bbox_left = x if new_bbox_left is not None: self.bbox_left = new_bbox_left
[docs] def get_left_touching_wall(self): """ Return whether the left side of this object is touching the right side of a :class:`SolidRight` object. """ for tile in self.collision(SolidRight, x=(self.x - 1)): if not self.collision(tile): return True return False
[docs] def get_right_touching_wall(self): """ Return whether the right side of this object is touching the right side of a :class:`SolidLeft` object. """ for tile in self.collision(SolidLeft, x=(self.x + 1)): if not self.collision(tile): return True return False
[docs] def get_top_touching_wall(self): """ Return whether the top side of this object is touching the bottom side of a :class:`SolidBottom` object. """ for tile in self.collision(SolidBottom, y=(self.y - 1)): if not self.collision(tile): return True return False
[docs] def get_bottom_touching_wall(self): """ Return whether the bottom side of this object is touching the top side of a :class:`SolidTop` object. """ for tile in self.collision(SolidTop, y=(self.y + 1)): if not self.collision(tile): return True return False
[docs] def get_left_touching_slope(self): """ Return whether the left side of this object is touching the right side of a :class:`SlopeTopRight` or :class:`SlopeBottomRight` object. """ for slope in self.collision(SlopeTopRight, x=(self.x - 1)): y = slope.get_slope_y(self.bbox_left) if self.bbox_bottom >= y and (not self.collision(slope) or self.bbox_bottom - 1 < y): return True else: for slope in self.collision(SlopeBottomRight, x=(self.x - 1)): y = slope.get_slope_y(self.bbox_left) if self.bbox_top <= y and (not self.collision(slope) or self.bbox_top + 1 > y): return True return False
[docs] def get_right_touching_slope(self): """ Return whether the right side of this object is touching the left side of a :class:`SlopeTopLeft` or :class:`SlopeBottomLeft` object. """ for slope in self.collision(SlopeTopLeft, x=(self.x + 1)): y = slope.get_slope_y(self.bbox_right) if self.bbox_bottom >= y and (not self.collision(slope) or self.bbox_bottom - 1 < y): return True else: for slope in self.collision(SlopeBottomLeft, x=(self.x + 1)): y = slope.get_slope_y(self.bbox_right) if self.bbox_top <= y and (not self.collision(slope) or self.bbox_top + 1 > y): return True return False
[docs] def get_top_touching_slope(self): """ Return whether the top side of this object is touching the bottom side of a :class:`SlopeBottomLeft` or :class:`SlopeBottomRight` object. """ for slope in self.collision(SlopeBottomLeft, y=(self.y - 1)): x = slope.get_slope_x(self.bbox_top) if self.bbox_right >= x and (not self.collision(slope) or self.bbox_right - 1 < x): return True else: for slope in self.collision(SlopeBottomRight, y=(self.y - 1)): x = slope.get_slope_x(self.bbox_top) if self.bbox_left <= x and (not self.collision(slope) or self.bbox_left + 1 > x): return True return False
[docs] def get_bottom_touching_slope(self): """ Return whether the bottom side of this object is touching the top side of a :class:`SlopeTopLeft` or :class:`SlopeTopRight` object. """ for slope in self.collision(SlopeTopLeft, y=(self.y + 1)): x = slope.get_slope_x(self.bbox_bottom) if self.bbox_right >= x and (not self.collision(slope) or self.bbox_right - 1 < x): return True else: for slope in self.collision(SlopeTopRight, y=(self.y + 1)): x = slope.get_slope_x(self.bbox_bottom) if self.bbox_left <= x and (not self.collision(slope) or self.bbox_left + 1 > x): return True return False
def event_update_position(self, delta_mult): xmove = self.xvelocity * delta_mult ymove = self.yvelocity * delta_mult self.move_x(xmove) self.move_y(ymove)
[docs]class SolidLeft(sge.Object): """ Class for walls which stop movement of :class:`Collider` objects from the top. """
[docs]class SolidRight(sge.Object): """ Class for walls which stop movement of :class:`Collider` objects from the right. """
[docs]class SolidTop(sge.Object): """ Class for walls which stop movement of :class:`Collider` objects from the top. """
[docs]class SolidBottom(sge.Object): """ Class for walls which stop movement of :class:`Collider` objects from the bottom. """
[docs]class Solid(SolidLeft, SolidRight, SolidTop, SolidBottom): """ Inherits :class:`SolidLeft`, :class:`SolidRight`, :class:`SolidTop`, and :class:`SolidBottom`. Meant to be a convenient parent class for walls that should stop movement in all directions. """
[docs]class SlopeTopLeft(sge.Object): """ A parent class for slopes which point in some direction upwards and to the left. Slopes of this type go from the bottom-left corner to the top-right corner of the bounding box. .. attribute:: xsticky If set to :const:`True`, a collider that moves to the left while touching the top side of the slope will attempt to keep touching the top side of the slope by moving downward. .. attribute:: ysticky If set to :const:`True`, a collider that moves upward while touching the left side of the slope will attempt to keep touching the left side of the slope by moving to the right. """ xsticky = False ysticky = False def get_slope_x(self, y): """ Get the corresponding y coordinate of a given x coordinate for the slope. """ # x = (y - b) / m [b is 0] m = -self.bbox_height / self.bbox_width y -= self.bbox_top return y / m + self.bbox_right def get_slope_y(self, x): """ Get the corresponding x coordinate of a given y coordinate for the slope. """ # y = mx + b [b is 0] m = -self.bbox_height / self.bbox_width x -= self.bbox_left return m * x + self.bbox_bottom
[docs]class SlopeTopRight(sge.Object): """ A parent class for slopes which point in some direction upwards and to the right. Slopes of this type go from the top-left corner to the bottom-right corner of the bounding box. .. attribute:: xsticky If set to :const:`True`, a collider that moves to the right while touching the top side of the slope will attempt to keep touching the top side of the slope by moving downward. .. attribute:: ysticky If set to :const:`True`, a collider that moves upward while touching the right side of the slope will attempt to keep touching the right side of the slope by moving to the left. """ xsticky = False ysticky = False def get_slope_x(self, y): """ Get the corresponding y coordinate of a given x coordinate for the slope. """ # x = (y - b) / m [b is 0] m = self.bbox_height / self.bbox_width y -= self.bbox_top return y / m + self.bbox_left def get_slope_y(self, x): """ Get the corresponding x coordinate of a given y coordinate for the slope. """ # y = mx + b [b is 0] m = self.bbox_height / self.bbox_width x -= self.bbox_left return m * x + self.bbox_top
[docs]class SlopeBottomLeft(sge.Object): """ A parent class for slopes which point in some direction upwards and to the left. Slopes of this type go from the top-left corner to the bottom-right corner of the bounding box. .. attribute:: xsticky If set to :const:`True`, a collider that moves to the left while touching the bottom side of the slope will attempt to keep touching the bottom side of the slope by moving upward. .. attribute:: ysticky If set to :const:`True`, a collider that moves downward while touching the left side of the slope will attempt to keep touching the left side of the slope by moving to the right. """ xsticky = False ysticky = False def get_slope_x(self, y): """ Get the corresponding y coordinate of a given x coordinate for the slope. """ # x = (y - b) / m [b is 0] m = self.bbox_height / self.bbox_width y -= self.bbox_top return y / m + self.bbox_left def get_slope_y(self, x): """ Get the corresponding x coordinate of a given y coordinate for the slope. """ # y = mx + b [b is 0] m = self.bbox_height / self.bbox_width x -= self.bbox_left return m * x + self.bbox_top
[docs]class SlopeBottomRight(sge.Object): """ A parent class for slopes which point in some direction upwards and to the right. Slopes of this type go from the bottom-left corner to the top-right corner of the bounding box. .. attribute:: xsticky If set to :const:`True`, a collider that moves to the right while touching the bottom side of the slope will attempt to keep touching the bottom side of the slope by moving upward. .. attribute:: ysticky If set to :const:`True`, a collider that moves downward while touching the left side of the slope will attempt to keep touching the left side of the slope by moving to the right. """ xsticky = False ysticky = False def get_slope_x(self, y): """ Get the corresponding x coordinate of a given y coordinate for the slope. """ # x = (y - b) / m [b is 0] m = -self.bbox_height / self.bbox_width y -= self.bbox_top return y / m + self.bbox_right def get_slope_y(self, x): """ Get the corresponding y coordinate of a given x coordinate for the slope. """ # y = mx + b [b is 0] m = -self.bbox_height / self.bbox_width x -= self.bbox_left return m * x + self.bbox_bottom