Created
November 22, 2023 03:50
-
-
Save danodic/2349435d38e9f8e4ee851263be5d3db5 to your computer and use it in GitHub Desktop.
Vector Collisions using PyGame
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This code is ugly and slow, just a reference. | |
# But is works (mostly) | |
from __future__ import annotations | |
from dataclasses import dataclass | |
from typing import Tuple | |
import pygame.draw | |
from pygame import Surface | |
WHITE = (255, 255, 255) | |
GREEN = (0, 255, 0) | |
BLUE = (0, 0, 255) | |
RED = (255, 0, 0) | |
PINK = (255, 0, 255) | |
YELLOW = (255, 255, 0) | |
@dataclass | |
class Vector2: | |
x: float | |
y: float | |
def __add__(self, other: Vector2 | int | float) -> Vector2: | |
if isinstance(other, Vector2): | |
return Vector2(self.x + other.x, self.y + other.y) | |
return Vector2(self.x + other, self.y + other) | |
def __sub__(self, other: Vector2 | int | float) -> Vector2: | |
if isinstance(other, Vector2): | |
return Vector2(self.x - other.x, self.y - other.y) | |
return Vector2(self.x - other, self.y - other) | |
def __mul__(self, other: Vector2 | int | float) -> Vector2: | |
if isinstance(other, Vector2): | |
return Vector2(self.x * other.x, self.y * other.y) | |
return Vector2(self.x * other, self.y * other) | |
def __truediv__(self, other: Vector2 | int | float) -> Vector2: | |
if isinstance(other, Vector2): | |
return Vector2(self.x / other.x, self.y / other.y) | |
return Vector2(self.x / other, self.y / other) | |
def __pow__(self, other: Vector2 | int | float): | |
if isinstance(other, Vector2): | |
return Vector2(self.x ** other.x, self.y ** other.y) | |
return Vector2(self.x ** other, self.y ** other) | |
def __bool__(self): | |
return not (self.x == 0 and self.y == 0) | |
@dataclass | |
class MovementVector: | |
origin: Vector2 | |
destination: Vector2 | |
def render(self, screen: Surface): | |
pygame.draw.line(screen, GREEN, (self.origin.x, self.origin.y), (self.destination.x, self.destination.y)) | |
class Entity: | |
def __init__(self, position: Vector2, size: Vector2): | |
self.position = position | |
self.last_position = position | |
self.size = size | |
self.collided = False | |
def movement_vector(self) -> MovementVector: | |
origin = self.last_position + (self.size / 2) | |
destination = self.position + (self.size / 2) | |
return MovementVector(origin, destination) | |
def render(self, screen: Surface, color=None): | |
if not color: | |
color = PINK if self.collided else WHITE | |
pygame.draw.rect(screen, color, (self.position.x, self.position.y, self.size.x, self.size.y), 1) | |
def translate(self, value: Vector2): | |
self.last_position = self.position | |
self.position = self.position + value | |
# --- | |
def make_entity_sides(entity: Entity) -> Tuple[MovementVector, MovementVector, MovementVector, MovementVector]: | |
upside = MovementVector(Vector2(entity.position.x, | |
entity.position.y), | |
Vector2(entity.position.x + entity.size.x, | |
entity.position.y)) | |
downside = MovementVector(Vector2(entity.position.x, | |
entity.position.y + entity.size.y), | |
Vector2(entity.position.x + entity.size.x, | |
entity.position.y + entity.size.y)) | |
leftside = MovementVector(Vector2(entity.position.x, | |
entity.position.y), | |
Vector2(entity.position.x, | |
entity.position.y + entity.size.y)) | |
rightside = MovementVector(Vector2(entity.position.x + entity.size.x, | |
entity.position.y), | |
Vector2(entity.position.x + entity.size.x, | |
entity.position.y + entity.size.y)) | |
return upside, downside, leftside, rightside | |
def expand_entity(to_expand: Entity, to_collide: Entity): | |
return Entity(to_expand.position - (to_collide.size / 2) + 1, | |
to_expand.size + to_collide.size - 1) | |
def calculate_normal(movement_vector: MovementVector) -> Vector2: | |
normal = Vector2(0, 0) | |
if movement_vector.origin.x > movement_vector.destination.x: | |
normal.x = 1 | |
elif movement_vector.origin.x < movement_vector.destination.x: | |
normal.x = -1 | |
if movement_vector.origin.y > movement_vector.destination.y: | |
normal.y = -1 | |
elif movement_vector.origin.y < movement_vector.destination.y: | |
normal.y = 1 | |
return normal | |
def collides(to_collide: Entity, entity: Entity): | |
dynamic_vector = to_collide.movement_vector() | |
expanded = expand_entity(entity, to_collide) | |
expanded.render(screen, BLUE) | |
up, down, left, right = make_entity_sides(expanded) | |
collides_right = intersect(dynamic_vector.origin, | |
dynamic_vector.destination, | |
right.origin, | |
right.destination) | |
collides_left = intersect(dynamic_vector.origin, | |
dynamic_vector.destination, | |
left.origin, | |
left.destination) | |
collides_up = intersect(dynamic_vector.origin, | |
dynamic_vector.destination, | |
up.origin, | |
up.destination) | |
collides_down = intersect(dynamic_vector.origin, | |
dynamic_vector.destination, | |
down.origin, | |
down.destination) | |
collides_diagonal = (collides_left and (collides_up or collides_down)) or \ | |
(collides_right and (collides_up or collides_down)) | |
normals = calculate_normal(dynamic_vector) | |
if collides_right: | |
pygame.draw.line(screen, YELLOW, (right.origin.x, right.origin.y), | |
(right.destination.x, right.destination.y)) | |
if collides_left: | |
pygame.draw.line(screen, YELLOW, (left.origin.x, left.origin.y), | |
(left.destination.x, left.destination.y)) | |
if collides_up: | |
pygame.draw.line(screen, YELLOW, (up.origin.x, up.origin.y), | |
(up.destination.x, up.destination.y)) | |
if collides_down: | |
pygame.draw.line(screen, YELLOW, (down.origin.x, down.origin.y), | |
(down.destination.x, down.destination.y)) | |
resolution = Vector2(0, 0) | |
if collides_diagonal: | |
if collides_left: | |
resolution.x = expanded.position.x - dynamic_vector.destination.x | |
elif collides_right: | |
resolution.x = (expanded.position.x + expanded.size.x) - dynamic_vector.destination.x | |
if collides_up: | |
resolution.y = expanded.position.y - dynamic_vector.destination.y - 1 | |
elif collides_down: | |
resolution.y = (expanded.position.y + expanded.size.y) - dynamic_vector.destination.y + 1 | |
elif collides_right or collides_left: | |
if normals.x > 0: | |
pygame.draw.line(screen, RED, | |
(expanded.position.x + expanded.size.x, | |
expanded.position.y + (expanded.size.y / 2)), | |
(expanded.position.x + expanded.size.x + 10, | |
expanded.position.y + (expanded.size.y / 2))) | |
resolution.x = (expanded.position.x + expanded.size.x) - dynamic_vector.destination.x + 1 | |
elif normals.x < 0: | |
pygame.draw.line(screen, RED, | |
(entity.position.x, entity.position.y + (entity.size.y / 2)), | |
(entity.position.x - 10, entity.position.y + (entity.size.y / 2))) | |
resolution.x = expanded.position.x - dynamic_vector.destination.x - 1 | |
elif collides_up or collides_down: | |
if normals.y > 0: | |
pygame.draw.line(screen, RED, | |
(expanded.position.x + (expanded.size.x / 2), expanded.position.y), | |
(expanded.position.x + (expanded.size.x / 2), | |
expanded.position.y - 10)) | |
resolution.y = expanded.position.y - dynamic_vector.destination.y - 1 | |
elif normals.y < 0: | |
pygame.draw.line(screen, RED, | |
(expanded.position.x + (expanded.size.x / 2), | |
expanded.position.y + expanded.size.y), | |
(expanded.position.x + (expanded.size.x / 2), | |
expanded.position.y + expanded.size.y + 10)) | |
resolution.y = (expanded.position.y + expanded.size.y) - dynamic_vector.destination.y + 1 | |
to_collide.position += resolution | |
# --- | |
def cross_product(p1: Vector2, p2: Vector2): | |
return p1.x * p2.y - p2.x * p1.y | |
def on_segment(p1: Vector2, p2: Vector2, p: Vector2): | |
return min(p1.x, p2.x) <= p.x <= max(p1.x, p2.x) and \ | |
min(p1.y, p2.y) <= p.y <= max(p1.y, p2.y) | |
def direction(p1: Vector2, p2: Vector2, p3: Vector2): | |
dir = cross_product(p3 - p1, p2 - p1) | |
if dir > 0: | |
return 1 # Clockwise (right) | |
if dir < 0: | |
return -1 # Counterclockwise (left) | |
return 0 # Collinear | |
def intersect(p1: Vector2, p2: Vector2, p3: Vector2, p4: Vector2): | |
d1 = direction(p3, p4, p1) | |
d2 = direction(p3, p4, p2) | |
d3 = direction(p1, p2, p3) | |
d4 = direction(p1, p2, p4) | |
return (((d1 > 0 and d2 < 0) or (d1 < 0 and d2 > 0)) and | |
((d3 > 0 and d4 < 0) or (d3 < 0 and d4 > 0))) or \ | |
(d1 == 0 and on_segment(p3, p4, p1)) or \ | |
(d2 == 0 and on_segment(p3, p4, p2)) or \ | |
(d3 == 0 and on_segment(p1, p2, p3)) or \ | |
(d4 == 0 and on_segment(p1, p2, p4)) | |
# --- | |
def move_entity(entity, speed): | |
global pos_y, pos_x | |
screen.fill((0, 0, 0)) | |
translation = Vector2(0, 0) | |
keys = pygame.key.get_pressed() | |
if keys[pygame.K_UP]: | |
translation.y -= speed | |
if keys[pygame.K_DOWN]: | |
translation.y += speed | |
if keys[pygame.K_LEFT]: | |
translation.x -= speed | |
if keys[pygame.K_RIGHT]: | |
translation.x += speed | |
for _ in pygame.event.get(): | |
pass | |
entity.translate(translation) | |
# --- | |
static_entity = Entity(Vector2(350, 250), Vector2(100, 100)) | |
moving_entity = Entity(Vector2(350, 100), Vector2(50, 50)) | |
# --- | |
pygame.init() | |
screen = pygame.display.set_mode((800, 600)) | |
clock = pygame.time.Clock() | |
speed = 2 | |
pos_y = 300 | |
pos_x = 300 | |
while True: | |
for event in pygame.event.get(): | |
pass | |
move_entity(moving_entity, speed) | |
dynamic_ray = MovementVector(Vector2(pos_x, pos_y), Vector2(pos_x + 100, pos_y + 100)) | |
collides(moving_entity, static_entity) | |
static_entity.render(screen) | |
moving_entity.render(screen) | |
moving_entity.movement_vector().render(screen) | |
pygame.display.update() | |
clock.tick(60) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment