Skip to content

Instantly share code, notes, and snippets.

@charles-l
Last active April 1, 2024 21:35
Show Gist options
  • Save charles-l/2af6870968a02d99c1d91fe977acf51d to your computer and use it in GitHub Desktop.
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
# `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