major UI improvements the UI doesn't tank FPS.
there has been a lot of changes and fixes currently refactoring the code base and optimizing - Added UI optimizations to improve performance and reduce FPS impact - Implemented various bug fixes and improvements - Started code refactoring for better maintainability - Updated particle system configuration in particles.json - Modified rendering pipeline for better efficiency - Updated simulation core logic in sim.py - Adjusted settings and configuration parameters - Updated gitignore rules - Fixed initialization code in __init__.py
This commit is contained in:
parent
28653ec606
commit
b2187693d7
4
.gitignore
vendored
4
.gitignore
vendored
@ -172,4 +172,6 @@ sandpypi.onefile-build/
|
|||||||
sandpypi.exe
|
sandpypi.exe
|
||||||
sandpypi.7z
|
sandpypi.7z
|
||||||
unittest/
|
unittest/
|
||||||
.7z
|
.7z
|
||||||
|
.zip
|
||||||
|
livenotes.txt
|
||||||
@ -1 +1,2 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|||||||
@ -5,11 +5,12 @@
|
|||||||
"hardness": 0.5,
|
"hardness": 0.5,
|
||||||
"color": [255, 255, 0, 255],
|
"color": [255, 255, 0, 255],
|
||||||
"velocity": 0.5,
|
"velocity": 0.5,
|
||||||
|
"wind": 1,
|
||||||
"mass": 0.5,
|
"mass": 0.5,
|
||||||
"conductivity": 0,
|
"conductivity": 0,
|
||||||
"heat_capacity": 1,
|
"heat_capacity": 1,
|
||||||
"flamability": 0.8,
|
"flamability": 0.8,
|
||||||
"temperature": 0,
|
"temperature": 20,
|
||||||
"explosive": false,
|
"explosive": false,
|
||||||
"explosion_radius": 0,
|
"explosion_radius": 0,
|
||||||
"explosion_color": [0, 0, 0],
|
"explosion_color": [0, 0, 0],
|
||||||
@ -79,7 +80,7 @@
|
|||||||
"velocity": 0.0,
|
"velocity": 0.0,
|
||||||
"conductivity": 0,
|
"conductivity": 0,
|
||||||
"heat_capacity": 0,
|
"heat_capacity": 0,
|
||||||
"color": [75, 75, 75, 255],
|
"color": [75, 75, 170, 255],
|
||||||
"mass": 1,
|
"mass": 1,
|
||||||
"flamability": 0.0,
|
"flamability": 0.0,
|
||||||
"temperature": 0,
|
"temperature": 0,
|
||||||
@ -104,7 +105,7 @@
|
|||||||
"color": [139, 69, 19, 255],
|
"color": [139, 69, 19, 255],
|
||||||
"mass": 0.5,
|
"mass": 0.5,
|
||||||
"flamability": 0,
|
"flamability": 0,
|
||||||
"temperature": 0,
|
"temperature": 20,
|
||||||
"explosive": false,
|
"explosive": false,
|
||||||
"explosion_radius": 0,
|
"explosion_radius": 0,
|
||||||
"explosion_color": [0, 0, 0],
|
"explosion_color": [0, 0, 0],
|
||||||
@ -165,7 +166,7 @@
|
|||||||
"color": [75, 75, 75, 255],
|
"color": [75, 75, 75, 255],
|
||||||
"mass": 1,
|
"mass": 1,
|
||||||
"flamability": 0,
|
"flamability": 0,
|
||||||
"temperature": 0,
|
"temperature": 20,
|
||||||
"explosive": false,
|
"explosive": false,
|
||||||
"explosion_radius": 0,
|
"explosion_radius": 0,
|
||||||
"explosion_color": [0, 0, 0],
|
"explosion_color": [0, 0, 0],
|
||||||
@ -185,7 +186,7 @@
|
|||||||
"color": [139, 69, 19, 255],
|
"color": [139, 69, 19, 255],
|
||||||
"mass": 0.5,
|
"mass": 0.5,
|
||||||
"flamability": 0,
|
"flamability": 0,
|
||||||
"temperature": 0,
|
"temperature": 20,
|
||||||
"explosive": false,
|
"explosive": false,
|
||||||
"explosion_radius": 0,
|
"explosion_radius": 0,
|
||||||
"explosion_color": [0, 0, 0],
|
"explosion_color": [0, 0, 0],
|
||||||
@ -202,14 +203,14 @@
|
|||||||
"velocity": 1.5,
|
"velocity": 1.5,
|
||||||
"conductivity": 0,
|
"conductivity": 0,
|
||||||
"heat_capacity": 0,
|
"heat_capacity": 0,
|
||||||
"color": [128, 128, 128, 255],
|
"color": [128, 128, 128, 220],
|
||||||
"mass": 1,
|
"mass": 1,
|
||||||
"flamability": 0,
|
"flamability": 0,
|
||||||
"melt": "molten-Stone",
|
"melt": "molten-Stone",
|
||||||
"melt_temperature": 800,
|
"melt_temperature": 800,
|
||||||
"solidify": "stone",
|
"solidify": "stone",
|
||||||
"solidify_temperature": 799,
|
"solidify_temperature": 799,
|
||||||
"temperature": 0,
|
"temperature": 20,
|
||||||
"explosive": false,
|
"explosive": false,
|
||||||
"explosion_radius": 0,
|
"explosion_radius": 0,
|
||||||
"explosion_color": [0, 0, 0],
|
"explosion_color": [0, 0, 0],
|
||||||
@ -253,9 +254,9 @@
|
|||||||
"flamability": 0.8,
|
"flamability": 0.8,
|
||||||
"burning_temperature": 250,
|
"burning_temperature": 250,
|
||||||
"burning_rate": 0.01,
|
"burning_rate": 0.01,
|
||||||
"burning_color": [255, 0, 0, 255],
|
"burning_color": [255, 69, 19, 255],
|
||||||
"burning": false,
|
"burning": false,
|
||||||
"temperature": 0,
|
"temperature": 20,
|
||||||
"explosive": false,
|
"explosive": false,
|
||||||
"explosion_radius": 0,
|
"explosion_radius": 0,
|
||||||
"explosion_color": [0, 0, 0],
|
"explosion_color": [0, 0, 0],
|
||||||
@ -272,9 +273,9 @@
|
|||||||
"velocity": 0.5,
|
"velocity": 0.5,
|
||||||
"conductivity": 0,
|
"conductivity": 0,
|
||||||
"heat_capacity": 1,
|
"heat_capacity": 1,
|
||||||
"color": [139, 69, 19, 255],
|
"color": [255, 69, 19, 255],
|
||||||
"mass": 0.5,
|
"mass": 0.5,
|
||||||
"flamability": 1,
|
"flamability": 0.8,
|
||||||
"temperature": 251,
|
"temperature": 251,
|
||||||
"burning": true,
|
"burning": true,
|
||||||
"explosive": false,
|
"explosive": false,
|
||||||
@ -293,7 +294,7 @@
|
|||||||
"velocity": 0.0,
|
"velocity": 0.0,
|
||||||
"conductivity": 0,
|
"conductivity": 0,
|
||||||
"heat_capacity": 1,
|
"heat_capacity": 1,
|
||||||
"color": [25, 25, 25, 25],
|
"color": [255, 255, 255, 25],
|
||||||
"mass": 0.0,
|
"mass": 0.0,
|
||||||
"flamability": 0,
|
"flamability": 0,
|
||||||
"temperature": 0,
|
"temperature": 0,
|
||||||
@ -313,7 +314,7 @@
|
|||||||
"velocity": 0.5,
|
"velocity": 0.5,
|
||||||
"conductivity": 0,
|
"conductivity": 0,
|
||||||
"heat_capacity": 1,
|
"heat_capacity": 1,
|
||||||
"color": [255, 45, 24, 255],
|
"color": [255, 45, 60, 255],
|
||||||
"mass": 0.3,
|
"mass": 0.3,
|
||||||
"flamability": 0,
|
"flamability": 0,
|
||||||
"temperature": 1400,
|
"temperature": 1400,
|
||||||
@ -340,7 +341,7 @@
|
|||||||
"flamability": 0,
|
"flamability": 0,
|
||||||
"melt": "molten-rock",
|
"melt": "molten-rock",
|
||||||
"melt_temperature": 600,
|
"melt_temperature": 600,
|
||||||
"temperature": 0,
|
"temperature": 20,
|
||||||
"explosive": false,
|
"explosive": false,
|
||||||
"explosion_radius": 0,
|
"explosion_radius": 0,
|
||||||
"explosion_color": [0, 0, 0],
|
"explosion_color": [0, 0, 0],
|
||||||
@ -438,19 +439,19 @@
|
|||||||
"solid": true,
|
"solid": true,
|
||||||
"is_gas": false,
|
"is_gas": false,
|
||||||
"melt": "molten-glass",
|
"melt": "molten-glass",
|
||||||
"melt_temperature": 1000
|
"melt_temperature": 600
|
||||||
},
|
},
|
||||||
"flame": {
|
"plasma": {
|
||||||
"name": "Flame",
|
"name": "Plasma",
|
||||||
"size": 1,
|
"size": 1,
|
||||||
"hardness": 0.0,
|
"hardness": 0.0,
|
||||||
"velocity": 0.0,
|
"velocity": 0.0,
|
||||||
"conductivity": 0,
|
"conductivity": 0,
|
||||||
"heat_capacity": 1,
|
"heat_capacity": 1,
|
||||||
"color": [255, 100, 0, 255],
|
"color": [255, 100, 200, 255],
|
||||||
"mass": 0.0,
|
"mass": 0.0,
|
||||||
"flamability": 0,
|
"flamability": 0,
|
||||||
"temperature": 1000,
|
"temperature": 3600,
|
||||||
"explosive": false,
|
"explosive": false,
|
||||||
"explosion_radius": 0,
|
"explosion_radius": 0,
|
||||||
"explosion_color": [0, 0, 0],
|
"explosion_color": [0, 0, 0],
|
||||||
|
|||||||
120
rendering.py
120
rendering.py
@ -28,6 +28,7 @@ from settings import pygame, random, particle_properties, engine_settings
|
|||||||
class Rendering:
|
class Rendering:
|
||||||
|
|
||||||
def __init__(self, width, height):
|
def __init__(self, width, height):
|
||||||
|
#self.setup_gpu_rendering()
|
||||||
self.screen = pygame.display.set_mode((width, height))
|
self.screen = pygame.display.set_mode((width, height))
|
||||||
self.background = pygame.Surface((width, height))
|
self.background = pygame.Surface((width, height))
|
||||||
self.background.fill((0, 0, 0))
|
self.background.fill((0, 0, 0))
|
||||||
@ -39,7 +40,13 @@ class Rendering:
|
|||||||
self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA)
|
self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA)
|
||||||
self.cached_fonts = {
|
self.cached_fonts = {
|
||||||
'debug': pygame.font.SysFont(None, 24),
|
'debug': pygame.font.SysFont(None, 24),
|
||||||
'button': pygame.font.SysFont(None, 20)
|
'button': pygame.font.SysFont(None, 20),
|
||||||
|
'slider': pygame.font.SysFont(None, 20),
|
||||||
|
'settings': pygame.font.SysFont(None, 20),
|
||||||
|
'zoom': pygame.font.SysFont(None, 20),
|
||||||
|
'brush_size': pygame.font.SysFont(None, 20),
|
||||||
|
'zoom_window': pygame.font.SysFont(None, 20),
|
||||||
|
'zoom_window_text': pygame.font.SysFont(None, 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Pre-render static UI elements
|
# Pre-render static UI elements
|
||||||
@ -64,7 +71,7 @@ class Rendering:
|
|||||||
self.category_buttons = {}
|
self.category_buttons = {}
|
||||||
self.setup_category_menu()
|
self.setup_category_menu()
|
||||||
self.setup_static_ui()
|
self.setup_static_ui()
|
||||||
|
|
||||||
|
|
||||||
def setup_gpu_rendering(self):
|
def setup_gpu_rendering(self):
|
||||||
# Initialize OpenGL context
|
# Initialize OpenGL context
|
||||||
@ -124,7 +131,15 @@ class Rendering:
|
|||||||
color = particle_colors[particle.particle_type]
|
color = particle_colors[particle.particle_type]
|
||||||
else:
|
else:
|
||||||
color = (255, 255, 255)
|
color = (255, 255, 255)
|
||||||
|
|
||||||
|
if engine_settings['enable_glow']:
|
||||||
|
glow_color = (255, 255, 255)
|
||||||
|
glow_radius = 0.5 * particle_size
|
||||||
|
glow_surface = pygame.Surface((glow_radius * 2, glow_radius * 2), pygame.SRCALPHA)
|
||||||
|
pygame.draw.circle(glow_surface, glow_color, (glow_radius, glow_radius), glow_radius)
|
||||||
|
glow_surface.set_alpha(85)
|
||||||
|
self.particle_surface.blit(glow_surface, (x * particle_size - glow_radius, y * particle_size - glow_radius))
|
||||||
|
|
||||||
if engine_settings['enable_gas_effect']:
|
if engine_settings['enable_gas_effect']:
|
||||||
if particle.is_gas:
|
if particle.is_gas:
|
||||||
# Enhanced gas visibility
|
# Enhanced gas visibility
|
||||||
@ -146,15 +161,6 @@ class Rendering:
|
|||||||
|
|
||||||
pygame.draw.rect(self.particle_surface, color, rect)
|
pygame.draw.rect(self.particle_surface, color, rect)
|
||||||
|
|
||||||
if engine_settings['enable_glow']:
|
|
||||||
glow_color = (255, 255, 255)
|
|
||||||
glow_radius = 0.5 * particle_size
|
|
||||||
glow_surface = pygame.Surface((glow_radius * 2, glow_radius * 2), pygame.SRCALPHA)
|
|
||||||
pygame.draw.circle(glow_surface, glow_color, (glow_radius, glow_radius), glow_radius)
|
|
||||||
glow_surface.set_alpha(85)
|
|
||||||
self.particle_surface.blit(glow_surface, (x * particle_size - glow_radius, y * particle_size - glow_radius))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
self.screen.blit(self.background, (0, 0))
|
self.screen.blit(self.background, (0, 0))
|
||||||
self.screen.blit(self.particle_surface, (0, 0))
|
self.screen.blit(self.particle_surface, (0, 0))
|
||||||
@ -277,42 +283,54 @@ class Rendering:
|
|||||||
self.screen.blit(temperature_surface, (0, 0))
|
self.screen.blit(temperature_surface, (0, 0))
|
||||||
|
|
||||||
|
|
||||||
def draw_debug_overlay(self, fps, particles): # this is the function that draws the debug overlay
|
def draw_debug_overlay(self, fps, sim):
|
||||||
if engine_settings ['enable_fps']:
|
if not engine_settings['enable_fps'] and not engine_settings['enable_debug']:
|
||||||
# Draw FPS
|
return
|
||||||
font = pygame.font.Font(None, 24)
|
|
||||||
fps_text = font.render(f"FPS: {fps:.2f}", True, (255, 255, 255))
|
|
||||||
self.screen.blit(fps_text, (10, 10))
|
|
||||||
|
|
||||||
if engine_settings ['enable_debug']:
|
|
||||||
# Get mouse position and convert to grid coordinates
|
|
||||||
self.debug_surface.fill((0, 0, 0, 0))
|
|
||||||
mouse_x, mouse_y = pygame.mouse.get_pos()
|
|
||||||
grid_x, grid_y = mouse_x // 3, mouse_y // 3
|
|
||||||
|
|
||||||
particle_info = self._get_particle_info(particles, grid_x, grid_y)
|
# Create static debug surface if not exists
|
||||||
|
if not hasattr(self, 'debug_surface'):
|
||||||
# Draw debug information
|
self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA)
|
||||||
font = pygame.font.SysFont(None, 24)
|
|
||||||
particle_count = sum(1 for row in particles for cell in row if cell is not None)
|
# Only update when values change significantly
|
||||||
|
mouse_x, mouse_y = pygame.mouse.get_pos()
|
||||||
|
grid_x, grid_y = mouse_x // 3, mouse_y // 3
|
||||||
|
|
||||||
debug_info = [
|
current_info = (
|
||||||
f"FPS: {int(fps)}",
|
int(fps),
|
||||||
f"Mouse: ({mouse_x}, {mouse_y})",
|
int(sim.track_tps()),
|
||||||
f"Grid: ({grid_x}, {grid_y})",
|
mouse_x // 10, # Reduced update frequency
|
||||||
f"Particle: {particle_info}",
|
mouse_y // 10,
|
||||||
#f"Active Particle Count: {sim.Simulation.get_active_particle_count(sim.Simulation)}",
|
sim.particle_count
|
||||||
f"Particle Count: {particle_count}"
|
)
|
||||||
]
|
|
||||||
|
if not hasattr(self, '_last_debug_info') or current_info != self._last_debug_info:
|
||||||
|
self._last_debug_info = current_info
|
||||||
|
self.debug_surface.fill((0, 0, 0, 0))
|
||||||
|
|
||||||
|
font = self.cached_fonts['debug']
|
||||||
y_offset = 10
|
y_offset = 10
|
||||||
|
|
||||||
for info in debug_info:
|
if engine_settings['enable_fps']:
|
||||||
debug_text = font.render(info, True, (255, 255, 255))
|
fps_surf = font.render(f"FPS: {fps:.1f} | TPS: {sim.track_tps():.1f}", True, (255, 255, 255))
|
||||||
self.screen.blit(debug_text, (10, y_offset))
|
self.debug_surface.blit(fps_surf, (10, y_offset))
|
||||||
y_offset += 25
|
y_offset += 25
|
||||||
self.screen.blit(self.debug_surface, (10, 10))
|
|
||||||
|
if engine_settings['enable_debug']:
|
||||||
|
debug_lines = [
|
||||||
|
f"Mouse: ({mouse_x}, {mouse_y})",
|
||||||
|
f"Grid: ({grid_x}, {grid_y})",
|
||||||
|
f"Particles: {sim.particle_count}"
|
||||||
|
]
|
||||||
|
|
||||||
|
for line in debug_lines:
|
||||||
|
text_surf = font.render(line, True, (255, 255, 255))
|
||||||
|
self.debug_surface.blit(text_surf, (10, y_offset))
|
||||||
|
y_offset += 25
|
||||||
|
|
||||||
|
# Single blit of cached surface
|
||||||
|
self.screen.blit(self.debug_surface, (0, 0))
|
||||||
|
|
||||||
|
|
||||||
def draw_buttons(self): # this is the function that draws the buttons
|
def draw_buttons(self): # this is the function that draws the buttons
|
||||||
self.buttons = {}
|
self.buttons = {}
|
||||||
|
|
||||||
@ -344,7 +362,7 @@ class Rendering:
|
|||||||
button_surface = pygame.Surface((80, 25))
|
button_surface = pygame.Surface((80, 25))
|
||||||
color = self.particle_properties[particle_type].get('color', (255, 255, 255))
|
color = self.particle_properties[particle_type].get('color', (255, 255, 255))
|
||||||
button_surface.fill(color)
|
button_surface.fill(color)
|
||||||
font = pygame.font.SysFont(None, 20)
|
font = self.cached_fonts.get('button')
|
||||||
label = font.render(particle_type, True, (0, 0, 0))
|
label = font.render(particle_type, True, (0, 0, 0))
|
||||||
button_surface.blit(label, (5, 5))
|
button_surface.blit(label, (5, 5))
|
||||||
self.button_surfaces[particle_type] = button_surface
|
self.button_surfaces[particle_type] = button_surface
|
||||||
@ -358,7 +376,7 @@ class Rendering:
|
|||||||
if 'clear' not in self.button_surfaces:
|
if 'clear' not in self.button_surfaces:
|
||||||
clear_surface = pygame.Surface((80, 25))
|
clear_surface = pygame.Surface((80, 25))
|
||||||
clear_surface.fill((255, 0, 0))
|
clear_surface.fill((255, 0, 0))
|
||||||
font = pygame.font.SysFont(None, 24)
|
font = self.cached_fonts.get('button')
|
||||||
label = font.render("Clear", True, (255, 255, 255))
|
label = font.render("Clear", True, (255, 255, 255))
|
||||||
clear_surface.blit(label, (5, 5))
|
clear_surface.blit(label, (5, 5))
|
||||||
self.button_surfaces['clear'] = clear_surface
|
self.button_surfaces['clear'] = clear_surface
|
||||||
@ -370,7 +388,7 @@ class Rendering:
|
|||||||
if 'settings' not in self.button_surfaces:
|
if 'settings' not in self.button_surfaces:
|
||||||
settings_surface = pygame.Surface((80, 25))
|
settings_surface = pygame.Surface((80, 25))
|
||||||
settings_surface.fill((255, 255, 255))
|
settings_surface.fill((255, 255, 255))
|
||||||
font = pygame.font.SysFont(None, 24)
|
font = self.cached_fonts.get('button')
|
||||||
label = font.render("Settings", True, (0, 0, 0))
|
label = font.render("Settings", True, (0, 0, 0))
|
||||||
settings_surface.blit(label, (5, 5))
|
settings_surface.blit(label, (5, 5))
|
||||||
self.button_surfaces['settings'] = settings_surface
|
self.button_surfaces['settings'] = settings_surface
|
||||||
@ -393,7 +411,7 @@ class Rendering:
|
|||||||
pygame.draw.rect(self.screen, (255, 255, 255), (500, 10, 100, 20))
|
pygame.draw.rect(self.screen, (255, 255, 255), (500, 10, 100, 20))
|
||||||
pygame.draw.rect(self.screen, (0, 0, 0), (500, 10, 100, 20), 2)
|
pygame.draw.rect(self.screen, (0, 0, 0), (500, 10, 100, 20), 2)
|
||||||
pygame.draw.rect(self.screen, (255, 0, 0), (500 + brush_size, 10, 100 - brush_size * 2, 20))
|
pygame.draw.rect(self.screen, (255, 0, 0), (500 + brush_size, 10, 100 - brush_size * 2, 20))
|
||||||
label = pygame.font.SysFont(None, 24).render(f"Brush Size: {brush_size}", True, (255, 255, 255))
|
label = self.cached_fonts.get('brush_size').render(f"Brush Size: {brush_size}", True, (255, 255, 255))
|
||||||
self.screen.blit(label, (500, 10))
|
self.screen.blit(label, (500, 10))
|
||||||
|
|
||||||
def draw_settings_menu(self):
|
def draw_settings_menu(self):
|
||||||
@ -401,7 +419,7 @@ class Rendering:
|
|||||||
settings_surface.fill((50, 50, 50))
|
settings_surface.fill((50, 50, 50))
|
||||||
|
|
||||||
y_offset = 10
|
y_offset = 10
|
||||||
font = pygame.font.SysFont(None, 24)
|
font = self.cached_fonts.get('settings')
|
||||||
|
|
||||||
for setting, value in engine_settings.items():
|
for setting, value in engine_settings.items():
|
||||||
# Create toggle button
|
# Create toggle button
|
||||||
@ -416,15 +434,17 @@ class Rendering:
|
|||||||
|
|
||||||
return settings_surface
|
return settings_surface
|
||||||
|
|
||||||
|
|
||||||
def clear_screen(self, sim): # this is the function that clears the screen
|
def clear_screen(self, sim): # this is the function that clears the screen
|
||||||
# Store current particle type
|
# Store current particle type
|
||||||
current_type = sim.current_particle_type
|
current_type = sim.current_particle_type
|
||||||
|
self.particle_count = 0
|
||||||
# Reset simulation grid while preserving particle type
|
# Reset simulation grid while preserving particle type
|
||||||
sim.particles = [[None for _ in range(sim.height)] for _ in range(sim.width)]
|
sim.particles = [[None for _ in range(sim.height)] for _ in range(sim.width)]
|
||||||
sim.active_particles.clear()
|
sim.active_particles.clear()
|
||||||
sim.current_particle_type = current_type
|
sim.current_particle_type = current_type
|
||||||
|
sim.reset_particle_count()
|
||||||
# Clear display surfaces
|
# Clear display surfaces
|
||||||
self.background.fill((0, 0, 0))
|
self.background.fill((0, 0, 0))
|
||||||
self.particle_surface.fill((0, 0, 0, 0))
|
self.particle_surface.fill((0, 0, 0, 0))
|
||||||
|
|
||||||
|
|||||||
262
sandpypi.py
262
sandpypi.py
@ -13,11 +13,147 @@ It handles user input events such as mouse clicks, mouse wheel scrolling, and ke
|
|||||||
It also updates the simulation, draws the particles, buttons, and other UI elements, and manages the settings menu.
|
It also updates the simulation, draws the particles, buttons, and other UI elements, and manages the settings menu.
|
||||||
The main loop runs at a target frame rate of 60 FPS, with the actual frame rate displayed in the debug overlay.
|
The main loop runs at a target frame rate of 60 FPS, with the actual frame rate displayed in the debug overlay.
|
||||||
"""
|
"""
|
||||||
|
import cProfile
|
||||||
|
import pstats
|
||||||
from settings import pygame, engine_settings
|
from settings import pygame, engine_settings
|
||||||
from rendering import Rendering
|
from rendering import Rendering
|
||||||
|
"""
|
||||||
|
This is for the future physics engine until i figure out a better method used for testing right now.
|
||||||
|
#import os
|
||||||
|
#import sys
|
||||||
|
#sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
"""
|
||||||
from sim import Simulation
|
from sim import Simulation
|
||||||
|
|
||||||
def main(): # Main function to run the program
|
def update_simulation(sim, dt, engine_settings):
|
||||||
|
"""Update simulation state"""
|
||||||
|
sim.simulate_step(dt, engine_settings)
|
||||||
|
|
||||||
|
def render_frame(rendering, sim, mouse_pos):
|
||||||
|
"""Render all visual elements"""
|
||||||
|
# Draw particles
|
||||||
|
rendering.draw_particles(sim.particles, sim.active_particles, sim.particle_size, rendering.particle_colors)
|
||||||
|
|
||||||
|
# Draw UI elements
|
||||||
|
rendering.draw_buttons()
|
||||||
|
rendering.draw_brush_size_slider(sim.brush_size)
|
||||||
|
|
||||||
|
# Draw brush cursor
|
||||||
|
mouse_x, mouse_y = mouse_pos
|
||||||
|
rendering.render_brush_cursor(mouse_x, mouse_y, sim.brush_size * sim.particle_size)
|
||||||
|
|
||||||
|
def handle_input(event, sim, rendering, settings_visible, zoom_active, zoom_locked, zoom_pos):
|
||||||
|
"""Handle all input events"""
|
||||||
|
if event.type == pygame.MOUSEBUTTONDOWN:
|
||||||
|
return handle_mouse_down(event, sim, rendering, settings_visible, zoom_active)
|
||||||
|
elif event.type == pygame.MOUSEBUTTONUP:
|
||||||
|
return handle_mouse_up(event)
|
||||||
|
elif event.type == pygame.KEYDOWN:
|
||||||
|
return handle_key_press(event, rendering, sim)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def handle_mouse_down(event, sim, rendering, settings_visible, zoom_active):
|
||||||
|
"""Handle mouse button down events"""
|
||||||
|
mouse_pos = pygame.mouse.get_pos()
|
||||||
|
in_settings_area = False
|
||||||
|
|
||||||
|
if settings_visible:
|
||||||
|
settings_rect = pygame.Rect(rendering.width - 320, 100, 300, 400)
|
||||||
|
in_settings_area = settings_rect.collidepoint(mouse_pos)
|
||||||
|
|
||||||
|
if event.button == 4: # Mouse wheel up
|
||||||
|
sim.brush_size = min(sim.brush_size + 1, sim.max_brush_size)
|
||||||
|
elif event.button == 5: # Mouse wheel down
|
||||||
|
sim.brush_size = max(sim.brush_size - 1, 1)
|
||||||
|
elif event.button == 1: # Left click
|
||||||
|
return handle_left_click(mouse_pos, sim, rendering, settings_visible, in_settings_area, zoom_active)
|
||||||
|
elif event.button == 3: # Right click
|
||||||
|
return {'mouse_down_right': True}
|
||||||
|
elif event.button == 2: # Middle click
|
||||||
|
return {'mouse_down_middle': True}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def handle_left_click(mouse_pos, sim, rendering, settings_visible, in_settings_area, zoom_active):
|
||||||
|
"""Handle left click interactions"""
|
||||||
|
result = {'mouse_down_left': False, 'settings_visible': settings_visible, 'over_button': False}
|
||||||
|
|
||||||
|
if rendering.settings_button.collidepoint(mouse_pos):
|
||||||
|
result['settings_visible'] = not settings_visible
|
||||||
|
result['over_button'] = True
|
||||||
|
return result
|
||||||
|
|
||||||
|
if zoom_active:
|
||||||
|
result['zoom_locked'] = not result.get('zoom_locked', False)
|
||||||
|
if result['zoom_locked']:
|
||||||
|
result['zoom_pos'] = mouse_pos
|
||||||
|
return result
|
||||||
|
|
||||||
|
if settings_visible and in_settings_area:
|
||||||
|
handle_settings_click(mouse_pos, sim)
|
||||||
|
result['over_button'] = True
|
||||||
|
return result
|
||||||
|
|
||||||
|
if not in_settings_area:
|
||||||
|
if handle_ui_click(mouse_pos, sim, rendering):
|
||||||
|
result['over_button'] = True
|
||||||
|
else:
|
||||||
|
result['mouse_down_left'] = True
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def handle_settings_click(mouse_pos, sim):
|
||||||
|
"""Handle clicks in settings menu"""
|
||||||
|
settings_menu_y = 100
|
||||||
|
relative_y = mouse_pos[1] - settings_menu_y
|
||||||
|
setting_index = relative_y // 30
|
||||||
|
if 0 <= setting_index < len(engine_settings):
|
||||||
|
setting_name = list(engine_settings.keys())[setting_index]
|
||||||
|
engine_settings[setting_name] = not engine_settings[setting_name]
|
||||||
|
|
||||||
|
def handle_ui_click(mouse_pos, sim, rendering):
|
||||||
|
"""Handle clicks on UI elements"""
|
||||||
|
for category, button in rendering.category_buttons.items():
|
||||||
|
if button.collidepoint(mouse_pos):
|
||||||
|
rendering.current_category = category
|
||||||
|
return True
|
||||||
|
|
||||||
|
for particle_type, button in rendering.buttons.items():
|
||||||
|
if button.collidepoint(mouse_pos):
|
||||||
|
sim.current_particle_type = particle_type
|
||||||
|
return True
|
||||||
|
|
||||||
|
if rendering.clear_screen_button.collidepoint(mouse_pos):
|
||||||
|
rendering.clear_screen(sim)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def handle_mouse_up(event):
|
||||||
|
"""Handle mouse button up events"""
|
||||||
|
if event.button == 1:
|
||||||
|
return {'mouse_down_left': False}
|
||||||
|
elif event.button == 3:
|
||||||
|
return {'mouse_down_right': False}
|
||||||
|
elif event.button == 2:
|
||||||
|
return {'mouse_down_middle': False}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def handle_key_press(event, rendering, sim):
|
||||||
|
"""Handle keyboard press events"""
|
||||||
|
if event.key == pygame.K_ESCAPE:
|
||||||
|
print("Escape button pressed")
|
||||||
|
print(f"Exiting Program {__file__}")
|
||||||
|
return {'running': False}
|
||||||
|
elif event.key == pygame.K_SPACE:
|
||||||
|
engine_settings['pause_sim'] = not engine_settings['pause_sim']
|
||||||
|
elif event.key == pygame.K_c:
|
||||||
|
rendering.clear_screen(sim)
|
||||||
|
sim.reset_particle_count()
|
||||||
|
elif event.key == pygame.K_z:
|
||||||
|
return {'zoom_active': True, 'zoom_locked': False, 'zoom_pos': pygame.mouse.get_pos()}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def main():
|
||||||
pygame.init()
|
pygame.init()
|
||||||
clock = pygame.time.Clock()
|
clock = pygame.time.Clock()
|
||||||
width = 1024
|
width = 1024
|
||||||
@ -25,36 +161,42 @@ def main(): # Main function to run the program
|
|||||||
screen = pygame.display.set_mode((width, height), pygame.HWSURFACE | pygame.DOUBLEBUF)
|
screen = pygame.display.set_mode((width, height), pygame.HWSURFACE | pygame.DOUBLEBUF)
|
||||||
sim = Simulation(width, height)
|
sim = Simulation(width, height)
|
||||||
rendering = Rendering(width, height)
|
rendering = Rendering(width, height)
|
||||||
|
|
||||||
|
# State variables
|
||||||
mouse_down_left = False
|
mouse_down_left = False
|
||||||
mouse_down_right = False
|
mouse_down_right = False
|
||||||
mouse_down_middle = False
|
mouse_down_middle = False
|
||||||
mouse_down_wheel_up = False
|
|
||||||
mouse_down_wheel_down = False
|
|
||||||
over_button = False
|
over_button = False
|
||||||
zoom_active = False
|
zoom_active = False
|
||||||
zoom_locked = False
|
zoom_locked = False
|
||||||
zoom_pos = None
|
zoom_pos = None
|
||||||
settings_visible = False
|
settings_visible = False
|
||||||
settings_menu_y = 100
|
running = True
|
||||||
running = True
|
|
||||||
|
|
||||||
while running:
|
while running:
|
||||||
fps = clock.get_fps() # Get the current frame rate
|
# Clear screen at start of frame
|
||||||
dt = clock.tick(300) / 1000 # sets the target frame rate to 60 FPS
|
screen.fill((0, 0, 0))
|
||||||
|
|
||||||
|
fps = clock.get_fps()
|
||||||
|
dt = clock.tick(1000) / 1000
|
||||||
keys = pygame.key.get_pressed()
|
keys = pygame.key.get_pressed()
|
||||||
|
mouse_pos = pygame.mouse.get_pos()
|
||||||
zoom_active = keys[pygame.K_z]
|
zoom_active = keys[pygame.K_z]
|
||||||
|
|
||||||
|
|
||||||
# Handle events
|
# Handle events
|
||||||
for event in pygame.event.get():
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
running = False
|
||||||
|
continue
|
||||||
|
|
||||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
if event.type == pygame.MOUSEBUTTONDOWN:
|
||||||
mouse_pos = pygame.mouse.get_pos()
|
mouse_pos = pygame.mouse.get_pos()
|
||||||
# Check if clicking in settings area when menu is visible
|
|
||||||
in_settings_area = False
|
in_settings_area = False
|
||||||
|
|
||||||
if settings_visible:
|
if settings_visible:
|
||||||
settings_rect = pygame.Rect(rendering.width - 320, 100, 300, 400)
|
settings_rect = pygame.Rect(rendering.width - 320, 100, 300, 400)
|
||||||
in_settings_area = settings_rect.collidepoint(mouse_pos)
|
in_settings_area = settings_rect.collidepoint(mouse_pos)
|
||||||
|
|
||||||
if event.button == 4: # Mouse wheel up
|
if event.button == 4: # Mouse wheel up
|
||||||
sim.brush_size = min(sim.brush_size + 1, sim.max_brush_size)
|
sim.brush_size = min(sim.brush_size + 1, sim.max_brush_size)
|
||||||
elif event.button == 5: # Mouse wheel down
|
elif event.button == 5: # Mouse wheel down
|
||||||
@ -62,19 +204,19 @@ def main(): # Main function to run the program
|
|||||||
elif event.button == 1: # Left click
|
elif event.button == 1: # Left click
|
||||||
over_button = False
|
over_button = False
|
||||||
|
|
||||||
# Check settings button first
|
# Check settings button first
|
||||||
if rendering.settings_button.collidepoint(mouse_pos):
|
if rendering.settings_button.collidepoint(mouse_pos):
|
||||||
settings_visible = not settings_visible
|
settings_visible = not settings_visible
|
||||||
over_button = True
|
over_button = True
|
||||||
|
|
||||||
if zoom_active:
|
elif zoom_active:
|
||||||
zoom_locked = not zoom_locked
|
zoom_locked = not zoom_locked
|
||||||
if zoom_locked:
|
if zoom_locked:
|
||||||
zoom_pos = mouse_pos
|
zoom_pos = mouse_pos
|
||||||
|
|
||||||
# Handle settings menu interactions
|
# Handle settings menu interactions
|
||||||
elif settings_visible and in_settings_area:
|
elif settings_visible and in_settings_area:
|
||||||
relative_y = mouse_pos[1] - settings_menu_y
|
relative_y = mouse_pos[1] - 100 # settings_menu_y
|
||||||
setting_index = relative_y // 30
|
setting_index = relative_y // 30
|
||||||
if 0 <= setting_index < len(engine_settings):
|
if 0 <= setting_index < len(engine_settings):
|
||||||
setting_name = list(engine_settings.keys())[setting_index]
|
setting_name = list(engine_settings.keys())[setting_index]
|
||||||
@ -82,7 +224,6 @@ def main(): # Main function to run the program
|
|||||||
over_button = True
|
over_button = True
|
||||||
|
|
||||||
elif not in_settings_area:
|
elif not in_settings_area:
|
||||||
|
|
||||||
# Check category buttons
|
# Check category buttons
|
||||||
for category, button in rendering.category_buttons.items():
|
for category, button in rendering.category_buttons.items():
|
||||||
if button.collidepoint(mouse_pos):
|
if button.collidepoint(mouse_pos):
|
||||||
@ -101,6 +242,7 @@ def main(): # Main function to run the program
|
|||||||
# Check clear screen button
|
# Check clear screen button
|
||||||
if rendering.clear_screen_button.collidepoint(mouse_pos):
|
if rendering.clear_screen_button.collidepoint(mouse_pos):
|
||||||
rendering.clear_screen(sim)
|
rendering.clear_screen(sim)
|
||||||
|
particle_count = 0
|
||||||
over_button = True
|
over_button = True
|
||||||
|
|
||||||
if not over_button and not in_settings_area:
|
if not over_button and not in_settings_area:
|
||||||
@ -108,87 +250,85 @@ def main(): # Main function to run the program
|
|||||||
|
|
||||||
elif event.button == 3: # Right click
|
elif event.button == 3: # Right click
|
||||||
mouse_down_right = True
|
mouse_down_right = True
|
||||||
|
|
||||||
elif event.button == 2: # Middle click
|
elif event.button == 2: # Middle click
|
||||||
mouse_down_middle = True
|
mouse_down_middle = True
|
||||||
|
|
||||||
elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
|
elif event.type == pygame.MOUSEBUTTONUP:
|
||||||
mouse_down_left = False
|
if event.button == 1:
|
||||||
elif event.type == pygame.MOUSEBUTTONUP and event.button == 3:
|
mouse_down_left = False
|
||||||
mouse_down_right = False
|
elif event.button == 3:
|
||||||
elif event.type == pygame.MOUSEBUTTONUP and event.button == 2:
|
mouse_down_right = False
|
||||||
mouse_down_middle = False
|
elif event.button == 2:
|
||||||
|
mouse_down_middle = False
|
||||||
|
|
||||||
elif event.type == pygame.KEYDOWN:
|
elif event.type == pygame.KEYDOWN:
|
||||||
if event.key == pygame.K_ESCAPE:
|
if event.key == pygame.K_ESCAPE:
|
||||||
print("Escape button pressed")
|
|
||||||
print(f"Exiting Program {__file__}")
|
|
||||||
running = False
|
running = False
|
||||||
|
|
||||||
elif event.key == pygame.K_SPACE:
|
elif event.key == pygame.K_SPACE:
|
||||||
print(f"Pause button pressed but not functional 'Space'")
|
engine_settings['pause_sim'] = not engine_settings['pause_sim']
|
||||||
pass
|
|
||||||
|
|
||||||
elif event.key == pygame.K_c:
|
elif event.key == pygame.K_c:
|
||||||
rendering.clear_screen(sim)
|
rendering.clear_screen(sim)
|
||||||
|
|
||||||
elif event.key == pygame.K_z:
|
elif event.key == pygame.K_z:
|
||||||
zoom_active = not zoom_active
|
zoom_active = True
|
||||||
if zoom_active:
|
zoom_locked = False
|
||||||
zoom_locked = False
|
zoom_pos = pygame.mouse.get_pos()
|
||||||
zoom_pos = pygame.mouse.get_pos()
|
|
||||||
|
|
||||||
elif event.type == pygame.QUIT:
|
# Update simulation if not paused
|
||||||
running = False
|
if not engine_settings['pause_sim']:
|
||||||
|
sim.simulate_step(dt, engine_settings)
|
||||||
|
|
||||||
|
# Handle continuous mouse input
|
||||||
if mouse_down_left and not over_button:
|
if mouse_down_left and not over_button:
|
||||||
x, y = pygame.mouse.get_pos()
|
x, y = mouse_pos
|
||||||
# Check if current particle type is wind or air
|
|
||||||
if sim.current_particle_type not in ['wind', 'air']:
|
if sim.current_particle_type not in ['wind', 'air']:
|
||||||
sim.create_particle_circle(x, y)
|
sim.create_particle_circle(x, y)
|
||||||
else:
|
else:
|
||||||
# Handle wind/air differently
|
|
||||||
sim.add_wind_zone(x, y)
|
sim.add_wind_zone(x, y)
|
||||||
|
|
||||||
if mouse_down_right:
|
if mouse_down_right:
|
||||||
x, y = pygame.mouse.get_pos()
|
x, y = mouse_pos
|
||||||
sim.clear_particles_circle(x, y)
|
sim.clear_particles_circle(x, y)
|
||||||
|
|
||||||
if mouse_down_middle:
|
if mouse_down_middle:
|
||||||
x, y = pygame.mouse.get_pos()
|
x, y = mouse_pos
|
||||||
sim.create_particle(x, y)
|
sim.create_particle(x, y)
|
||||||
|
|
||||||
|
# Draw everything in correct order
|
||||||
|
rendering.draw_particles(sim.particles, sim.active_particles, sim.particle_size, rendering.particle_colors)
|
||||||
|
rendering.draw_buttons()
|
||||||
|
rendering.draw_brush_size_slider(sim.brush_size)
|
||||||
|
rendering.render_brush_cursor(mouse_pos[0], mouse_pos[1], sim.brush_size * sim.particle_size)
|
||||||
|
|
||||||
|
# Handle zoom window
|
||||||
if zoom_active or zoom_locked:
|
if zoom_active or zoom_locked:
|
||||||
mouse_pos = zoom_pos if zoom_locked else pygame.mouse.get_pos()
|
current_zoom_pos = zoom_pos if zoom_locked else mouse_pos
|
||||||
zoom_surface = rendering.draw_zoom_window(sim.particles, sim.particle_size, rendering.particle_colors, mouse_pos)
|
zoom_surface = rendering.draw_zoom_window(sim.particles, sim.particle_size,
|
||||||
# Position zoom window
|
rendering.particle_colors, current_zoom_pos)
|
||||||
zoom_x = 10 if mouse_pos[0] > width/2 else width - 110
|
zoom_x = 10 if current_zoom_pos[0] > width/2 else width - 110
|
||||||
zoom_y = 10 if mouse_pos[1] > height/2 else height - 110
|
zoom_y = 10 if current_zoom_pos[1] > height/2 else height - 110
|
||||||
screen.blit(zoom_surface, (zoom_x, zoom_y))
|
screen.blit(zoom_surface, (zoom_x, zoom_y))
|
||||||
|
|
||||||
# Draw Settings
|
# Draw settings and debug overlay last
|
||||||
if settings_visible:
|
if settings_visible:
|
||||||
settings_menu = rendering.draw_settings_menu()
|
settings_menu = rendering.draw_settings_menu()
|
||||||
rendering.screen.blit(settings_menu, (rendering.width - 320, 100))
|
rendering.screen.blit(settings_menu, (rendering.width - 320, 100))
|
||||||
rendering.draw_debug_overlay(fps, sim.particles)
|
|
||||||
pygame.display.flip()
|
|
||||||
|
|
||||||
# Update and draw particles
|
|
||||||
sim.simulate_step(dt=0.016)
|
|
||||||
# Draw particles
|
|
||||||
rendering.draw_particles(sim.particles, sim.active_particles, sim.particle_size, rendering.particle_colors)
|
|
||||||
# Draw buttons
|
|
||||||
rendering.draw_buttons()
|
|
||||||
# Draw brush size slider
|
|
||||||
rendering.draw_brush_size_slider(sim.brush_size)
|
|
||||||
# Get current mouse position
|
|
||||||
mouse_x, mouse_y = pygame.mouse.get_pos()
|
|
||||||
# Draw brush cursor at mouse position
|
|
||||||
rendering.render_brush_cursor(mouse_x, mouse_y, sim.brush_size * sim.particle_size)
|
|
||||||
|
|
||||||
|
rendering.draw_debug_overlay(fps, sim)
|
||||||
|
pygame.display.flip()
|
||||||
|
|
||||||
pygame.quit()
|
pygame.quit()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
# Profile the application
|
||||||
|
profiler = cProfile.Profile()
|
||||||
|
profiler.enable()
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
profiler.disable()
|
||||||
|
# Write profiling results to file
|
||||||
|
with open('profile_results.log', 'w') as f:
|
||||||
|
stats = pstats.Stats(profiler, stream=f)
|
||||||
|
stats.sort_stats('cumulative')
|
||||||
|
stats.print_stats()
|
||||||
|
|||||||
@ -16,9 +16,11 @@ The `particle_properties` variable is initialized by calling `load_particle_prop
|
|||||||
import pygame
|
import pygame
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
|
import time
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
engine_settings = {
|
engine_settings = {
|
||||||
|
'pause_sim': True,
|
||||||
'enable_cursor': True,
|
'enable_cursor': True,
|
||||||
'enable_glow': False,
|
'enable_glow': False,
|
||||||
'enable_gas_effect': True,
|
'enable_gas_effect': True,
|
||||||
@ -27,6 +29,7 @@ engine_settings = {
|
|||||||
'enable_WVisuals': False,
|
'enable_WVisuals': False,
|
||||||
'enable_PVisuals': False,
|
'enable_PVisuals': False,
|
||||||
'enable_TempVisuals': False,
|
'enable_TempVisuals': False,
|
||||||
|
'outerwall': True,
|
||||||
# 'settings': True/False
|
# 'settings': True/False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
255
sim.py
255
sim.py
@ -19,7 +19,7 @@ Key Components:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
#Load the imports. Pygame is what makes this even work and so simple may consider other engines for performance depends on learning curve.
|
#Load the imports. Pygame is what makes this even work and so simple may consider other engines for performance depends on learning curve.
|
||||||
from settings import random, particle_properties
|
from settings import random, time, particle_properties
|
||||||
|
|
||||||
# Load particle properties from json so we know what particles we got and how they should be simulated.
|
# Load particle properties from json so we know what particles we got and how they should be simulated.
|
||||||
class Particle:
|
class Particle:
|
||||||
@ -84,7 +84,8 @@ class Simulation:
|
|||||||
self.width = width
|
self.width = width
|
||||||
self.height = height
|
self.height = height
|
||||||
self.particle_size = 3
|
self.particle_size = 3
|
||||||
self.particles = [[None for _ in range(height)] for _ in range(width)]
|
self.particles = [[None for _ in range(height)] for _ in range(width)]
|
||||||
|
self.particle_count = 0
|
||||||
self.active_particles = set()
|
self.active_particles = set()
|
||||||
self.cell_size = 32
|
self.cell_size = 32
|
||||||
self.spatial_grid = {}
|
self.spatial_grid = {}
|
||||||
@ -96,6 +97,9 @@ class Simulation:
|
|||||||
self.wind_zones = []
|
self.wind_zones = []
|
||||||
self.wind = [0.0, 0.0] # Global wind vector (x, y)
|
self.wind = [0.0, 0.0] # Global wind vector (x, y)
|
||||||
|
|
||||||
|
def reset_particle_count(self):
|
||||||
|
self.particle_count = 0
|
||||||
|
|
||||||
|
|
||||||
def get_cell_key(self, x, y): # this is where we get the cell key.
|
def get_cell_key(self, x, y): # this is where we get the cell key.
|
||||||
# Convert coordinates to grid cell
|
# Convert coordinates to grid cell
|
||||||
@ -162,25 +166,29 @@ class Simulation:
|
|||||||
if particle.temperature >= particle.evaporate_temperature and particle.evaporate:
|
if particle.temperature >= particle.evaporate_temperature and particle.evaporate:
|
||||||
self.transform_particle(x, y, particle.evaporate)
|
self.transform_particle(x, y, particle.evaporate)
|
||||||
|
|
||||||
# Check melting
|
|
||||||
if hasattr(particle, 'melt_temperature') and particle.melt_temperature is not None:
|
|
||||||
if particle.temperature >= particle.melt_temperature and particle.melt:
|
|
||||||
self.transform_particle(x, y, particle.melt)
|
|
||||||
|
|
||||||
# Check freezing
|
# Check freezing
|
||||||
if hasattr(particle, 'freeze_temperature') and particle.freeze_temperature is not None:
|
if hasattr(particle, 'freeze_temperature') and particle.freeze_temperature is not None:
|
||||||
if particle.temperature <= particle.freeze_temperature and particle.freeze:
|
if particle.temperature <= particle.freeze_temperature and particle.freeze:
|
||||||
self.transform_particle(x, y, particle.freeze)
|
self.transform_particle(x, y, particle.freeze)
|
||||||
|
|
||||||
# Check solidification
|
# Check for melting with proper attribute validation
|
||||||
if hasattr(particle, 'solidify_temperature') and particle.solidify_temperature is not None:
|
if (hasattr(particle, 'melt') and hasattr(particle, 'melt_temperature')
|
||||||
if particle.temperature <= particle.solidify_temperature and particle.solidify:
|
and particle.melt_temperature is not None):
|
||||||
self.transform_particle(x, y, particle.solidify)
|
if particle.temperature >= particle.melt_temperature:
|
||||||
|
new_type = particle.melt
|
||||||
|
if new_type in self.particle_properties:
|
||||||
|
self.transform_particle(x, y, new_type)
|
||||||
|
|
||||||
|
# Check for solidification with proper attribute validation
|
||||||
|
if (hasattr(particle, 'solidify') and hasattr(particle, 'solidify_temperature')
|
||||||
|
and particle.solidify_temperature is not None):
|
||||||
|
if particle.temperature <= particle.solidify_temperature:
|
||||||
|
new_type = particle.solidify
|
||||||
|
if new_type in self.particle_properties:
|
||||||
|
self.transform_particle(x, y, new_type)
|
||||||
|
|
||||||
def handle_particle_interactions(self, dt): # this is where we handle all the particle interactions.
|
def handle_particle_interactions(self, dt): # this is where we handle all the particle interactions.
|
||||||
"""Handle interactions between different particle types"""
|
"""Handle interactions between different particle types"""
|
||||||
self.update_spatial_grid()
|
|
||||||
for x, y in list(self.active_particles):
|
for x, y in list(self.active_particles):
|
||||||
particle = self.particles[x][y]
|
particle = self.particles[x][y]
|
||||||
if not particle:
|
if not particle:
|
||||||
@ -252,22 +260,6 @@ class Simulation:
|
|||||||
self.active_particles.add((new_x, new_y))
|
self.active_particles.add((new_x, new_y))
|
||||||
self.active_particles.discard((x, y))
|
self.active_particles.discard((x, y))
|
||||||
|
|
||||||
|
|
||||||
def temperature(self, dt): # this is where we handle the temperature.
|
|
||||||
"""Handle temperature changes and state transitions"""
|
|
||||||
for x, y in list(self.active_particles):
|
|
||||||
particle = self.particles[x][y]
|
|
||||||
if not particle:
|
|
||||||
continue
|
|
||||||
if particle.temperature > 100:
|
|
||||||
# Transition to gas
|
|
||||||
particle.is_gas = True
|
|
||||||
particle.temperature = 100
|
|
||||||
particle.velocity = [random.uniform(-1, 1), random.uniform(-1, 1)]
|
|
||||||
particle.temperature < 100
|
|
||||||
particle.is_gas = False
|
|
||||||
|
|
||||||
|
|
||||||
def add_wind_zone(self, x, y):
|
def add_wind_zone(self, x, y):
|
||||||
# Instead of creating particles, store wind zone data
|
# Instead of creating particles, store wind zone data
|
||||||
wind_zone = {
|
wind_zone = {
|
||||||
@ -316,6 +308,53 @@ class Simulation:
|
|||||||
|
|
||||||
|
|
||||||
return fx, fy
|
return fx, fy
|
||||||
|
def _process_particle_batch(self, batch, dt):
|
||||||
|
updates = []
|
||||||
|
new_active = set()
|
||||||
|
|
||||||
|
# Filter out dormant particles from the batch
|
||||||
|
active_batch = [pos for pos in batch if pos not in self.dormant_particles]
|
||||||
|
|
||||||
|
for x, y in active_batch:
|
||||||
|
particle = self.particles[x][y]
|
||||||
|
if not particle:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if particle.particle_type == 'wall':
|
||||||
|
new_active.add((x, y))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if particle should become dormant
|
||||||
|
if self._check_dormant_state(x, y, particle):
|
||||||
|
new_active.add((x, y))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# physics calculations
|
||||||
|
fx, fy = self.calculate_forces(particle, x, y)
|
||||||
|
# Use max() to ensure mass is never zero
|
||||||
|
mass = max(particle.mass, 0.001)
|
||||||
|
particle.velocity[0] += (fx / mass) * dt
|
||||||
|
particle.velocity[1] += (fy / mass) * dt
|
||||||
|
|
||||||
|
new_x = int(x + particle.velocity[0] * dt)
|
||||||
|
new_y = int(y + particle.velocity[1] * dt)
|
||||||
|
|
||||||
|
if 0 <= new_x < self.width and 0 <= new_y < self.height:
|
||||||
|
if self.particles[new_x][new_y] is None:
|
||||||
|
updates.append((x, y, new_x, new_y, particle))
|
||||||
|
new_active.add((new_x, new_y))
|
||||||
|
# Wake up neighboring dormant particles
|
||||||
|
self._wake_neighbors(new_x, new_y)
|
||||||
|
else:
|
||||||
|
new_active.add((x, y))
|
||||||
|
|
||||||
|
# Apply updates and return new active set
|
||||||
|
for old_x, old_y, new_x, new_y, particle in updates:
|
||||||
|
self.particles[old_x][old_y] = None
|
||||||
|
self.particles[new_x][new_y] = particle
|
||||||
|
particle.position = (new_x, new_y)
|
||||||
|
|
||||||
|
return new_active
|
||||||
|
|
||||||
def _get_quick_neighbors(self, x, y):
|
def _get_quick_neighbors(self, x, y):
|
||||||
"""Quick neighbor lookup without full spatial grid"""
|
"""Quick neighbor lookup without full spatial grid"""
|
||||||
@ -393,22 +432,13 @@ class Simulation:
|
|||||||
particle = self.particles[x][y]
|
particle = self.particles[x][y]
|
||||||
if not particle:
|
if not particle:
|
||||||
continue
|
continue
|
||||||
|
if particle.temperature > 1700:
|
||||||
# Check for melting with proper attribute validation
|
# Transition to gas
|
||||||
if (hasattr(particle, 'melt') and hasattr(particle, 'melt_temperature')
|
particle.is_gas = True
|
||||||
and particle.melt_temperature is not None):
|
particle.temperature = 1700
|
||||||
if particle.temperature >= particle.melt_temperature:
|
particle.velocity = [random.uniform(-1, 1), random.uniform(-1, 1)]
|
||||||
new_type = particle.melt
|
particle.temperature < 1400
|
||||||
if new_type in self.particle_properties:
|
particle.is_gas = False
|
||||||
self.transform_particle(x, y, new_type)
|
|
||||||
|
|
||||||
# Check for solidification with proper attribute validation
|
|
||||||
if (hasattr(particle, 'solidify') and hasattr(particle, 'solidify_temperature')
|
|
||||||
and particle.solidify_temperature is not None):
|
|
||||||
if particle.temperature <= particle.solidify_temperature:
|
|
||||||
new_type = particle.solidify
|
|
||||||
if new_type in self.particle_properties:
|
|
||||||
self.transform_particle(x, y, new_type)
|
|
||||||
|
|
||||||
# Temperature spread to neighbors
|
# Temperature spread to neighbors
|
||||||
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||||
@ -438,9 +468,11 @@ class Simulation:
|
|||||||
def create_particle(self, x, y): # this is where we create the particle.
|
def create_particle(self, x, y): # this is where we create the particle.
|
||||||
"""Create a new particle with full property support"""
|
"""Create a new particle with full property support"""
|
||||||
particle_type = self.current_particle_type.lower()
|
particle_type = self.current_particle_type.lower()
|
||||||
|
# Check if the particle is within the grid boundaries
|
||||||
if particle_type in self.particle_properties:
|
if particle_type in self.particle_properties:
|
||||||
grid_x = x // self.particle_size
|
grid_x = x // self.particle_size
|
||||||
grid_y = y // self.particle_size
|
grid_y = y // self.particle_size
|
||||||
|
|
||||||
if 0 <= grid_x < self.width and 0 <= grid_y < self.height:
|
if 0 <= grid_x < self.width and 0 <= grid_y < self.height:
|
||||||
properties = self.particle_properties[particle_type]
|
properties = self.particle_properties[particle_type]
|
||||||
position = (grid_x, grid_y)
|
position = (grid_x, grid_y)
|
||||||
@ -451,10 +483,11 @@ class Simulation:
|
|||||||
particle_type=particle_type,
|
particle_type=particle_type,
|
||||||
properties=properties
|
properties=properties
|
||||||
)
|
)
|
||||||
|
# Add to the grid
|
||||||
if 0 <= grid_x < len(self.particles) and 0 <= grid_y < len(self.particles[0]):
|
if 0 <= grid_x < len(self.particles) and 0 <= grid_y < len(self.particles[0]):
|
||||||
self.particles[grid_x][grid_y] = new_particle
|
self.particles[grid_x][grid_y] = new_particle
|
||||||
self.active_particles.add((grid_x, grid_y))
|
self.active_particles.add((grid_x, grid_y))
|
||||||
|
self.particle_count += 1
|
||||||
|
|
||||||
|
|
||||||
def create_particle_circle(self, center_x, center_y): # this is where we create the particle circle.
|
def create_particle_circle(self, center_x, center_y): # this is where we create the particle circle.
|
||||||
@ -462,10 +495,16 @@ class Simulation:
|
|||||||
for dx in range(-brush_size, brush_size + 1):
|
for dx in range(-brush_size, brush_size + 1):
|
||||||
for dy in range(-brush_size, brush_size + 1):
|
for dy in range(-brush_size, brush_size + 1):
|
||||||
if dx*dx + dy*dy <= brush_size*brush_size: # Circle check
|
if dx*dx + dy*dy <= brush_size*brush_size: # Circle check
|
||||||
self.create_particle(center_x + dx * self.particle_size,
|
self.create_particle(center_x + dx * self.particle_size, center_y + dy * self.particle_size)
|
||||||
center_y + dy * self.particle_size)
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_particle_state(self, x, y): # this is where we get the particle state.
|
||||||
|
"""Get the state of a particle at a given position"""
|
||||||
|
particle = self.particles[x][y]
|
||||||
|
if particle:
|
||||||
|
return particle.particle_type
|
||||||
|
return None
|
||||||
|
|
||||||
def apply_gravity(self, dt): # this is where we apply gravity.
|
def apply_gravity(self, dt): # this is where we apply gravity.
|
||||||
"""Handle only gravity and basic particle movement"""
|
"""Handle only gravity and basic particle movement"""
|
||||||
self.spatial_grid.clear()
|
self.spatial_grid.clear()
|
||||||
@ -484,7 +523,7 @@ class Simulation:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Handle granular materials (sand, dirt)
|
# Handle granular materials (sand, dirt)
|
||||||
if particle.particle_type in ['sand', 'dirt']:
|
if particle.particle_type in ['sand', 'dirt', 'snow', 'ice']:
|
||||||
if self.particles[x][new_y] is None:
|
if self.particles[x][new_y] is None:
|
||||||
new_x, new_y = x, y + 1
|
new_x, new_y = x, y + 1
|
||||||
else:
|
else:
|
||||||
@ -526,7 +565,7 @@ class Simulation:
|
|||||||
particle.position = (new_x, new_y)
|
particle.position = (new_x, new_y)
|
||||||
|
|
||||||
|
|
||||||
def apply_physics(self, dt): # this is where we apply physics.
|
def apply_physics(self, dt, engine_settings): # this is where we apply physics.
|
||||||
"""Handle all physics effects"""
|
"""Handle all physics effects"""
|
||||||
new_active_particles = set()
|
new_active_particles = set()
|
||||||
|
|
||||||
@ -535,6 +574,27 @@ class Simulation:
|
|||||||
if not particle:
|
if not particle:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Handle boundaries based on settings
|
||||||
|
if engine_settings['outerwall']:
|
||||||
|
if x <= 0 or x >= self.width-1 or y <= 0 or y >= self.height-1:
|
||||||
|
# Create wall particle at boundary if none exists
|
||||||
|
if self.particles[x][y] is None:
|
||||||
|
properties = self.particle_properties['wall']
|
||||||
|
wall = Particle.from_type((x, y), 'wall', properties)
|
||||||
|
self.particles[x][y] = wall
|
||||||
|
new_active_particles.add((x, y))
|
||||||
|
self.particle_count += 1 # Track new wall particle
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# Delete particles that go out of bounds
|
||||||
|
if x <= 0 or x >= self.width-1 or y <= 0 or y >= self.height-1:
|
||||||
|
if self.particles[x][y] is not None:
|
||||||
|
self.particles[x][y] = None
|
||||||
|
self.active_particles.discard((x, y))
|
||||||
|
self.particle_count -= 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
# Skip wall physics - walls are immutable
|
# Skip wall physics - walls are immutable
|
||||||
if particle.particle_type == 'wall':
|
if particle.particle_type == 'wall':
|
||||||
new_active_particles.add((x, y))
|
new_active_particles.add((x, y))
|
||||||
@ -607,8 +667,9 @@ class Simulation:
|
|||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
# Regular particle physics
|
# Regular particle physics
|
||||||
particle.velocity[0] += (fx / particle.mass) * dt
|
mass = max(particle.mass, 0.001)
|
||||||
particle.velocity[1] += (fy / particle.mass) * dt
|
particle.velocity[0] += (fx / mass) * dt
|
||||||
|
particle.velocity[1] += (fy / mass) * dt
|
||||||
|
|
||||||
if particle.liquid:
|
if particle.liquid:
|
||||||
# Enhanced liquid spreading
|
# Enhanced liquid spreading
|
||||||
@ -642,6 +703,8 @@ class Simulation:
|
|||||||
def clear_particles_circle(self, center_x, center_y): # this is for the brush tool
|
def clear_particles_circle(self, center_x, center_y): # this is for the brush tool
|
||||||
"""Clear particles in a circle around the given point based on brush size"""
|
"""Clear particles in a circle around the given point based on brush size"""
|
||||||
brush_size = int(self.brush_size)
|
brush_size = int(self.brush_size)
|
||||||
|
particles_cleared = 0 # Track how many particles we clear
|
||||||
|
|
||||||
for dx in range(-brush_size, brush_size + 1):
|
for dx in range(-brush_size, brush_size + 1):
|
||||||
for dy in range(-brush_size, brush_size + 1):
|
for dy in range(-brush_size, brush_size + 1):
|
||||||
if dx*dx + dy*dy <= brush_size*brush_size: # Circle check
|
if dx*dx + dy*dy <= brush_size*brush_size: # Circle check
|
||||||
@ -653,6 +716,10 @@ class Simulation:
|
|||||||
self.particles[grid_x][grid_y] = None
|
self.particles[grid_x][grid_y] = None
|
||||||
self.active_particles.discard((grid_x, grid_y))
|
self.active_particles.discard((grid_x, grid_y))
|
||||||
self.remove_from_spatial_grid(grid_x, grid_y)
|
self.remove_from_spatial_grid(grid_x, grid_y)
|
||||||
|
particles_cleared += 1
|
||||||
|
|
||||||
|
self.particle_count = max(0, self.particle_count - particles_cleared) # Update count, ensure it doesn't go negative
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def mix_liquids(self, liquid1, liquid2): # this is for the mix tool
|
def mix_liquids(self, liquid1, liquid2): # this is for the mix tool
|
||||||
@ -669,64 +736,6 @@ class Simulation:
|
|||||||
liquid2.color = self.calculate_color(liquid2.temperature)
|
liquid2.color = self.calculate_color(liquid2.temperature)
|
||||||
|
|
||||||
|
|
||||||
def get_particle_count(self):
|
|
||||||
"""Returns the number of particles in the simulation for the Debug display and for performance analysis."""
|
|
||||||
count = 0
|
|
||||||
for x in range(self.width):
|
|
||||||
for y in range(self.height):
|
|
||||||
if self.particles[x][y] is not None:
|
|
||||||
count += 1
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
def _process_particle_batch(self, batch, dt):
|
|
||||||
updates = []
|
|
||||||
new_active = set()
|
|
||||||
|
|
||||||
# Filter out dormant particles from the batch
|
|
||||||
active_batch = [pos for pos in batch if pos not in self.dormant_particles]
|
|
||||||
|
|
||||||
for x, y in active_batch:
|
|
||||||
particle = self.particles[x][y]
|
|
||||||
if not particle:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if particle.particle_type == 'wall':
|
|
||||||
new_active.add((x, y))
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check if particle should become dormant
|
|
||||||
if self._check_dormant_state(x, y, particle):
|
|
||||||
new_active.add((x, y))
|
|
||||||
continue
|
|
||||||
|
|
||||||
# physics calculations
|
|
||||||
fx, fy = self.calculate_forces(particle, x, y)
|
|
||||||
# Use max() to ensure mass is never zero
|
|
||||||
mass = max(particle.mass, 0.001)
|
|
||||||
particle.velocity[0] += (fx / mass) * dt
|
|
||||||
particle.velocity[1] += (fy / mass) * dt
|
|
||||||
|
|
||||||
new_x = int(x + particle.velocity[0] * dt)
|
|
||||||
new_y = int(y + particle.velocity[1] * dt)
|
|
||||||
|
|
||||||
if 0 <= new_x < self.width and 0 <= new_y < self.height:
|
|
||||||
if self.particles[new_x][new_y] is None:
|
|
||||||
updates.append((x, y, new_x, new_y, particle))
|
|
||||||
new_active.add((new_x, new_y))
|
|
||||||
# Wake up neighboring dormant particles
|
|
||||||
self._wake_neighbors(new_x, new_y)
|
|
||||||
else:
|
|
||||||
new_active.add((x, y))
|
|
||||||
|
|
||||||
# Apply updates and return new active set
|
|
||||||
for old_x, old_y, new_x, new_y, particle in updates:
|
|
||||||
self.particles[old_x][old_y] = None
|
|
||||||
self.particles[new_x][new_y] = particle
|
|
||||||
particle.position = (new_x, new_y)
|
|
||||||
|
|
||||||
return new_active
|
|
||||||
|
|
||||||
def _wake_neighbors(self, x, y):
|
def _wake_neighbors(self, x, y):
|
||||||
for dx in [-1, 0, 1]:
|
for dx in [-1, 0, 1]:
|
||||||
for dy in [-1, 0, 1]:
|
for dy in [-1, 0, 1]:
|
||||||
@ -735,9 +744,30 @@ class Simulation:
|
|||||||
if key in self.dormant_particles:
|
if key in self.dormant_particles:
|
||||||
self.dormant_particles.discard(key)
|
self.dormant_particles.discard(key)
|
||||||
self.particle_movement_counter[key] = 0
|
self.particle_movement_counter[key] = 0
|
||||||
|
|
||||||
|
def track_tps(self):
|
||||||
|
"""Track Ticks Per Second for simulation performance monitoring"""
|
||||||
|
if not hasattr(self, '_tps_counter'):
|
||||||
|
self._tps_counter = 0
|
||||||
|
self._tps_timer = time.time()
|
||||||
|
self._current_tps = 0
|
||||||
|
|
||||||
|
self._tps_counter += 1
|
||||||
|
current_time = time.time()
|
||||||
|
elapsed = current_time - self._tps_timer
|
||||||
|
|
||||||
|
# Update TPS count every second
|
||||||
|
if elapsed >= 1.0:
|
||||||
|
self._current_tps = self._tps_counter / elapsed
|
||||||
|
self._tps_counter = 0
|
||||||
|
self._tps_timer = current_time
|
||||||
|
|
||||||
|
return self._current_tps
|
||||||
|
|
||||||
def simulate_step(self, dt):
|
|
||||||
|
def simulate_step(self, dt, engine_settings):
|
||||||
"""Run a single step of the simulation"""
|
"""Run a single step of the simulation"""
|
||||||
|
|
||||||
active_list = list(self.active_particles)
|
active_list = list(self.active_particles)
|
||||||
batch_size = 1000
|
batch_size = 1000
|
||||||
|
|
||||||
@ -751,11 +781,10 @@ class Simulation:
|
|||||||
|
|
||||||
# Update particle positions and physics
|
# Update particle positions and physics
|
||||||
self.apply_gravity(dt)
|
self.apply_gravity(dt)
|
||||||
self.apply_physics(dt)
|
self.apply_physics(dt, engine_settings)
|
||||||
|
|
||||||
# Handle state changes and interactions
|
# Handle state changes and interactions
|
||||||
self.handle_temperature(dt)
|
self.handle_temperature(dt)
|
||||||
self.handle_particle_interactions(dt)
|
self.handle_particle_interactions(dt)
|
||||||
self.burning()
|
self.burning()
|
||||||
self.spread_fire()
|
self.spread_fire()
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user