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:
Stan44 2024-12-28 13:29:37 -06:00
parent 28653ec606
commit b2187693d7
7 changed files with 441 additions and 245 deletions

4
.gitignore vendored
View File

@ -172,4 +172,6 @@ sandpypi.onefile-build/
sandpypi.exe sandpypi.exe
sandpypi.7z sandpypi.7z
unittest/ unittest/
.7z .7z
.zip
livenotes.txt

View File

@ -1 +1,2 @@
import os
import sys

View File

@ -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],

View File

@ -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))

View File

@ -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()

View File

@ -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
View File

@ -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()