178 lines
5.7 KiB
Python
178 lines
5.7 KiB
Python
import pygame as pg
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass
|
|
from itertools import combinations
|
|
from collections import namedtuple
|
|
from random import random
|
|
|
|
GLOBAL_GRAVITY=250
|
|
DIMENSIONS = namedtuple("Dimensions", ["WIDTH", "HEIGHT"])(500,500)
|
|
|
|
@dataclass
|
|
class ColliderContact:
|
|
__slots__ = ["point", "normal", "penetration"]
|
|
point: pg.Vector2
|
|
normal: pg.Vector2
|
|
penetration: float
|
|
|
|
@dataclass
|
|
class RigidBody:
|
|
position: pg.Vector2
|
|
collider: "BaseCollider"
|
|
velocity: pg.Vector2
|
|
mass: float = 1.0
|
|
restitution: float = 0.5
|
|
|
|
@property
|
|
def inv_mass(self):
|
|
return 0.0 if self.mass == 0 else 1.0 / self.mass
|
|
|
|
class BaseCollider(ABC):
|
|
|
|
def collide(self, other: "BaseCollider", this_position: pg.Vector2, other_position: pg.Vector2) -> ColliderContact | None:
|
|
if isinstance(other, CircleCollider):
|
|
return self.collide_circle(other, this_position, other_position)
|
|
|
|
|
|
@abstractmethod
|
|
def collide_circle(self, other: "CircleCollider", this_position: pg.Vector2, other_position: pg.Vector2) -> ColliderContact | None:
|
|
pass
|
|
|
|
class CircleCollider(BaseCollider):
|
|
|
|
def __init__(self, radius: float):
|
|
self.radius = radius
|
|
|
|
def collide_circle(self, other: "CircleCollider", this_position: pg.Vector2, other_position: pg.Vector2) -> ColliderContact | None:
|
|
delta = this_position - other_position
|
|
dist = delta.length()
|
|
radii = self.radius + other.radius
|
|
if dist >= radii or dist == 0:
|
|
return None
|
|
normal = delta.normalize()
|
|
return ColliderContact(
|
|
point=other_position + normal * other.radius,
|
|
normal=normal,
|
|
penetration=radii - dist,
|
|
)
|
|
|
|
class PhysicsSystem:
|
|
|
|
def __init__(self):
|
|
self.bodies: list[RigidBody] = []
|
|
self.gravity = pg.Vector2(0,GLOBAL_GRAVITY)
|
|
|
|
def add_body(self, body: RigidBody) -> None:
|
|
self.bodies.append(body)
|
|
|
|
def update(self, dt: float):
|
|
g = self.gravity * dt
|
|
for body in self.bodies:
|
|
body.velocity += g
|
|
body.position += dt * body.velocity
|
|
self.resolve_bounds(body)
|
|
|
|
for a, b in combinations(self.bodies, 2):
|
|
if collision := a.collider.collide(b.collider, a.position, b.position):
|
|
self.resolve_collision(a, b, collision)
|
|
|
|
def resolve_collision(self, a: RigidBody, b: RigidBody, collision: ColliderContact) -> None:
|
|
SLACK=0.4
|
|
correction = collision.penetration / (a.inv_mass + b.inv_mass) * SLACK * collision.normal
|
|
a.position += correction
|
|
b.position -= correction
|
|
|
|
v_rel = a.velocity - b.velocity
|
|
restitution = a.restitution * b.restitution
|
|
impulse = (-(1 + restitution) * (collision.normal.dot(v_rel))) / (a.inv_mass + b.inv_mass)
|
|
a.velocity += collision.normal*impulse*a.inv_mass
|
|
b.velocity -= collision.normal*impulse*b.inv_mass
|
|
|
|
def resolve_bounds(self, body: RigidBody) -> None:
|
|
r = body.collider.radius
|
|
|
|
if body.position.x - r < 0:
|
|
body.position.x = r
|
|
body.velocity.x = abs(body.velocity.x) * body.restitution
|
|
elif body.position.x + r > DIMENSIONS.WIDTH:
|
|
body.position.x = DIMENSIONS.WIDTH - r
|
|
body.velocity.x = -abs(body.velocity.x) * body.restitution
|
|
|
|
if body.position.y - r < 0:
|
|
body.position.y = r
|
|
body.velocity.y = abs(body.velocity.y) * body.restitution
|
|
elif body.position.y + r > DIMENSIONS.HEIGHT:
|
|
body.position.y = DIMENSIONS.HEIGHT - r
|
|
body.velocity.y = -abs(body.velocity.y) * body.restitution
|
|
|
|
class Circle:
|
|
|
|
def __init__(self, radius: float, body: RigidBody, sprite: pg.Surface):
|
|
self.radius = radius
|
|
self.body = body
|
|
self.sprite = sprite
|
|
|
|
def draw(self, screen: pg.Surface):
|
|
screen.blit(
|
|
self.sprite,
|
|
(
|
|
self.body.position.x - self.radius,
|
|
self.body.position.y - self.radius
|
|
)
|
|
)
|
|
|
|
class World:
|
|
|
|
def __init__(self, screen: pg.Surface):
|
|
self.physics = PhysicsSystem()
|
|
self.screen = screen
|
|
self.entities = []
|
|
|
|
def update(self, dt) -> None:
|
|
self.physics.update(dt)
|
|
|
|
def draw(self) -> None:
|
|
for entity in self.entities:
|
|
entity.draw(self.screen)
|
|
|
|
def spawn_circle(self, radius: float, position: pg.Vector2, velocity: pg.Vector2 | None = None, restitution:float = 0.5, mass: float = 1.0) -> None:
|
|
if velocity is None:
|
|
velocity = pg.Vector2(0,0)
|
|
collider = CircleCollider(radius)
|
|
body = RigidBody(position=position, collider=collider, velocity=velocity, restitution=restitution,mass=mass)
|
|
self.physics.add_body(body)
|
|
sprite = pg.Surface((2*radius, 2*radius),pg.SRCALPHA)
|
|
sprite.fill((255,255,255,0))
|
|
pg.draw.circle(
|
|
sprite,
|
|
color=(0,255,0,255),
|
|
center=(radius, radius),
|
|
radius=radius
|
|
)
|
|
self.entities.append(Circle(radius,body,sprite))
|
|
|
|
def main():
|
|
running=True
|
|
pg.init()
|
|
clock=pg.time.Clock()
|
|
screen = pg.display.set_mode(size=DIMENSIONS)
|
|
world=World(screen)
|
|
|
|
while running:
|
|
dt = clock.tick(144) / 1000
|
|
screen.fill((0,0,0,0))
|
|
world.update(dt)
|
|
world.draw()
|
|
pg.display.flip()
|
|
for event in pg.event.get():
|
|
if event.type == pg.MOUSEBUTTONDOWN:
|
|
world.spawn_circle(
|
|
10,
|
|
pg.Vector2(pg.mouse.get_pos()),
|
|
restitution=random()
|
|
)
|
|
if event.type == pg.QUIT:
|
|
running = False
|
|
|
|
if __name__ == "__main__":
|
|
main() |