Last active
April 1, 2024 21:35
-
-
Save charles-l/2af6870968a02d99c1d91fe977acf51d to your computer and use it in GitHub Desktop.
Simple collisions for action games (raylib python). Blog post: https://c.har.li/e/2024/03/28/implementing-robust-2D-collision-resolution.html
This file contains 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
# `pip install raylib` to get pyray | |
import pyray as rl | |
from dataclasses import dataclass | |
import random | |
from typing import Optional | |
SIZE = 32 | |
def copy_rect(rect): | |
return rl.Rectangle(rect.x, rect.y, rect.width, rect.height) | |
def get_signed_collision_rec(rect1: rl.Rectangle, rect2: rl.Rectangle) -> rl.Rectangle: | |
"""Compute the rectangle of intersection between rect1 and rect2. | |
If rect2 is to the left or above rect1, the width or height will | |
be flipped, respectively.""" | |
r = rl.get_collision_rec(rect1, rect2) | |
if rect2.x < rect1.x: | |
r.width = -r.width | |
if rect2.y < rect1.y: | |
r.height = -r.height | |
return r | |
def resolve_map_collision(map_aabbs, actor_aabb) -> Optional[rl.Vector2]: | |
"""Fix overlap with map tiles. Returns new position for actor_aabb.""" | |
# internal copy of actor_aabb that will be mutated | |
aabb = copy_rect(actor_aabb) | |
if map_aabbs: | |
for i in range(10): # run multiple iters to handle corners/passages | |
most_overlap = max( | |
(get_signed_collision_rec(r, aabb) for r in map_aabbs), | |
key=lambda x: abs(x.width * x.height), | |
) | |
if abs(most_overlap.width) < abs(most_overlap.height): | |
aabb.x += most_overlap.width | |
else: | |
aabb.y += most_overlap.height | |
new_pos = (aabb.x, aabb.y) | |
old_pos = (actor_aabb.x, actor_aabb.y) | |
return new_pos if new_pos != old_pos else None | |
@dataclass | |
class Static: | |
rect: rl.Rectangle | |
@dataclass | |
class Dynamic: | |
rect: rl.Rectangle | |
vel: rl.Vector2 | |
def __hash__(self): | |
return id(self) | |
entities = [] | |
# Oscillations are still a problem (get enough speed, and you can phase through) | |
entities.append(Static(rl.Rectangle(100 + SIZE, 100 + SIZE, SIZE, SIZE))) | |
entities.append(Static(rl.Rectangle(100 + SIZE * 3 - 2, 100 + SIZE, SIZE, SIZE))) | |
rl.init_window(800, 600, "Game") | |
rl.set_target_fps(60) | |
while not rl.window_should_close(): | |
rl.begin_drawing() | |
rl.clear_background(rl.BLACK) | |
static_rects = [x.rect for x in entities if isinstance(x, Static)] | |
p = rl.get_mouse_position() | |
p.x = (p.x // SIZE) * SIZE | |
p.y = (p.y // SIZE) * SIZE | |
width = SIZE | |
height = SIZE | |
#width = random.uniform(10, 100) | |
#height = random.uniform(10, 100) | |
if rl.is_mouse_button_released(rl.MOUSE_BUTTON_LEFT): | |
entities.append(Static(rl.Rectangle(p.x, p.y, width, height))) | |
if rl.is_mouse_button_released(rl.MOUSE_BUTTON_RIGHT): | |
entities.append(Dynamic(rl.Rectangle(p.x, p.y, SIZE, SIZE), rl.Vector2(0, 0))) | |
prev_pos = {} | |
for e in entities: | |
if hasattr(e, 'vel'): | |
prev_pos[e] = rl.Vector2(e.rect.x, e.rect.y) | |
if rl.is_key_down(rl.KEY_LEFT): | |
e.vel.x = -2 | |
elif rl.is_key_down(rl.KEY_RIGHT): | |
e.vel.x = 2 | |
else: | |
e.vel.x = 0 | |
if rl.is_key_down(rl.KEY_UP): | |
e.vel.y = -2 | |
else: | |
e.vel.y += 0.1 | |
e.rect.x += e.vel.x | |
e.rect.y += e.vel.y | |
for e in entities: | |
if isinstance(e, Dynamic): | |
if (new_pos := resolve_map_collision(static_rects, e.rect)) is not None: | |
e.rect.x, e.rect.y = new_pos | |
for e in entities: | |
if isinstance(e, Dynamic) and e in prev_pos: | |
e.vel.x = e.rect.x - prev_pos[e].x | |
e.vel.y = e.rect.y - prev_pos[e].y | |
for e in entities: | |
if isinstance(e, Static): | |
rl.draw_rectangle_rec(e.rect, rl.BROWN) | |
elif isinstance(e, Dynamic): | |
rl.draw_rectangle_rec(e.rect, rl.GREEN) | |
rl.end_drawing() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment