Skip to content

Instantly share code, notes, and snippets.

@EncodeTheCode
Created February 21, 2026 13:36
Show Gist options
  • Select an option

  • Save EncodeTheCode/1f0fca03ffeef4fb063dc92223c8b426 to your computer and use it in GitHub Desktop.

Select an option

Save EncodeTheCode/1f0fca03ffeef4fb063dc92223c8b426 to your computer and use it in GitHub Desktop.
import numpy as np
from typing import Callable, Optional
# -------------------------
# Utility functions
# -------------------------
def aabb_overlap(min_a: np.ndarray, max_a: np.ndarray, min_b: np.ndarray, max_b: np.ndarray) -> bool:
"""Fast AABB overlap test: True if boxes overlap on X,Y,Z."""
# vectorized: overlap if max_a >= min_b and max_b >= min_a on all axes
return np.all(max_a >= min_b) and np.all(max_b >= min_a)
def sphere_overlap(center_a: np.ndarray, r_a: float, center_b: np.ndarray, r_b: float) -> bool:
"""Bounding-sphere overlap test."""
# compare squared distances to avoid sqrt
d2 = np.sum((center_a - center_b) ** 2)
rr = (r_a + r_b) ** 2
return d2 <= rr
# -------------------------
# Core classes
# -------------------------
class CollisionBehavior:
"""Container for callbacks and trigger control."""
__slots__ = ("callback", "trigger_once", "triggered")
def __init__(self, callback: Optional[Callable] = None, trigger_once: bool = True):
self.callback = callback
self.trigger_once = trigger_once
self.triggered = False
def try_trigger(self, *args, **kwargs):
"""Call callback if allowed by trigger_once state."""
if self.callback is None:
return
if not self.trigger_once or not self.triggered:
# call user callback
self.callback(*args, **kwargs)
self.triggered = True
def reset(self):
self.triggered = False
class Entity:
"""
Minimal memory footprint entity.
- position: numpy array shape (3,)
- aabb_min/aabb_max: numpy arrays shape (3,) cached for fast tests
- bs_center/bs_radius: bounding sphere optional
- vertices: optional numpy array (n,3) if you need vertex-level construction; can be freed.
"""
__slots__ = (
"position", "aabb_min", "aabb_max",
"bs_center", "bs_radius",
"vertices", "collision_behavior", "_dirty"
)
def __init__(
self,
position=(0.0, 0.0, 0.0),
half_extents=(0.5, 0.5, 0.5),
*,
vertices: Optional[np.ndarray] = None,
callback: Optional[Callable] = None,
trigger_once: bool = True
):
# keep position as float64 numpy vector
self.position = np.asarray(position, dtype=np.float64)
self.vertices = None if vertices is None else np.asarray(vertices, dtype=np.float64)
self.collision_behavior = CollisionBehavior(callback=callback, trigger_once=trigger_once)
# If vertices provided, build bounds from them (local space assumed)
if self.vertices is not None:
self._build_bounds_from_vertices()
else:
# center = position, half extents define AABB around center
he = np.asarray(half_extents, dtype=np.float64)
self.aabb_min = self.position - he
self.aabb_max = self.position + he
self.bs_center = self.position.copy()
self.bs_radius = float(np.linalg.norm(he))
self._dirty = False # if you later change position or vertices set dirty=True
# --- bounds construction ---
def _build_bounds_from_vertices(self):
"""Compute AABB and bounding sphere from vertex array (assumes vertices in world coords)."""
v = self.vertices
# use vectorized numpy min/max
vmin = v.min(axis=0)
vmax = v.max(axis=0)
self.aabb_min = vmin.copy()
self.aabb_max = vmax.copy()
# sphere center = midpoint of aabb, radius = max distance to corner
center = (vmin + vmax) / 2.0
self.bs_center = center
# compute radius squared then sqrt
self.bs_radius = float(np.sqrt(np.max(np.sum((v - center) ** 2, axis=1))))
# make sure position is consistent with center (optional)
self.position = center.copy()
def recompute_bounds_if_dirty(self):
"""Call this if you mutate position or vertices and set _dirty = True."""
if not getattr(self, "_dirty", False):
return
if self.vertices is not None:
self._build_bounds_from_vertices()
else:
# nothing to do without vertices unless user changed position externally
pass
self._dirty = False
def free_vertices(self):
"""Free vertex buffer to save memory after bounds are built."""
self.vertices = None
# --- convenience ---
def set_position(self, new_pos):
self.position = np.asarray(new_pos, dtype=np.float64)
# translate AABB and sphere by delta if needed
# assume aabb currently centered at previous self.position; compute delta:
center = (self.aabb_min + self.aabb_max) / 2.0
delta = self.position - center
self.aabb_min = self.aabb_min + delta
self.aabb_max = self.aabb_max + delta
self.bs_center = self.bs_center + delta
def reset_triggered(self):
self.collision_behavior.reset()
# -------------------------
# Single-call collision check & trigger
# -------------------------
def check_collision(a: Entity, b: Entity, method: str = "aabb_and_sphere") -> bool:
"""
Efficient collision check between two entities.
- method options:
- 'aabb' : only AABB vs AABB
- 'sphere' : bounding-sphere only (fast, conservative)
- 'aabb_and_sphere' (default): cheap sphere test first, then exact AABB test
Returns True if overlap.
"""
# ensure cached bounds are up-to-date (cheap check)
a.recompute_bounds_if_dirty()
b.recompute_bounds_if_dirty()
if method == "sphere":
return sphere_overlap(a.bs_center, a.bs_radius, b.bs_center, b.bs_radius)
if method == "aabb":
return aabb_overlap(a.aabb_min, a.aabb_max, b.aabb_min, b.aabb_max)
# default: cheap sphere reject, then AABB check
if not sphere_overlap(a.bs_center, a.bs_radius, b.bs_center, b.bs_radius):
return False
return aabb_overlap(a.aabb_min, a.aabb_max, b.aabb_min, b.aabb_max)
def check_and_trigger(a: Entity, b: Entity, method: str = "aabb_and_sphere", call_both: bool = True) -> bool:
"""
Check collision and trigger callbacks:
- If a collision occurs:
- runs a.collision_behavior.try_trigger(a, b)
- if call_both True, also runs b.collision_behavior.try_trigger(b, a)
- Returns True if collision detected.
"""
collided = check_collision(a, b, method=method)
if collided:
a.collision_behavior.try_trigger(a, b)
if call_both:
b.collision_behavior.try_trigger(b, a)
else:
# If not collided and the user wants one-shot triggers to be re-armed behaviorally
# we do NOT automatically reset triggered flags here; leaving control to user is nicer.
# If you want automatic rearming when they separate: uncomment next two lines.
# a.reset_triggered()
# b.reset_triggered()
pass
return collided
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment