"""
The Rigidbody component contains an implementation of rigidbody physics. They
have hitboxes and can collide and interact with other rigidbodies.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from . import Component
from ... import Vector, Defaults, Time
if TYPE_CHECKING:
from . import ColInfo
[docs]class RigidBody(Component):
"""
A RigidBody implementation with built in physics and collisions.
Rigidbodies require hitboxes.
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.
inv_mass (float): The inverse of the mass of the Rigidbody (0 if the
mass is infinite).
bounciness (float): How bouncy the rigidbody is (usually a value
between 0 and 1).
"""
[docs] def __init__(self, options: dict = {}):
"""
Initializes a Rigidbody.
Args:
options: A rigidbody config. Defaults to the :ref:`Rigidbody defaults <rigidbodydef>`.
"""
params = Defaults.rigidbody_defaults | options
super().__init__()
self.static: bool = params["static"]
self.gravity: Vector = params["gravity"]
self.friction: float = params["friction"]
self.max_speed: Vector = params["max_speed"]
self.pos_correction: float = params["pos_correction"]
self.velocity: Vector = params["velocity"]
self.singular = True
if params["mass"] == 0 or self.static:
self.inv_mass = 0
else:
self.inv_mass: float = 1 / params["mass"]
self.bounciness: float = params["bounciness"]
@property
def mass(self) -> float:
"""The mass of the Rigidbody (readonly)"""
if self.inv_mass == 0:
return 0
else:
return 1 / self.inv_mass
[docs] def physics(self):
"""Applies general kinematic laws to the rigidbody."""
self.add_force(self.gravity * self.mass)
self.velocity.clamp(-self.max_speed, self.max_speed)
self.gameobj.pos += self.velocity * Time.milli_to_sec(Time.fixed_delta)
[docs] def add_force(self, force: Vector):
"""
Add a force to the Rigidbody.
Args:
force (Vector): The force to add.
"""
accel = force * self.inv_mass
self.velocity += accel * Time.milli_to_sec(Time.fixed_delta)
[docs] def add_cont_force(self, impulse: 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:
impulse (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(impulse)
Time.delayed_frames(1, lambda: self.add_impulse(impulse, time - Time.delta_time))
[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,
"rotation": self.rotation,
}
)
[docs] @staticmethod
def handle_collision(col: ColInfo):
"""
Resolve the collision between two rigidbodies.
Utilizes a simplistic impulse resolution method.
Args:
col: The collision information.
"""
# Get the rigidbody components
rb_a: RigidBody = col.shape_b.gameobj.get(RigidBody)
rb_b: RigidBody = col.shape_a.gameobj.get(RigidBody)
rb_a_none = rb_a is None
rb_b_none = rb_b is None
if rb_a_none and rb_b_none:
return
# Find inverse masses
inv_mass_a: float = 0 if rb_a_none else rb_a.inv_mass
inv_mass_b: float = 0 if rb_b_none else rb_b.inv_mass
# Handle infinite mass cases
if inv_mass_a == inv_mass_b == 0:
if rb_a_none:
inv_mass_b = 1
elif rb_b_none:
inv_mass_a = 1
else:
inv_mass_a, inv_mass_b = 1, 1
inv_sys_mass = 1 / (inv_mass_a + inv_mass_b)
# Find collision separation normal
collision_norm = col.sep.unit()
# Position correction
correction = max(col.sep.magnitude - 0.01, 0) * inv_sys_mass * collision_norm
# Impulse Resolution
# Relative velocity
rv = (0 if rb_b_none else rb_b.velocity) - (0 if rb_a_none else rb_a.velocity)
if (vel_along_norm := rv.dot(collision_norm)) > 0:
return
# Calculate restitution
e = max(0 if rb_a_none else rb_a.bounciness, 0 if rb_b_none else rb_b.bounciness)
# Calculate impulse scalar
j = -(1 + e) * vel_along_norm * inv_sys_mass
# Apply the impulse
impulse = j * collision_norm
if not (rb_a_none or rb_a.static):
rb_a.gameobj.pos -= inv_mass_a * correction * rb_a.pos_correction
rb_a.velocity -= inv_mass_a * impulse
if not (rb_b_none or rb_b.static):
rb_b.gameobj.pos += inv_mass_b * correction * rb_b.pos_correction
rb_b.velocity += inv_mass_b * impulse
# Friction
# Calculate friction coefficient
if rb_a_none:
mu = rb_b.friction * rb_b.friction
elif rb_b_none:
mu = rb_a.friction * rb_a.friction
else:
mu = min(rb_a.friction * rb_a.friction, rb_b.friction * rb_b.friction)
# Stop redundant friction calculations
if mu == 0:
return
# Tangent vector
tangent = rv - rv.dot(collision_norm) * collision_norm
tangent.magnitude = 1
# Solve for magnitude to apply along the friction vector
jt = -rv.dot(tangent) * inv_sys_mass
# Calculate friction impulse
if abs(jt) < j * mu:
friction_impulse = jt * tangent # "Static friction"
else:
friction_impulse = -j * tangent * mu # "Dynamic friction"
if not (rb_a_none or rb_a.static):
rb_a.velocity -= inv_mass_a * friction_impulse
if not (rb_b_none or rb_b.static):
rb_b.velocity += inv_mass_b * friction_impulse