v1.1: Performance optimizations and bug fixes
Major improvements: - NumPy-based collision system supporting 200+ units (~3ms/frame) - Spatial hashing with vectorized distance calculations - 4-pass game loop ensuring correct collision timing - Blood overlay system with pre-generated stain pool - Cached render positions and viewport bounds - Spawn protection preventing rats spawning on weapons Bug fixes: - Fixed bombs not killing rats (collision system timing) - Fixed gas not affecting rats (collision system timing) - Fixed rats spawning on weapons (added has_weapon_at check) - Fixed AttributeError with Gas collisions (added isinstance check) - Fixed blood stain transparency (RGBA + SDL_BLENDMODE_BLEND) - Reduced point lifetime from 200 to 90 frames (~1.5s) - Blood layer now clears on game restart Technical changes: - Added engine/collision_system.py with CollisionLayer enum - Updated all units to use collision layers - Pre-allocate NumPy arrays with capacity management - Hybrid collision approach (<10 simple, ≥10 vectorized) - Python 3.13 compatibility
This commit is contained in:
+43
-28
@@ -6,16 +6,25 @@ class Graphics():
|
||||
self.tunnel = self.render_engine.load_image("Rat/BMP_TUNNEL.png", surface=True)
|
||||
self.grasses = [self.render_engine.load_image(f"Rat/BMP_1_GRASS_{i+1}.png", surface=True) for i in range(4)]
|
||||
self.rat_assets = {}
|
||||
self.rat_assets_textures = {}
|
||||
self.rat_assets_textures = {}
|
||||
self.rat_image_sizes = {} # Pre-cache image sizes
|
||||
self.bomb_assets = {}
|
||||
|
||||
for sex in ["MALE", "FEMALE", "BABY"]:
|
||||
self.rat_assets[sex] = {}
|
||||
for direction in ["UP", "DOWN", "LEFT", "RIGHT"]:
|
||||
self.rat_assets[sex][direction] = self.render_engine.load_image(f"Rat/BMP_{sex}_{direction}.png", transparent_color=(128, 128, 128))
|
||||
|
||||
# Load textures and pre-cache sizes
|
||||
for sex in ["MALE", "FEMALE", "BABY"]:
|
||||
self.rat_assets_textures[sex] = {}
|
||||
self.rat_image_sizes[sex] = {}
|
||||
for direction in ["UP", "DOWN", "LEFT", "RIGHT"]:
|
||||
self.rat_assets_textures[sex][direction] = self.render_engine.load_image(f"Rat/BMP_{sex}_{direction}.png", transparent_color=(128, 128, 128), surface=False)
|
||||
texture = self.render_engine.load_image(f"Rat/BMP_{sex}_{direction}.png", transparent_color=(128, 128, 128), surface=False)
|
||||
self.rat_assets_textures[sex][direction] = texture
|
||||
# Cache size to avoid get_image_size() calls in draw loop
|
||||
self.rat_image_sizes[sex][direction] = texture.size
|
||||
|
||||
for n in range(5):
|
||||
self.bomb_assets[n] = self.render_engine.load_image(f"Rat/BMP_BOMB{n}.png", transparent_color=(128, 128, 128))
|
||||
self.assets = {}
|
||||
@@ -23,6 +32,18 @@ class Graphics():
|
||||
if file.endswith(".png"):
|
||||
self.assets[file[:-4]] = self.render_engine.load_image(f"Rat/{file}", transparent_color=(128, 128, 128))
|
||||
|
||||
# Pre-generate blood stain textures pool (optimization)
|
||||
print("Pre-generating blood stain pool...")
|
||||
self.blood_stain_textures = []
|
||||
for _ in range(10):
|
||||
blood_surface = self.render_engine.generate_blood_surface()
|
||||
blood_texture = self.render_engine.draw_blood_surface(blood_surface, (0, 0))
|
||||
if blood_texture:
|
||||
self.blood_stain_textures.append(blood_texture)
|
||||
|
||||
# Blood layer sprites (instead of regenerating background)
|
||||
self.blood_layer_sprites = []
|
||||
|
||||
|
||||
|
||||
# ==================== RENDERING ====================
|
||||
@@ -32,9 +53,17 @@ class Graphics():
|
||||
print("Generating background texture")
|
||||
self.regenerate_background()
|
||||
self.render_engine.draw_background(self.background_texture)
|
||||
|
||||
# Draw blood layer as sprites (optimized - no background regeneration)
|
||||
self.draw_blood_layer()
|
||||
|
||||
def draw_blood_layer(self):
|
||||
"""Draw all blood stains as sprites overlay (optimized)"""
|
||||
for blood_texture, x, y in self.blood_layer_sprites:
|
||||
self.render_engine.draw_image(x, y, blood_texture, tag="blood")
|
||||
|
||||
def regenerate_background(self):
|
||||
"""Generate or regenerate the background texture with all permanent elements"""
|
||||
"""Generate or regenerate the background texture (static - no blood stains)"""
|
||||
texture_tiles = []
|
||||
for y, row in enumerate(self.map.matrix):
|
||||
for x, cell in enumerate(row):
|
||||
@@ -42,37 +71,23 @@ class Graphics():
|
||||
tile = self.grasses[variant] if cell else self.tunnel
|
||||
texture_tiles.append((tile, x*self.cell_size, y*self.cell_size))
|
||||
|
||||
# Add blood stains if any exist
|
||||
if hasattr(self, 'blood_stains'):
|
||||
for position, blood_surface in self.blood_stains.items():
|
||||
texture_tiles.append((blood_surface, position[0]*self.cell_size, position[1]*self.cell_size))
|
||||
|
||||
# Blood stains now handled separately as overlay layer
|
||||
self.background_texture = self.render_engine.create_texture(texture_tiles)
|
||||
|
||||
def add_blood_stain(self, position):
|
||||
"""Add a blood stain to the background at the specified position"""
|
||||
if not hasattr(self, 'blood_stains'):
|
||||
self.blood_stains = {}
|
||||
"""Add a blood stain as sprite overlay (optimized - no background regeneration)"""
|
||||
import random
|
||||
|
||||
# Generate new blood surface
|
||||
new_blood_surface = self.render_engine.generate_blood_surface()
|
||||
# Pick random blood texture from pre-generated pool
|
||||
if not self.blood_stain_textures:
|
||||
return
|
||||
|
||||
if position in self.blood_stains:
|
||||
# If there's already a blood stain at this position, combine them
|
||||
existing_surface = self.blood_stains[position]
|
||||
combined_surface = self.render_engine.combine_blood_surfaces(existing_surface, new_blood_surface)
|
||||
|
||||
# Free the old surfaces
|
||||
self.render_engine.free_surface(existing_surface)
|
||||
self.render_engine.free_surface(new_blood_surface)
|
||||
|
||||
self.blood_stains[position] = combined_surface
|
||||
else:
|
||||
# First blood stain at this position
|
||||
self.blood_stains[position] = new_blood_surface
|
||||
blood_texture = random.choice(self.blood_stain_textures)
|
||||
x = position[0] * self.cell_size
|
||||
y = position[1] * self.cell_size
|
||||
|
||||
# Regenerate background to include the updated blood stain
|
||||
self.background_texture = None
|
||||
# Add to blood layer sprites instead of regenerating background
|
||||
self.blood_layer_sprites.append((blood_texture, x, y))
|
||||
|
||||
def scroll_cursor(self, x=0, y=0):
|
||||
if self.pointer[0] + x > self.map.width or self.pointer[1] + y > self.map.height:
|
||||
|
||||
+55
-29
@@ -31,6 +31,9 @@ class GameWindow:
|
||||
self.max_h_offset = self.target_size[1] - self.height
|
||||
self.scale = self.target_size[1] // self.cell_size
|
||||
|
||||
# Cached viewport bounds for fast visibility checks
|
||||
self._update_viewport_bounds()
|
||||
|
||||
print(f"Screen size: {self.width}x{self.height}")
|
||||
|
||||
# SDL2 initialization
|
||||
@@ -305,6 +308,13 @@ class GameWindow:
|
||||
# VIEW & NAVIGATION
|
||||
# ======================
|
||||
|
||||
def _update_viewport_bounds(self):
|
||||
"""Update cached viewport bounds for fast visibility checks"""
|
||||
self.visible_x_min = -self.w_offset - self.cell_size
|
||||
self.visible_x_max = self.width - self.w_offset
|
||||
self.visible_y_min = -self.h_offset - self.cell_size
|
||||
self.visible_y_max = self.height - self.h_offset
|
||||
|
||||
def scroll_view(self, pointer):
|
||||
"""Adjust the view offset based on pointer coordinates"""
|
||||
x, y = pointer
|
||||
@@ -323,11 +333,14 @@ class GameWindow:
|
||||
|
||||
self.w_offset = x
|
||||
self.h_offset = y
|
||||
|
||||
# Update cached bounds when viewport changes
|
||||
self._update_viewport_bounds()
|
||||
|
||||
def is_in_visible_area(self, x, y):
|
||||
"""Check if coordinates are within the visible area"""
|
||||
return (-self.w_offset - self.cell_size <= x <= self.width - self.w_offset and
|
||||
-self.h_offset - self.cell_size <= y <= self.height - self.h_offset)
|
||||
"""Check if coordinates are within the visible area (optimized with cached bounds)"""
|
||||
return (self.visible_x_min <= x <= self.visible_x_max and
|
||||
self.visible_y_min <= y <= self.visible_y_max)
|
||||
|
||||
def get_view_center(self):
|
||||
"""Get the center coordinates of the current view"""
|
||||
@@ -531,10 +544,10 @@ class GameWindow:
|
||||
# ======================
|
||||
|
||||
def generate_blood_surface(self):
|
||||
"""Generate a dynamic blood splatter surface using SDL2"""
|
||||
"""Generate a dynamic blood splatter surface using SDL2 with transparency"""
|
||||
size = self.cell_size
|
||||
|
||||
# Create RGBA surface for blood splatter
|
||||
# Create RGBA surface for blood splatter with proper alpha channel
|
||||
blood_surface = sdl2.SDL_CreateRGBSurface(
|
||||
0, size, size, 32,
|
||||
0x000000FF, # R mask
|
||||
@@ -545,6 +558,13 @@ class GameWindow:
|
||||
|
||||
if not blood_surface:
|
||||
return None
|
||||
|
||||
# Enable alpha blending for the surface
|
||||
sdl2.SDL_SetSurfaceBlendMode(blood_surface, sdl2.SDL_BLENDMODE_BLEND)
|
||||
|
||||
# Fill with transparent color first
|
||||
sdl2.SDL_FillRect(blood_surface, None,
|
||||
sdl2.SDL_MapRGBA(blood_surface.contents.format, 0, 0, 0, 0))
|
||||
|
||||
# Lock surface for pixel manipulation
|
||||
sdl2.SDL_LockSurface(blood_surface)
|
||||
@@ -553,13 +573,13 @@ class GameWindow:
|
||||
pixels = cast(blood_surface.contents.pixels, POINTER(c_uint32))
|
||||
pitch = blood_surface.contents.pitch // 4 # Convert pitch to pixels (32-bit)
|
||||
|
||||
# Blood color variations (ABGR format)
|
||||
# Blood color variations (RGBA format for proper alpha)
|
||||
blood_colors = [
|
||||
0xFF00008B, # Dark red
|
||||
0xFF002222, # Brick red
|
||||
0xFF003C14, # Crimson
|
||||
0xFF0000FF, # Pure red
|
||||
0xFF000080, # Reddish brown
|
||||
(139, 0, 0), # Dark red
|
||||
(178, 34, 34), # Firebrick
|
||||
(160, 0, 0), # Dark red
|
||||
(200, 0, 0), # Red
|
||||
(128, 0, 0), # Maroon
|
||||
]
|
||||
|
||||
# Generate splatter with diffusion algorithm
|
||||
@@ -581,13 +601,14 @@ class GameWindow:
|
||||
|
||||
if random.random() < probability * noise:
|
||||
# Choose random blood color
|
||||
color = random.choice(blood_colors)
|
||||
r, g, b = random.choice(blood_colors)
|
||||
|
||||
# Add alpha variation for transparency
|
||||
alpha = int(255 * probability * random.uniform(0.6, 1.0))
|
||||
color = (color & 0x00FFFFFF) | (alpha << 24)
|
||||
|
||||
pixels[y * pitch + x] = color
|
||||
# Pack RGBA into uint32 (ABGR format for SDL)
|
||||
pixel_color = (alpha << 24) | (b << 16) | (g << 8) | r
|
||||
pixels[y * pitch + x] = pixel_color
|
||||
else:
|
||||
# Transparent pixel
|
||||
pixels[y * pitch + x] = 0x00000000
|
||||
@@ -607,10 +628,12 @@ class GameWindow:
|
||||
nx, ny = drop_x + dx, drop_y + dy
|
||||
if 0 <= nx < size and 0 <= ny < size:
|
||||
if random.random() < 0.6:
|
||||
color = random.choice(blood_colors[:3]) # Darker colors for drops
|
||||
r, g, b = random.choice(blood_colors[:3]) # Darker colors for drops
|
||||
alpha = random.randint(100, 200)
|
||||
color = (color & 0x00FFFFFF) | (alpha << 24)
|
||||
pixels[ny * pitch + nx] = color
|
||||
|
||||
# Pack RGBA into uint32 (ABGR format for SDL)
|
||||
pixel_color = (alpha << 24) | (b << 16) | (g << 8) | r
|
||||
pixels[ny * pitch + nx] = pixel_color
|
||||
|
||||
# Unlock surface
|
||||
sdl2.SDL_UnlockSurface(blood_surface)
|
||||
@@ -618,21 +641,24 @@ class GameWindow:
|
||||
return blood_surface
|
||||
|
||||
def draw_blood_surface(self, blood_surface, position):
|
||||
"""Convert blood surface to texture and return it"""
|
||||
# Create temporary surface for blood texture
|
||||
temp_surface = sdl2.SDL_CreateRGBSurface(0, self.cell_size, self.cell_size, 32, 0, 0, 0, 0)
|
||||
if temp_surface is None:
|
||||
"""Convert blood surface to texture with proper alpha blending"""
|
||||
# Create texture directly from renderer
|
||||
texture_ptr = sdl2.SDL_CreateTextureFromSurface(self.renderer.renderer, blood_surface)
|
||||
|
||||
if texture_ptr:
|
||||
# Enable alpha blending
|
||||
sdl2.SDL_SetTextureBlendMode(texture_ptr, sdl2.SDL_BLENDMODE_BLEND)
|
||||
|
||||
# Wrap in sprite for compatibility
|
||||
sprite = sdl2.ext.TextureSprite(texture_ptr)
|
||||
|
||||
# Free the surface
|
||||
sdl2.SDL_FreeSurface(blood_surface)
|
||||
return None
|
||||
|
||||
return sprite
|
||||
|
||||
# Copy blood surface to temporary surface
|
||||
sdl2.SDL_BlitSurface(blood_surface, None, temp_surface, None)
|
||||
sdl2.SDL_FreeSurface(blood_surface)
|
||||
|
||||
# Create texture from temporary surface
|
||||
texture = self.factory.from_surface(temp_surface)
|
||||
sdl2.SDL_FreeSurface(temp_surface)
|
||||
return texture
|
||||
return None
|
||||
|
||||
def combine_blood_surfaces(self, existing_surface, new_surface):
|
||||
"""Combine two blood surfaces by blending them together"""
|
||||
|
||||
+23
-1
@@ -4,7 +4,16 @@ from units import gas, rat, bomb, mine
|
||||
|
||||
|
||||
|
||||
class UnitManager:
|
||||
class UnitManager:
|
||||
def has_weapon_at(self, position):
|
||||
"""Check if there's a weapon (bomb, gas, mine) at the given position"""
|
||||
for unit in self.units.values():
|
||||
if unit.position == position:
|
||||
# Check if it's a weapon type (not a rat or points)
|
||||
if isinstance(unit, (bomb.Timer, bomb.NuclearBomb, gas.Gas, mine.Mine)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def count_rats(self):
|
||||
count = 0
|
||||
for unit in self.units.values():
|
||||
@@ -24,6 +33,19 @@ class UnitManager:
|
||||
def spawn_rat(self, position=None):
|
||||
if position is None:
|
||||
position = self.choose_start()
|
||||
|
||||
# Don't spawn rats on top of weapons
|
||||
if self.has_weapon_at(position):
|
||||
# Try nearby positions
|
||||
for dx, dy in [(0,1), (1,0), (0,-1), (-1,0), (1,1), (-1,-1), (1,-1), (-1,1)]:
|
||||
alt_pos = (position[0] + dx, position[1] + dy)
|
||||
if not self.map.is_wall(alt_pos[0], alt_pos[1]) and not self.has_weapon_at(alt_pos):
|
||||
position = alt_pos
|
||||
break
|
||||
else:
|
||||
# All nearby positions blocked, abort spawn
|
||||
return
|
||||
|
||||
rat_class = rat.Male if random.random() < 0.5 else rat.Female
|
||||
self.spawn_unit(rat_class, position)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user