overhaul to the collision system... should have maybe made more commits.

This commit is contained in:
=
2026-03-11 12:14:45 -04:00
parent ebfffe5c09
commit 55f20a950f
11 changed files with 386 additions and 152 deletions

218
main.py
View File

@@ -1,176 +1,90 @@
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
from math import pi
from rigidbody import *
from collider import LineCollider, CircleCollider
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
class Ball:
@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):
def __init__(self, transform: Transform, radius: float):
self.transform = transform
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
self.surface = pg.Surface((2*self.radius, 2*self.radius), pg.SRCALPHA)
self.surface.fill((255,255,255,0))
pg.draw.circle(
self.surface,
color=(0,255,0,255),
center=(self.radius,self.radius),
radius=self.radius
)
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)
surface = pg.transform.rotate(self.surface, self.transform.global_degrees)
screen.blit(surface, self.transform.global_position - pg.Vector2(self.radius, self.radius))
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
class Square:
def __init__(self, transform: Transform, side: float, color=(255,0,0,255)):
self.transform = transform
self.side = side
self.surface = pg.Surface((side, side), pg.SRCALPHA)
self.surface.fill((255,255,255,0))
pg.draw.rect(
self.surface,
color=color,
rect=pg.Rect(0, 0, self.side, self.side)
)
self.entities.append(Circle(radius,body,sprite))
def draw(self, screen: pg.Surface):
surface = pg.transform.rotate(self.surface, self.transform.global_degrees)
screen.blit(surface, self.transform.global_position - pg.Vector2(self.side / 2.0, self.side / 2.0))
def main():
running=True
pg.init()
clock=pg.time.Clock()
screen = pg.display.set_mode(size=DIMENSIONS)
world=World(screen)
screen = pg.display.set_mode((500,500))
physics = PhysicsSystem()
ball_transform = Transform(position=pg.Vector2(250,250), rotation=pi/8.0)
square_transform = Transform(position=pg.Vector2(250, 100))
ball = Square(ball_transform, 20)
ball2 = Square(square_transform, 20, color=(0,255,0,255))
physics.add_body(
RigidBody(
ball_transform,
RectCollider((20,20)),
velocity=pg.Vector2(0,-400),
restitution=1.0,
)
)
physics.add_body(
RigidBody(
square_transform,
RectCollider((20,20)),
pg.Vector2(0,0),
restitution=1.0
)
)
while running:
dt = clock.tick(144) / 1000
screen.fill((0,0,0,0))
world.update(dt)
world.draw()
physics.update(dt)
ball.draw(screen)
ball2.draw(screen)
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