"""
The Rigidbody component contains an implementation of rigidbody physics. They
have hitboxes and can collide and interact with other rigidbodies.
"""
from __future__ import annotations
import math
from . import Component, Circle, Hitbox, Rectangle, Polygon
from ... import Vector, Time, Math
[docs]class RigidBody(Component):
"""
A RigidBody implementation with built in physics and collisions.
Rigidbodies require hitboxes.
Args:
offset: The offset of the rigidbody from the gameobject. Defaults to Vector(0, 0).
rot_offset: The offset of the rigidbody's rotation from the gameobject. Defaults to 0.
density: The density of the rigidbody. Defaults to 1.
bounciness: The bounciness of the rigidbody. Defaults to 0.
gravity: The gravity of the rigidbody. Defaults to Vector(0, 0).
max_speed: The maximum speed of the rigidbody. Defaults to Vector(INF, INF).
velocity: The velocity of the rigidbody. Defaults to Vector(0, 0).
ang_vel: The angular velocity of the rigidbody. Defaults to 0.
friction: The friction of the rigidbody. Defaults to 0.
static: Whether the rigidbody is static. Defaults to False.
pos_correction: The positional correction of the rigidbody. Defaults to 0.25.
moment: The moment of inertia of the rigidbody. Defaults to -1.
mass: The mass of the rigidbody. Defaults to -1.
advanced: Whether the rigidbody uses advanced physics. Defaults to False.
Attributes:
static (bool): Whether or not the rigidbody is static (as in, it does
not move).
gravity (Vector): The acceleration of the gravity that should be
applied.
friction (float): The friction coefficient of the Rigidbody (usually a
a value between 0 and 1).
max_speed (Vector): The maximum speed of the Rigidbody.
min_speed (Vector): The minimum speed of the Rigidbody.
velocity (Vector): The current velocity of the Rigidbody.
ang_vel (float): The current angular velocity of the Rigidbody.
bounciness (float): How bouncy the rigidbody is (usually a value
between 0 and 1).
advanced (bool): Whether to use rotational collision resolution
(not desired in basic platformers, for instance).
"""
def __init__(
self,
offset: Vector = Vector(),
rot_offset: float = 0,
density: float = 1,
bounciness: float = 0,
gravity: Vector = Vector(),
max_speed: Vector = Vector(Math.INF, Math.INF),
velocity: Vector = Vector(),
ang_vel: float = 0,
friction: float = 0,
static: bool = False,
pos_correction: float = 0.25,
moment: float = -1,
mass: float = -1,
advanced: bool = False,
):
super().__init__(offset=offset, rot_offset=rot_offset)
self.static: bool = static
self.gravity: Vector = gravity
self.friction: float = friction
self.max_speed: Vector = max_speed
self.pos_correction: float = pos_correction
self.advanced: bool = advanced
self.velocity: Vector = velocity
self.ang_vel: float = ang_vel
self.singular = True
if mass != -1 and moment != -1:
if mass == 0 or self.static:
self._inv_mass = 0
else:
self._inv_mass: float = 1 / mass
if moment == 0 or self.static:
self._inv_moment = 0
else:
self._inv_moment: float = 1 / moment
self._density = -1
else:
self._density = density
self._inv_mass = 0
self._inv_moment = 0
self._last_density_calc = -1
self.bounciness: float = bounciness
@property
def inv_mass(self) -> float:
"""The inverse mass of the Rigidbody."""
self.calc_mass_and_moment()
return self._inv_mass
@inv_mass.setter
def inv_mass(self, new: float):
self._inv_mass = new
@property
def mass(self) -> float:
"""The mass of the Rigidbody."""
self.calc_mass_and_moment()
if self.inv_mass == 0:
return 0
else:
return 1 / self.inv_mass
@mass.setter
def mass(self, new: float):
if new == 0:
self.inv_mass = 0
else:
self.inv_mass: float = 1 / new
@property
def inv_moment(self) -> float:
"""The inverse moment of the Rigidbody."""
self.calc_mass_and_moment()
return self._inv_moment
@inv_moment.setter
def inv_moment(self, new: float):
self._inv_moment = new
@property
def moment(self) -> float:
"""The moment of inertia of the Rigidbody."""
self.calc_mass_and_moment()
if self.inv_moment == 0:
return 0
else:
return 1 / self.inv_moment
@moment.setter
def moment(self, new: float):
if new == 0:
self.inv_moment = 0
else:
self.inv_moment: float = 1 / new
[docs] def calc_mass_and_moment(self):
"""
Calculates the mass and the moment of interia from the density if it hasn't already been calculate this frame.
"""
if self._density > -1 and self._last_density_calc < Time.frames:
self._last_density_calc = Time.frames
hitboxes = self.gameobj.get_all(Hitbox)
tots_mass = 0
tots_moment = 0
for hitbox in hitboxes:
if hitbox.trigger:
continue
if isinstance(hitbox, Circle):
mass = self._density * hitbox.radius**2 * math.pi
moment = mass * hitbox.radius**2
elif isinstance(hitbox, Rectangle):
mass = self._density * hitbox.width * hitbox.height
moment = mass * (hitbox.width**2 + hitbox.height**2) / 12
elif isinstance(hitbox, Polygon):
c = Vector(0, 0) # centroid
area = 0
inert = 0
k_inv3 = 1 / 3
verts = hitbox.translated_verts()
for i in range(len(verts)):
p1 = verts[i]
p2 = verts[(i + 1) % len(verts)]
d = p1.cross(p2)
tri_area = 0.5 * d
area += tri_area
# Use area to weight the centroid average, not just vertex position
weight = tri_area * k_inv3
c += (p1 + p2) * weight
intx2 = p1.x**2 + p2.x * p1.x + p2.x**2
inty2 = p1.y**2 + p2.y * p1.y + p2.y**2
inert += (0.25 * k_inv3 * d) * (intx2 + inty2)
mass = self._density * area
moment = inert * self._density
tots_mass += mass
tots_moment += moment
self._inv_mass = 0 if tots_mass == 0 else 1 / tots_mass
self._inv_moment = 0 if tots_moment == 0 else 1 / tots_moment
[docs] def physics(self):
"""Applies general kinematic laws to the rigidbody."""
self.velocity += self.gravity * Time.fixed_delta
self.velocity.clamp(-self.max_speed, self.max_speed)
self.gameobj.pos += self.velocity * Time.fixed_delta
self.gameobj.rotation += self.ang_vel * Time.fixed_delta
[docs] def add_force(self, force: Vector):
"""
Applies a force to the Rigidbody.
Args:
force (Vector): The force to add.
"""
accel = force * self.inv_mass
self.velocity += accel * Time.fixed_delta
[docs] def add_impulse(self, impulse: Vector):
"""
Applies an impulse to the rigidbody.
Args:
impulse (Vector): _description_
"""
self.velocity += impulse * Time.fixed_delta
[docs] def add_cont_force(self, force: Vector, time: int):
"""
Add a continuous force to the Rigidbody.
A continuous force is a force that is continuously applied over a time period.
(the force is added every frame for a specific duration).
Args:
force (Vector): The force to add.
time (int): The time in seconds that the force should be added.
"""
if time <= 0:
return
else:
self.add_force(force)
Time.delayed_frames(1, lambda: self.add_cont_force(force, time - (1000 * Time.delta_time)))
[docs] def add_cont_impulse(self, impulse: Vector, time: int):
"""
Add a continuous impulse to the Rigidbody.
A continuous impulse is a impulse that is continuously applied over a time period.
(the impulse is added every frame for a specific duration).
Args:
impulse (Vector): The impulse to add.
time (int): The time in seconds that the impulse should be added.
"""
if time <= 0:
return
else:
self.add_impulse(impulse)
Time.delayed_frames(1, lambda: self.add_cont_impulse(impulse, time - (1000 * Time.delta_time)))
[docs] def setup(self):
self.calc_mass_and_moment()
[docs] def fixed_update(self):
"""The physics loop for the rigidbody component."""
if not self.static:
self.physics()
[docs] def clone(self) -> RigidBody:
return RigidBody(
{
"static": self.static,
"gravity": self.gravity,
"friction": self.friction,
"max_speed": self.max_speed,
"pos_correction": self.pos_correction,
"velocity": self.velocity,
"mass": self.mass,
"bounciness": self.bounciness,
}
)