Source code for pyunity.physics.core

"""
Core classes of the PyUnity
physics engine.
"""

__all__ = ["PhysicMaterial", "Collider", "SphereCollider", "Manifold",
           "AABBoxCollider", "Rigidbody", "CollManager", "Infinity"]

from ..errors import PyUnityException
from ..values import *
from ..core import *
from . import config
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 Attributes ---------- restitution : float Bounciness of the material friction : float Friction of the material combine : int Combining function. -1 means minimum, 0 means average, and 1 means maximum """
[docs] def exception(self, *args, **kwargs): raise PyUnityException( "Cannot modify properties of PhysicMaterial: it is immutable")
def __init__(self, restitution=0.75, friction=1, immutable=False): self.restitution = restitution self.friction = friction self.combine = -1 if immutable: self.__setattr__ = self.exception
[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, metaclass=ABCMeta): """Collider base class.""" offset = ShowInInspector(Vector3) def __init__(self, transform): super(Collider, self).__init__(transform) self.offset = Vector3.zero() @property def pos(self): return self.transform.position + self.offset @pos.setter def pos(self, value): self.transform.position = value - self.offset
[docs] @abstractmethod def collidingWith(self, other): pass
[docs]class SphereCollider(Collider): """ A spherical collider that cannot be deformed. Attributes ---------- radius : Vector3 The radius of the SphereCollider """ radius = ShowInInspector(float, 0) def __init__(self, transform): super(SphereCollider, self).__init__(transform) self.radius = self.transform.scale.length
[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): otherMin = other.pos - other.size / 2 otherMax = other.pos + other.size / 2 inside = (otherMin.x < self.pos.x < otherMax.x and otherMin.y < self.pos.y < otherMax.y and otherMin.z < self.pos.z < otherMax.z) pos = self.pos.copy() if inside: pos.x = otherMin.x if pos.x - otherMin.x < otherMax.x - pos.x else otherMax.x pos.y = otherMin.y if pos.y - otherMin.y < otherMax.y - pos.y else otherMax.y pos.z = otherMin.z if pos.z - otherMin.z < otherMax.z - pos.z else otherMax.z else: pos.clamp(otherMin, otherMax) 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 - math.sqrt(dist))
[docs]class AABBoxCollider(Collider): """ An axis-aligned box collider that cannot be deformed. Attributes ---------- size : Vector3 The size of the AABBoxCollider. """ size = ShowInInspector(Vector3) def __init__(self, transform): super(AABBoxCollider, self).__init__(transform) self.size = self.transform.scale * 2
[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. """ selfMin = self.pos - self.size / 2 selfMax = self.pos + self.size / 2 if isinstance(other, AABBoxCollider): otherMin = other.pos - other.size / 2 otherMax = other.pos + other.size / 2 n = other.pos - self.pos a_extent = (selfMax.x - selfMin.x) / 2 b_extent = (otherMax.x - otherMin.x) / 2 x_overlap = a_extent + b_extent - abs(n.x) if x_overlap > 0: a_extent = (selfMax.y - selfMin.y) / 2 b_extent = (otherMax.y - otherMin.y) / 2 y_overlap = a_extent + b_extent - abs(n.y) if y_overlap > 0: a_extent = (selfMax.z - selfMin.z) / 2 b_extent = (otherMax.z - otherMin.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) elif isinstance(other, SphereCollider): inside = (selfMin.x < other.pos.x < selfMax.x and selfMin.y < other.pos.y < selfMax.y and selfMin.z < other.pos.z < selfMax.z) pos = other.pos.copy() if inside: pos.x = selfMin.x if pos.x - selfMin.x < selfMax.x - pos.x else selfMax.x pos.y = selfMin.y if pos.y - selfMin.y < selfMax.y - pos.y else selfMax.y pos.z = selfMin.z if pos.z - selfMin.z < selfMax.z - pos.z else selfMax.z else: pos.clamp(selfMin, 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 Rigidbody(Component): """ Class to let a GameObject follow physics rules. Attributes ---------- mass : int or float Mass of the Rigidbody. Defaults to 100 velocity : Vector3 Velocity of the Rigidbody physicMaterial : PhysicMaterial Physics material of the Rigidbody """ mass = ShowInInspector(float, 100) velocity = ShowInInspector(Vector3) physicMaterial = ShowInInspector( PhysicMaterial, PhysicMaterial(immutable=True)) force = ShowInInspector(Vector3) gravity = ShowInInspector(bool, True) def __init__(self, transform, dummy=False): super(Rigidbody, self).__init__(transform, dummy) self.mass = 100 self.position = Vector3.zero() self.velocity = Vector3.zero() self.force = Vector3.zero()
[docs] def Move(self, dt): """ Moves all colliders on the GameObject by the Rigidbody's velocity times the delta time. Parameters ---------- dt : float Time to simulate movement by """ if self.gravity: self.force += config.gravity self.velocity += self.force * (1 / self.mass) # self.velocity *= 0.999 self.transform.position += self.velocity * dt self.force = Vector3.zero()
[docs] def AddForce(self, force): """ Apply a force to the center of the Rigidbody. Parameters ---------- force : Vector3 Force to apply Notes ----- A force is a gradual change in velocity, whereas an impulse is just a jump in velocity. """ self.force += force
[docs] def AddImpulse(self, impulse): """ Apply an impulse to the center of the Rigidbody. Parameters ---------- impulse : Vector3 Impulse to apply Notes ----- A force is a gradual change in velocity, whereas an impulse is just a jump in velocity. """ self.velocity += impulse
[docs]class CollManager: """ Manages the collisions between all colliders. Attributes ---------- rigidbodies : dict Dictionary of rigidbodies andthe colliders on the gameObject that the Rigidbody belongs to dummyRigidbody : Rigidbody A dummy rigidbody used when a GameObject has colliders but no rigidbody. It has infinite mass """ def __init__(self): self.rigidbodies = {} self.dummyRigidbody = Rigidbody(None, True) self.dummyRigidbody.mass = Infinity self.steps = 10
[docs] def AddPhysicsInfo(self, scene): """ Get all colliders and rigidbodies from a specified scene. This overwrites the collider and rigidbody lists, and so can be called whenever a new collider or rigidbody is added or removed. Parameters ---------- scene : Scene Scene to search for physics info Notes ----- This function will overwrite the pre-existing dictionary of rigidbodies. When there are colliders but no rigidbody is on the GameObject, then they are placed in the dictionary with a dummy Rigidbody that has infinite mass and a default physic material. Thus, they cannot move. """ self.rigidbodies = {} dummies = [] for gameObject in scene.gameObjects: colliders = gameObject.GetComponents(Collider) if colliders != []: rb = gameObject.GetComponent(Rigidbody) if rb is None: dummies += colliders continue else: rb.position = rb.transform.position self.rigidbodies[rb] = colliders self.rigidbodies[self.dummyRigidbody] = dummies
[docs] def GetRestitution(self, a, b): """ Get the restitution needed for two rigidbodies, based on their combine function Parameters ---------- a : Rigidbody Rigidbody 1 b : Rigidbody Rigidbody 2 Returns ------- float Restitution """ if a.physicMaterial.combine + b.physicMaterial.combine < 0: return min(a.physicMaterial.restitution, b.physicMaterial.restitution) elif a.physicMaterial.combine + b.physicMaterial.combine > 0: return max(a.physicMaterial.restitution, b.physicMaterial.restitution) else: return (a.physicMaterial.restitution + b.physicMaterial.restitution) / 2
[docs] def CheckCollisions(self): """ Goes through every pair exactly once, then checks their collisions and resolves them. """ for x, rbA in zip(range(0, len(self.rigidbodies) - 1), list(self.rigidbodies.keys())[:-1]): for y, rbB in zip(range(x + 1, len(self.rigidbodies)), list(self.rigidbodies.keys())[x + 1:]): for colliderA in self.rigidbodies[rbA]: for colliderB in self.rigidbodies[rbB]: m = colliderA.collidingWith(colliderB) or \ colliderB.collidingWith(colliderA) if m: self.ResolveCollision(m, rbA, rbB)
[docs] def ResolveCollision(self, m, rbA, rbB): e = self.GetRestitution(rbA, rbB) normal = m.normal.copy() rv = rbA.velocity - rbB.velocity velAlongNormal = rv.dot(normal) if velAlongNormal < 0: return b = velAlongNormal / normal.dot(normal) # Infinite mass testing if math.isinf(rbA.mass): a = 0 elif math.isinf(rbB.mass): a = 1 + e else: a = (1 + e) * rbB.mass / (rbA.mass + rbB.mass) velA = a * b * normal # Reverse the normal (normal from B to A) normal *= -1 # Infinite mass testing if math.isinf(rbA.mass): a = 1 + e elif math.isinf(rbB.mass): a = 0 else: a = (1 + e) * rbA.mass / (rbA.mass + rbB.mass) velB = a * b * normal rbA.velocity -= velA rbB.velocity -= velB # Start friction rv = rbB.velocity - rbA.velocity t = (rv - rv.dot(m.normal) * m.normal).normalized() jt = rv.dot(t) jt /= 1 / rbA.mass + 1 / rbB.mass if math.isinf(rbA.mass + rbB.mass): j = 0 else: j = -(1 + e) * (rbB.velocity - rbA.velocity).dot(normal) j /= 1 / rbA.mass + 1 / rbB.mass mu = (rbA.physicMaterial.friction + rbB.physicMaterial.friction) / 2 if abs(jt) < j * mu: frictionImpulse = jt * t else: frictionImpulse = -j * t * mu rbA.velocity -= 1 / rbA.mass * frictionImpulse rbB.velocity += 1 / rbB.mass * frictionImpulse # End friction correction = m.penetration * \ (rbA.mass + rbB.mass) * 0.8 * m.normal if rbA is not self.dummyRigidbody: rbA.transform.position += self.correct_inf( rbA.mass, rbB.mass, correction, rbA.mass) if rbB is not self.dummyRigidbody: rbB.transform.position += self.correct_inf( rbA.mass, rbB.mass, correction, rbB.mass)
[docs] def correct_inf(self, a, b, correction, target): if not math.isinf(a + b): return 1 / target * correction else: return 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 manually by the scene, so it is more precise. """ for rb in self.rigidbodies: if rb is not self.dummyRigidbody: rb.Move(dt / 1) self.CheckCollisions()