Implement optimized collision detection system using NumPy

- Introduced a hybrid collision detection approach that utilizes NumPy for vectorized operations, improving performance for games with many entities (200+).
- Added a spatial grid for efficient lookups and AABB (Axis-Aligned Bounding Box) collision detection.
- Implemented a new `CollisionSystem` class with methods for registering units, checking collisions, and managing spatial data.
- Created performance tests to benchmark the new collision system against the old O(n²) method, demonstrating significant speed improvements.
- Updated existing code to integrate the new collision detection system and ensure compatibility with game logic.
This commit is contained in:
2025-10-24 19:13:30 +02:00
parent 47028c95ae
commit 12836dd2d2
17 changed files with 1591 additions and 64 deletions
Binary file not shown.
Binary file not shown.
+39 -18
View File
@@ -1,6 +1,7 @@
from .unit import Unit
from . import rat
from .points import Point
from engine.collision_system import CollisionLayer
import uuid
import random
@@ -11,7 +12,7 @@ NUCLEAR_TIMER = 50 # 1 second at ~50 FPS
class Bomb(Unit):
def __init__(self, game, position=(0,0), id=None):
super().__init__(game, position, id)
super().__init__(game, position, id, collision_layer=CollisionLayer.BOMB)
# Specific attributes for bombs
self.speed = 4 # Bombs age faster
self.fight = False
@@ -50,7 +51,7 @@ class Timer(Bomb):
self.die()
def die(self, unit=None, score=None):
"""Handle bomb explosion and chain reactions."""
"""Handle bomb explosion and chain reactions using vectorized collision system."""
score = 10
print("BOOM")
target_unit = unit if unit else self
@@ -65,24 +66,16 @@ class Timer(Bomb):
# Bomb-specific behavior: create explosion
self.game.spawn_unit(Explosion, target_unit.position)
# Collect all explosion positions using vectorized approach
explosion_positions = []
# Check for chain reactions in all four directions
for direction in ["N", "S", "E", "W"]:
x, y = target_unit.position
while True:
if not self.game.map.is_wall(x, y):
self.game.spawn_unit(Explosion, (x, y))
for victim in self.game.unit_positions.get((x, y), []):
if victim.id in self.game.units:
if victim.partial_move >= 0.5:
victim.die(score=score)
if score < 160:
score *= 2
for victim in self.game.unit_positions_before.get((x, y), []):
if victim.id in self.game.units:
if victim.partial_move < 0.5:
victim.die(score=score)
if score < 160:
score *= 2
explosion_positions.append((x, y))
else:
break
if direction == "N":
@@ -93,12 +86,40 @@ class Timer(Bomb):
x += 1
elif direction == "W":
x -= 1
# Create all explosions at once
for pos in explosion_positions:
self.game.spawn_unit(Explosion, pos)
# Use optimized collision system to get all rats in explosion area
# This replaces the nested loop with a single vectorized operation
victim_ids = self.game.collision_system.get_units_in_area(
explosion_positions,
layer_filter=CollisionLayer.RAT
)
# Kill all victims with score multiplier
for victim_id in victim_ids:
victim = self.game.get_unit_by_id(victim_id)
if victim and victim.id in self.game.units:
# Determine position based on partial_move
victim_pos = victim.position if victim.partial_move >= 0.5 else victim.position_before
if victim_pos in explosion_positions:
victim.die(score=score)
if score < 160:
score *= 2
class Explosion(Bomb):
def __init__(self, game, position=(0,0), id=None):
# Initialize with proper EXPLOSION layer
Unit.__init__(self, game, position, id, collision_layer=CollisionLayer.EXPLOSION)
self.speed = 20 # Bombs age faster * 5
self.fight = False
def move(self):
self.age += self.speed*5
if self.age == AGE_THRESHOLD:
self.age += self.speed
if self.age >= AGE_THRESHOLD:
self.die()
def draw(self):
@@ -114,7 +135,7 @@ class Explosion(Bomb):
class NuclearBomb(Unit):
def __init__(self, game, position=(0,0), id=None):
super().__init__(game, position, id)
super().__init__(game, position, id, collision_layer=CollisionLayer.BOMB)
self.speed = 1 # Slow countdown
self.fight = False
self.timer = NUCLEAR_TIMER # 1 second timer
+25 -8
View File
@@ -1,5 +1,6 @@
from .unit import Unit
from .rat import Rat
from engine.collision_system import CollisionLayer
import random
# Costanti
@@ -7,7 +8,7 @@ AGE_THRESHOLD = 200
class Gas(Unit):
def __init__(self, game, position=(0,0), id=None, parent_id=None):
super().__init__(game, position, id)
super().__init__(game, position, id, collision_layer=CollisionLayer.GAS)
self.parent_id = parent_id
# Specific attributes for gas
self.speed = 50
@@ -24,13 +25,29 @@ class Gas(Unit):
self.die()
return
self.age += 1
#victims = self.game.unit_positions.get(self.position, [])
victims = [rat for rat in self.game.unit_positions.get(self.position, []) if rat.partial_move>0.5]
for rat in self.game.unit_positions_before.get(self.position, []):
if rat.partial_move<0.5 and rat is Rat:
victims.append(rat)
for victim in victims:
victim.gassed += 1
# Use optimized collision system to find rats in gas cloud
victim_ids = self.game.collision_system.get_units_in_cell(
self.position, use_before=False
)
for victim_id in victim_ids:
victim = self.game.get_unit_by_id(victim_id)
if victim and isinstance(victim, Rat):
if victim.partial_move > 0.5:
victim.gassed += 1
# Check position_before as well
victim_ids_before = self.game.collision_system.get_units_in_cell(
self.position, use_before=True
)
for victim_id in victim_ids_before:
victim = self.game.get_unit_by_id(victim_id)
if victim and isinstance(victim, Rat):
if victim.partial_move < 0.5:
victim.gassed += 1
if self.age % self.speed:
return
parent = self.game.get_unit_by_id(self.parent_id)
+13 -6
View File
@@ -1,8 +1,10 @@
from .unit import Unit
from .bomb import Explosion
from engine.collision_system import CollisionLayer
class Mine(Unit):
def __init__(self, game, position=(0,0), id=None):
super().__init__(game, position, id)
super().__init__(game, position, id, collision_layer=CollisionLayer.MINE)
self.speed = 1.0 # Mine doesn't move but needs speed for consistency
self.armed = True # Mine is active and ready to explode
@@ -11,13 +13,18 @@ class Mine(Unit):
pass
def collisions(self):
"""Check if a rat steps on the mine (has position_before on mine's position)."""
"""Check if a rat steps on the mine using optimized collision system."""
if not self.armed:
return
# Check for rats that have position_before on this mine's position
for rat_unit in self.game.unit_positions_before.get(self.position, []):
if hasattr(rat_unit, 'sex'): # Check if it's a rat (rats have sex attribute)
# Use collision system to check for rats at mine's position_before
victim_ids = self.game.collision_system.get_units_in_cell(
self.position, use_before=True
)
for victim_id in victim_ids:
rat_unit = self.game.get_unit_by_id(victim_id)
if rat_unit and hasattr(rat_unit, 'sex'): # Check if it's a rat
# Mine explodes and kills the rat
self.explode(rat_unit)
break
+12 -6
View File
@@ -6,14 +6,20 @@ import uuid
AGE_THRESHOLD = 200
from .unit import Unit
from engine.collision_system import CollisionLayer
class Point(Unit):
def __init__(self, game, position=(0,0), id=None, value=5):
super().__init__(game, position, id)
# Specific attributes for points
self.speed = 4 # Points age faster
self.fight = False
"""
Represents a collectible point in the game.
Appears when a rat dies and can be collected by the player.
"""
def __init__(self, game, position=(0,0), id=None, value=10):
super().__init__(game, position, id, collision_layer=CollisionLayer.POINT)
self.value = value
self.game.add_point(self.value)
self.speed = 1 # Points don't move but need speed for draw timing
def move(self):
self.age += self.speed
+34 -20
View File
@@ -1,5 +1,6 @@
from .unit import Unit
from .points import Point
from engine.collision_system import CollisionLayer
import random
import uuid
@@ -13,7 +14,7 @@ BABY_INTERVAL = 50
class Rat(Unit):
def __init__(self, game, position=(0,0), id=None):
super().__init__(game, position, id)
super().__init__(game, position, id, collision_layer=CollisionLayer.RAT)
# Specific attributes for rats
self.speed = 0.10 # Rats are slower
self.fight = False
@@ -72,30 +73,43 @@ class Rat(Unit):
self.direction = self.calculate_rat_direction()
def collisions(self):
"""
Optimized collision detection using the vectorized collision system.
Uses spatial hashing and numpy for efficient checks with 200+ units.
"""
OVERLAP_TOLERANCE = self.game.cell_size // 4
# Only adult rats can collide for reproduction/fighting
if self.age < AGE_THRESHOLD:
return
units = []
units.extend(self.game.unit_positions.get(self.position_before, []))
units.extend(self.game.unit_positions.get(self.position, []))
for unit in units:
if unit.id == self.id or unit.age < AGE_THRESHOLD or self.position != unit.position_before:
continue
x1, y1, x2, y2 = self.bbox
ox1, oy1, ox2, oy2 = unit.bbox
# Get collisions from the optimized collision system
collisions = self.game.collision_system.get_collisions_for_unit(
self.id,
CollisionLayer.RAT,
tolerance=OVERLAP_TOLERANCE
)
# Process each collision
for _, other_id in collisions:
other_unit = self.game.get_unit_by_id(other_id)
# Verifica se c'è collisione con una tolleranza di sovrapposizione
if (x1 < ox2 - OVERLAP_TOLERANCE and
x2 > ox1 + OVERLAP_TOLERANCE and
y1 < oy2 - OVERLAP_TOLERANCE and
y2 > oy1 + OVERLAP_TOLERANCE):
if self.id in self.game.units and unit.id in self.game.units:
if self.sex == unit.sex and self.fight:
self.die(unit)
elif self.sex != unit.sex:
if "fuck" in dir(self):
self.fuck(unit)
if not other_unit or other_unit.age < AGE_THRESHOLD:
continue
# Check if units are actually moving towards each other
if self.position != other_unit.position_before:
continue
# Both units still exist in game
if self.id in self.game.units and other_id in self.game.units:
if self.sex == other_unit.sex and self.fight:
# Same sex + fight mode = combat
self.die(other_unit)
elif self.sex != other_unit.sex:
# Different sex = reproduction
if "fuck" in dir(self):
self.fuck(other_unit)
def die(self, unit=None, score=10):
"""Handle rat death and spawn points."""
+4 -1
View File
@@ -26,6 +26,8 @@ class Unit(ABC):
Bounding box for collision detection (x1, y1, x2, y2).
stop : int
Number of ticks to remain stationary.
collision_layer : int
Collision layer for the optimized collision system.
Methods
-------
@@ -38,7 +40,7 @@ class Unit(ABC):
die()
Remove unit from game and handle cleanup.
"""
def __init__(self, game, position=(0, 0), id=None):
def __init__(self, game, position=(0, 0), id=None, collision_layer=0):
"""Initialize a unit with game reference and position."""
self.id = id if id else uuid.uuid4()
self.game = game
@@ -49,6 +51,7 @@ class Unit(ABC):
self.partial_move = 0
self.bbox = (0, 0, 0, 0)
self.stop = 0
self.collision_layer = collision_layer
@abstractmethod
def move(self):