engine/physics/physics_2d.py
from engine.util.math import divide_toward_zero
from engine import geometry
class Physics2d(object):
"""Simple two dimensional physics simulation.
Attributes:
mass (int):
Mass of the object in arbitrary units.
velocity (:obj:`engine.geometry.Point2d`):
Velocity along the x and y axes in units per second.
acceleration (:obj:`engine.geometry.Point2d`):
Acceleration along the x and y axes in units per second.
friction (int): Coefficient of friction between 1 and 100 when no
acceleration is applied, to slow the object to rest.
"""
def __init__(self, mass=1, friction=100, gravity=(0, -10),
terminal_velocity=(100, 100)):
"""Creates a new two dimensional physics simulation.
Args:
mass (int, optional): Mass of the object in units. Defaults to 100.
friction (int): Coefficient of friction between 1 and 100 when no
acceleration is applied, to slow it to rest. Defaults to 100.
gravity (tuple of int, optional): Gravitational pull on the object
in units per second. Defaults to (0, 10).
terminal_velocity (tuple of int, optional): Terminal velocity along
the x and y axes in units per second. Defaults to (100, 100).
"""
super(Physics2d, self).__init__()
self.velocity = geometry.Point2d(0, 0)
self.acceleration = geometry.Point2d(0, 0)
self.friction = max(min(100, friction), 1) # Clamp between 1 and 100
self._terminal_velocity = geometry.Point2d(*terminal_velocity)
self._gravity = geometry.Point2d(*gravity)
self.mass = mass
# Higher resolution copies for simulation
self._velocity_1000 = self.velocity * 1000
def run_simulation(self, ms):
"""Adjusts velocities based on simulation results after the given time.
This method call is instantaneous, it does not run for the given time.
Args:
ms (int): The number of milliseconds to run the simulation for.
"""
# Calculate the total acceleration as force applied plus gravity
total_acceleration = (self.acceleration + self._gravity)
for axis in ('x', 'y'):
total_axis_acceleration = getattr(total_acceleration, axis)
self._update_acceleration_on_axis(total_axis_acceleration, axis)
# Update velocity based on acceleration, mass, and time
self._velocity_1000 += (total_acceleration * self.mass * ms)
for axis in ('x', 'y'):
self._update_velocity_on_axis(axis)
def _update_acceleration_on_axis(self, total_acceleration, axis):
"""Updates acceleration on the given axis.
Args:
total_acceleration (int):
Total acceleration on the axis, as force applied plus gravity.
axis (str):
Axis to update acceleration for, either 'x' or 'y'.
"""
friction = 100 - self.friction
velocity = getattr(self.velocity, axis)
velocity_1000 = getattr(self._velocity_1000, axis)
# Ensure high resolution velocity matches the current velocity
if divide_toward_zero(velocity_1000, 1000) != velocity:
setattr(self._velocity_1000, axis, velocity * 1000)
# Apply friction to decelerate when no acceleration is present
if total_acceleration == 0:
setattr(self._velocity_1000, axis,
divide_toward_zero(velocity_1000 * friction, 100))
def _update_velocity_on_axis(self, axis):
"""Updates velocity on the given axis by applying acceleration.
Args:
axis (str):
Axis to update acceleration for, either 'x' or 'y'.
"""
velocity_1000 = getattr(self._velocity_1000, axis)
# Round off the integer velocity towards 0
setattr(self.velocity, axis,
divide_toward_zero(velocity_1000, 1000))
velocity = getattr(self.velocity, axis)
terminal_velocity = getattr(self._terminal_velocity, axis)
# Apply terminal velocity
if abs(velocity) > terminal_velocity:
if velocity < 0:
terminal_velocity = -terminal_velocity
setattr(self.velocity, axis, terminal_velocity)