"""
Core classes of the PyUnity
physics engine.
"""
from ..vector3 import *
from ..core import *
import math
infinity = math.inf
"""A representation of infinity"""
[docs]class PhysicMaterial:
"""
Class to store data on a collider's material.
Parameters
----------
restitution : float
Bounciness of the material
friction : float
Friction of the material
"""
def __init__(self, restitution = 0.75, friction = 1):
self.restitution = restitution
self.friction = friction
[docs]class Manifold:
"""
Class to store collision data.
Parameters
----------
a : Collider
The first collider
b : Collider
The second collider
normal : Vector3
The collision normal
penetration : float
How much the two colliders overlap
"""
def __init__(self, a, b, normal, penetration):
self.a = a
self.b = b
self.normal = normal
self.penetration = penetration
[docs]class Collider(Component):
"""
Collider base class. The default
mass is 100.
"""
def __init__(self):
super(Collider, self).__init__()
self.mass = 100
self.velocity = Vector3.zero()
self.physicMaterial = PhysicMaterial()
[docs]class SphereCollider(Collider):
"""
A spherical collider that cannot be
deformed.
"""
def __init__(self):
super(SphereCollider, self).__init__()
[docs] def SetSize(self, radius, offset):
"""
Sets the size of the collider.
Parameters
----------
radius : float
The radius of the collider.
offset : Vector3
Offset of the collider.
"""
self.radius = radius
self.pos = offset + self.transform.position
self.min = pos - radius
self.max = pos + radius
[docs] def Move(self, dt):
"""
Moves the whole collider by its velocity times
the delta time.
Parameters
----------
dt : float
Delta time to move the velocity by
"""
self.min += dt * self.velocity
self.max += dt * self.velocity
self.pos += dt * self.velocity
[docs] def collidingWith(self, other):
"""
Check to see if the collider is
colliding with another collider.
Parameters
----------
other : Collider
Other collider to check against
Returns
-------
Manifold or None
Collision data
Notes
-----
To check against another SphereCollider, the
distance and the sum of the radii is checked.
To check against an AABBoxColider, the check
is as follows:
1. The sphere's center is checked to see if it
is inside the AABB.
#. If it is, then the two are colliding.
#. If it isn't, then a copy of the position is
clamped to the AABB's bounds.
#. Finally, the distance between the clamped
position and the original position is
measured.
#. If the distance is bigger than the sphere's
radius, then the two are colliding.
#. If not, then they aren't colliding.
"""
if isinstance(other, SphereCollider):
objDistSqrd = abs(self.pos - other.pos).get_length_sqrd()
radDistSqrd = (self.radius + other.radius) ** 2
normal = self.pos - other.pos
penetration = radDistSqrd - objDistSqrd
return Manifold(self, other, normal, penetration) if objDistSqrd <= radDistSqrd else None
elif isinstance(other, AABBoxCollider):
inside = (other.min.x < self.pos.x < other.max.x and
other.min.y < self.pos.y < other.max.y and
other.min.z < self.pos.z < other.max.z)
pos = self.pos.copy()
if inside:
pos.x = other.min.x if pos.x - other.min.x < other.max.x - pos.x else other.max.x
pos.y = other.min.y if pos.y - other.min.y < other.max.y - pos.y else other.max.y
pos.z = other.min.z if pos.z - other.min.z < other.max.z - pos.z else other.max.z
else:
pos.clamp(other.min, other.max)
dist = (self.pos - pos).get_length_sqrd()
if not inside and dist > self.radius ** 2:
return None
return Manifold(self, other, self.pos - other.pos, self.radius - dist)
[docs]class AABBoxCollider(Collider):
"""
An axis-aligned box collider that
cannot be deformed.
"""
def __init__(self):
super(AABBoxCollider, self).__init__()
[docs] def SetSize(self, min, max):
"""
Sets the size of the collider.
Parameters
----------
min : Vector3
The corner with the lowest coordinates.
max : Vector3
The corner with the highest coordinates.
"""
self.min = min
self.max = max
self.pos = Vector3((min.x + max.x) / 2, (min.y + max.y) / 2, (min.z + max.z) / 2)
[docs] def Move(self, dt):
"""
Moves the whole collider by its velocity times
the delta time.
Parameters
----------
dt : float
Delta time to move the velocity by
"""
self.min += dt * self.velocity
self.max += dt * self.velocity
self.pos += dt * self.velocity
[docs] def collidingWith(self, other):
"""
Check to see if the collider is
colliding with another collider.
Parameters
----------
other : Collider
Other collider to check against
Returns
-------
Manifold or None
Collision data
Notes
-----
To check against another AABBoxCollider, the
corners are checked to see if they are inside
the other collider.
To check against a SphereCollider, the check
is as follows:
1. The sphere's center is checked to see if it
is inside the AABB.
#. If it is, then the two are colliding.
#. If it isn't, then a copy of the position is
clamped to the AABB's bounds.
#. Finally, the distance between the clamped
position and the original position is
measured.
#. If the distance is bigger than the sphere's
radius, then the two are colliding.
#. If not, then they aren't colliding.
"""
if isinstance(other, AABBoxCollider):
n = other.pos - self.pos
a_extent = (self.max.x - self.min.x) / 2
b_extent = (other.max.x - other.min.x) / 2
x_overlap = a_extent + b_extent - abs(n.x)
if x_overlap > 0:
a_extent = (self.max.y - self.min.y) / 2
b_extent = (other.max.y - other.min.y) / 2
y_overlap = a_extent + b_extent - abs(n.y)
if y_overlap > 0:
a_extent = (self.max.z - self.min.z) / 2
b_extent = (other.max.z - other.min.z) / 2
z_overlap = a_extent + b_extent - abs(n.z)
if z_overlap > 0:
if x_overlap < y_overlap and x_overlap < z_overlap:
if n.x < 0: normal = Vector3.left()
else: normal = Vector3.right()
penetration = x_overlap
elif y_overlap < x_overlap and y_overlap < z_overlap:
if n.y < 0: normal = Vector3.down()
else: normal = Vector3.up()
penetration = y_overlap
else:
if n.z < 0: normal = Vector3.back()
else: normal = Vector3.forward()
penetration = z_overlap
return Manifold(self, other, normal, penetration)
# if self.min.x > other.max.x or self.max.x < other.min.x: collided = False
# elif self.min.y > other.max.y or self.max.y < other.min.y: collided = False
# elif self.min.z > other.max.z or self.max.z < other.min.z: collided = False
# else: collided = True
elif isinstance(other, SphereCollider):
inside = (self.min.x < other.pos.x < self.max.x and
self.min.y < other.pos.y < self.max.y and
self.min.z < other.pos.z < self.max.z)
pos = other.pos.copy()
if inside:
pos.x = self.min.x if pos.x - self.min.x < self.max.x - pos.x else self.max.x
pos.y = self.min.y if pos.y - self.min.y < self.max.y - pos.y else self.max.y
pos.z = self.min.z if pos.z - self.min.z < self.max.z - pos.z else self.max.z
else:
pos.clamp(self.min, self.max)
dist = (other.pos - pos).get_length_sqrd()
if not inside and dist > other.radius ** 2:
return None
return Manifold(self, other, self.pos - other.pos, other.radius - dist)
[docs]class CollManager:
"""
Manage the collisions between all colliders.
"""
def __init__(self):
self.colliders = []
[docs] def AddColliders(self, scene):
"""
Get all colliders from a specified scene.
This overwrites the collider list, and so
can be called whenever a new collider is
added or removed.
"""
self.colliders = []
for gameObject in scene.gameObjects:
for component in gameObject.components:
if isinstance(component, Collider):
self.colliders.append(component)
[docs] def CheckCollisions(self):
"""
Goes through every pair exactly once,
then checks their collisions and
resolves them.
"""
for i in range(0, len(self.colliders) - 1):
for j in range(i + 1, len(self.colliders)):
a = self.colliders[i]
b = self.colliders[j]
m = a.collidingWith(b) or b.collidingWith(a)
if m:
e = min(m.a.physicMaterial.restitution, m.b.physicMaterial.restitution)
normal = m.normal.copy()
if math.isinf(m.a.mass): a = 0
elif math.isinf(m.b.mass): a = 2
else: a =2 * m.b.mass / (m.a.mass + m.b.mass)
b = (m.a.velocity - m.b.velocity).dot(normal) / normal.dot(normal)
velA = a * b * normal * (0.5 + e / 2)
normal *= -1
if math.isinf(m.a.mass): a = 2
elif math.isinf(m.b.mass): a = 0
else: a = 2 * m.a.mass / (m.a.mass + m.b.mass)
b = (m.b.velocity - m.a.velocity).dot(normal) / normal.dot(normal)
velB = a * b * normal * (0.5 + e / 2)
m.a.velocity -= velA
m.b.velocity -= velB
rv = m.b.velocity - m.a.velocity
t = (rv - rv.dot(m.normal) * m.normal).normalized()
jt = rv.dot(t)
jt /= 1 / m.a.mass + 1 / m.b.mass
mu = (m.a.physicMaterial.friction + m.b.physicMaterial.friction) / 2
if abs(jt) < j * mu:
frictionImpulse = jt * t
else:
frictionImpulse = -j * t * mu
m.a.velocity -= 1 / m.a.mass * frictionImpulse
m.b.velocity += 1 / m.b.mass * frictionImpulse
correction = m.penetration * (m.a.mass + m.b.mass) * 0.8 * m.normal
m.a.pos -= 1 / m.a.mass * correction if not math.isinf(m.a.mass + m.b.mass) else 0
m.b.pos += 1 / m.b.mass * correction if not math.isinf(m.a.mass + m.b.mass) else 0
[docs] def Step(self, dt):
"""
Steps through the simulation at a
given delta time.
Parameters
----------
dt : float
Delta time to step
Notes
-----
The simulation is stepped 10 times,
so that it is more precise.
"""
for i in range(10):
for collider in self.colliders:
collider.Move(dt / 10)
self.CheckCollisions()
for collider in self.colliders:
collider.transform.position = collider.pos