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

2
.gitignore vendored
View File

@ -173,3 +173,5 @@ sandpypi.exe
sandpypi.7z
unittest/
.7z
.zip
livenotes.txt

View File

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

View File

@ -5,11 +5,12 @@
"hardness": 0.5,
"color": [255, 255, 0, 255],
"velocity": 0.5,
"wind": 1,
"mass": 0.5,
"conductivity": 0,
"heat_capacity": 1,
"flamability": 0.8,
"temperature": 0,
"temperature": 20,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
@ -79,7 +80,7 @@
"velocity": 0.0,
"conductivity": 0,
"heat_capacity": 0,
"color": [75, 75, 75, 255],
"color": [75, 75, 170, 255],
"mass": 1,
"flamability": 0.0,
"temperature": 0,
@ -104,7 +105,7 @@
"color": [139, 69, 19, 255],
"mass": 0.5,
"flamability": 0,
"temperature": 0,
"temperature": 20,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
@ -165,7 +166,7 @@
"color": [75, 75, 75, 255],
"mass": 1,
"flamability": 0,
"temperature": 0,
"temperature": 20,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
@ -185,7 +186,7 @@
"color": [139, 69, 19, 255],
"mass": 0.5,
"flamability": 0,
"temperature": 0,
"temperature": 20,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
@ -202,14 +203,14 @@
"velocity": 1.5,
"conductivity": 0,
"heat_capacity": 0,
"color": [128, 128, 128, 255],
"color": [128, 128, 128, 220],
"mass": 1,
"flamability": 0,
"melt": "molten-Stone",
"melt_temperature": 800,
"solidify": "stone",
"solidify_temperature": 799,
"temperature": 0,
"temperature": 20,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
@ -253,9 +254,9 @@
"flamability": 0.8,
"burning_temperature": 250,
"burning_rate": 0.01,
"burning_color": [255, 0, 0, 255],
"burning_color": [255, 69, 19, 255],
"burning": false,
"temperature": 0,
"temperature": 20,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
@ -272,9 +273,9 @@
"velocity": 0.5,
"conductivity": 0,
"heat_capacity": 1,
"color": [139, 69, 19, 255],
"color": [255, 69, 19, 255],
"mass": 0.5,
"flamability": 1,
"flamability": 0.8,
"temperature": 251,
"burning": true,
"explosive": false,
@ -293,7 +294,7 @@
"velocity": 0.0,
"conductivity": 0,
"heat_capacity": 1,
"color": [25, 25, 25, 25],
"color": [255, 255, 255, 25],
"mass": 0.0,
"flamability": 0,
"temperature": 0,
@ -313,7 +314,7 @@
"velocity": 0.5,
"conductivity": 0,
"heat_capacity": 1,
"color": [255, 45, 24, 255],
"color": [255, 45, 60, 255],
"mass": 0.3,
"flamability": 0,
"temperature": 1400,
@ -340,7 +341,7 @@
"flamability": 0,
"melt": "molten-rock",
"melt_temperature": 600,
"temperature": 0,
"temperature": 20,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
@ -438,19 +439,19 @@
"solid": true,
"is_gas": false,
"melt": "molten-glass",
"melt_temperature": 1000
"melt_temperature": 600
},
"flame": {
"name": "Flame",
"plasma": {
"name": "Plasma",
"size": 1,
"hardness": 0.0,
"velocity": 0.0,
"conductivity": 0,
"heat_capacity": 1,
"color": [255, 100, 0, 255],
"color": [255, 100, 200, 255],
"mass": 0.0,
"flamability": 0,
"temperature": 1000,
"temperature": 3600,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],

View File

@ -28,6 +28,7 @@ from settings import pygame, random, particle_properties, engine_settings
class Rendering:
def __init__(self, width, height):
#self.setup_gpu_rendering()
self.screen = pygame.display.set_mode((width, height))
self.background = pygame.Surface((width, height))
self.background.fill((0, 0, 0))
@ -39,7 +40,13 @@ class Rendering:
self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA)
self.cached_fonts = {
'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
@ -125,6 +132,14 @@ class Rendering:
else:
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 particle.is_gas:
# Enhanced gas visibility
@ -146,15 +161,6 @@ class Rendering:
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.particle_surface, (0, 0))
@ -277,41 +283,53 @@ class Rendering:
self.screen.blit(temperature_surface, (0, 0))
def draw_debug_overlay(self, fps, particles): # this is the function that draws the debug overlay
if engine_settings ['enable_fps']:
# Draw FPS
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))
def draw_debug_overlay(self, fps, sim):
if not engine_settings['enable_fps'] and not engine_settings['enable_debug']:
return
if engine_settings ['enable_debug']:
# Get mouse position and convert to grid coordinates
# Create static debug surface if not exists
if not hasattr(self, 'debug_surface'):
self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA)
# Only update when values change significantly
mouse_x, mouse_y = pygame.mouse.get_pos()
grid_x, grid_y = mouse_x // 3, mouse_y // 3
current_info = (
int(fps),
int(sim.track_tps()),
mouse_x // 10, # Reduced update frequency
mouse_y // 10,
sim.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))
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)
# Draw debug information
font = pygame.font.SysFont(None, 24)
particle_count = sum(1 for row in particles for cell in row if cell is not None)
debug_info = [
f"FPS: {int(fps)}",
f"Mouse: ({mouse_x}, {mouse_y})",
f"Grid: ({grid_x}, {grid_y})",
f"Particle: {particle_info}",
#f"Active Particle Count: {sim.Simulation.get_active_particle_count(sim.Simulation)}",
f"Particle Count: {particle_count}"
]
font = self.cached_fonts['debug']
y_offset = 10
for info in debug_info:
debug_text = font.render(info, True, (255, 255, 255))
self.screen.blit(debug_text, (10, y_offset))
if engine_settings['enable_fps']:
fps_surf = font.render(f"FPS: {fps:.1f} | TPS: {sim.track_tps():.1f}", True, (255, 255, 255))
self.debug_surface.blit(fps_surf, (10, y_offset))
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
self.buttons = {}
@ -344,7 +362,7 @@ class Rendering:
button_surface = pygame.Surface((80, 25))
color = self.particle_properties[particle_type].get('color', (255, 255, 255))
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))
button_surface.blit(label, (5, 5))
self.button_surfaces[particle_type] = button_surface
@ -358,7 +376,7 @@ class Rendering:
if 'clear' not in self.button_surfaces:
clear_surface = pygame.Surface((80, 25))
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))
clear_surface.blit(label, (5, 5))
self.button_surfaces['clear'] = clear_surface
@ -370,7 +388,7 @@ class Rendering:
if 'settings' not in self.button_surfaces:
settings_surface = pygame.Surface((80, 25))
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))
settings_surface.blit(label, (5, 5))
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, (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))
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))
def draw_settings_menu(self):
@ -401,7 +419,7 @@ class Rendering:
settings_surface.fill((50, 50, 50))
y_offset = 10
font = pygame.font.SysFont(None, 24)
font = self.cached_fonts.get('settings')
for setting, value in engine_settings.items():
# Create toggle button
@ -416,15 +434,17 @@ class Rendering:
return settings_surface
def clear_screen(self, sim): # this is the function that clears the screen
# Store current particle type
current_type = sim.current_particle_type
self.particle_count = 0
# Reset simulation grid while preserving particle type
sim.particles = [[None for _ in range(sim.height)] for _ in range(sim.width)]
sim.active_particles.clear()
sim.current_particle_type = current_type
sim.reset_particle_count()
# Clear display surfaces
self.background.fill((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.
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 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
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()
clock = pygame.time.Clock()
width = 1024
@ -25,32 +161,38 @@ def main(): # Main function to run the program
screen = pygame.display.set_mode((width, height), pygame.HWSURFACE | pygame.DOUBLEBUF)
sim = Simulation(width, height)
rendering = Rendering(width, height)
# State variables
mouse_down_left = False
mouse_down_right = False
mouse_down_middle = False
mouse_down_wheel_up = False
mouse_down_wheel_down = False
over_button = False
zoom_active = False
zoom_locked = False
zoom_pos = None
settings_visible = False
settings_menu_y = 100
running = True
while running:
fps = clock.get_fps() # Get the current frame rate
dt = clock.tick(300) / 1000 # sets the target frame rate to 60 FPS
keys = pygame.key.get_pressed()
zoom_active = keys[pygame.K_z]
# Clear screen at start of frame
screen.fill((0, 0, 0))
fps = clock.get_fps()
dt = clock.tick(1000) / 1000
keys = pygame.key.get_pressed()
mouse_pos = pygame.mouse.get_pos()
zoom_active = keys[pygame.K_z]
# Handle events
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
continue
if event.type == pygame.MOUSEBUTTONDOWN:
mouse_pos = pygame.mouse.get_pos()
# Check if clicking in settings area when menu is visible
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)
@ -62,19 +204,19 @@ def main(): # Main function to run the program
elif event.button == 1: # Left click
over_button = False
# Check settings button first
# Check settings button first
if rendering.settings_button.collidepoint(mouse_pos):
settings_visible = not settings_visible
over_button = True
if zoom_active:
elif zoom_active:
zoom_locked = not zoom_locked
if zoom_locked:
zoom_pos = mouse_pos
# Handle settings menu interactions
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
if 0 <= setting_index < len(engine_settings):
setting_name = list(engine_settings.keys())[setting_index]
@ -82,7 +224,6 @@ def main(): # Main function to run the program
over_button = True
elif not in_settings_area:
# Check category buttons
for category, button in rendering.category_buttons.items():
if button.collidepoint(mouse_pos):
@ -101,6 +242,7 @@ def main(): # Main function to run the program
# Check clear screen button
if rendering.clear_screen_button.collidepoint(mouse_pos):
rendering.clear_screen(sim)
particle_count = 0
over_button = True
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
mouse_down_right = True
elif event.button == 2: # Middle click
mouse_down_middle = True
elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
mouse_down_left = False
elif event.type == pygame.MOUSEBUTTONUP and event.button == 3:
mouse_down_right = False
elif event.type == pygame.MOUSEBUTTONUP and event.button == 2:
mouse_down_middle = False
elif event.type == pygame.MOUSEBUTTONUP:
if event.button == 1:
mouse_down_left = False
elif event.button == 3:
mouse_down_right = False
elif event.button == 2:
mouse_down_middle = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
print("Escape button pressed")
print(f"Exiting Program {__file__}")
running = False
elif event.key == pygame.K_SPACE:
print(f"Pause button pressed but not functional 'Space'")
pass
engine_settings['pause_sim'] = not engine_settings['pause_sim']
elif event.key == pygame.K_c:
rendering.clear_screen(sim)
elif event.key == pygame.K_z:
zoom_active = not zoom_active
if zoom_active:
zoom_locked = False
zoom_pos = pygame.mouse.get_pos()
zoom_active = True
zoom_locked = False
zoom_pos = pygame.mouse.get_pos()
elif event.type == pygame.QUIT:
running = False
# Update simulation if not paused
if not engine_settings['pause_sim']:
sim.simulate_step(dt, engine_settings)
# Handle continuous mouse input
if mouse_down_left and not over_button:
x, y = pygame.mouse.get_pos()
# Check if current particle type is wind or air
x, y = mouse_pos
if sim.current_particle_type not in ['wind', 'air']:
sim.create_particle_circle(x, y)
else:
# Handle wind/air differently
sim.add_wind_zone(x, y)
if mouse_down_right:
x, y = pygame.mouse.get_pos()
x, y = mouse_pos
sim.clear_particles_circle(x, y)
if mouse_down_middle:
x, y = pygame.mouse.get_pos()
x, y = mouse_pos
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:
mouse_pos = zoom_pos if zoom_locked else pygame.mouse.get_pos()
zoom_surface = rendering.draw_zoom_window(sim.particles, sim.particle_size, rendering.particle_colors, mouse_pos)
# Position zoom window
zoom_x = 10 if mouse_pos[0] > width/2 else width - 110
zoom_y = 10 if mouse_pos[1] > height/2 else height - 110
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, current_zoom_pos)
zoom_x = 10 if current_zoom_pos[0] > width/2 else width - 110
zoom_y = 10 if current_zoom_pos[1] > height/2 else height - 110
screen.blit(zoom_surface, (zoom_x, zoom_y))
# Draw Settings
# Draw settings and debug overlay last
if settings_visible:
settings_menu = rendering.draw_settings_menu()
rendering.screen.blit(settings_menu, (rendering.width - 320, 100))
rendering.draw_debug_overlay(fps, sim.particles)
rendering.draw_debug_overlay(fps, sim)
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)
pygame.quit()
if __name__ == "__main__":
# Profile the application
profiler = cProfile.Profile()
profiler.enable()
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 json
import random
import time
import numpy as np
engine_settings = {
'pause_sim': True,
'enable_cursor': True,
'enable_glow': False,
'enable_gas_effect': True,
@ -27,6 +29,7 @@ engine_settings = {
'enable_WVisuals': False,
'enable_PVisuals': False,
'enable_TempVisuals': False,
'outerwall': True,
# 'settings': True/False
}

251
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.
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.
class Particle:
@ -85,6 +85,7 @@ class Simulation:
self.height = height
self.particle_size = 3
self.particles = [[None for _ in range(height)] for _ in range(width)]
self.particle_count = 0
self.active_particles = set()
self.cell_size = 32
self.spatial_grid = {}
@ -96,6 +97,9 @@ class Simulation:
self.wind_zones = []
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.
# Convert coordinates to grid cell
@ -162,25 +166,29 @@ class Simulation:
if particle.temperature >= particle.evaporate_temperature and 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
if hasattr(particle, 'freeze_temperature') and particle.freeze_temperature is not None:
if particle.temperature <= particle.freeze_temperature and particle.freeze:
self.transform_particle(x, y, particle.freeze)
# Check solidification
if hasattr(particle, 'solidify_temperature') and particle.solidify_temperature is not None:
if particle.temperature <= particle.solidify_temperature and particle.solidify:
self.transform_particle(x, y, particle.solidify)
# Check for melting with proper attribute validation
if (hasattr(particle, 'melt') and hasattr(particle, 'melt_temperature')
and particle.melt_temperature is not None):
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.
"""Handle interactions between different particle types"""
self.update_spatial_grid()
for x, y in list(self.active_particles):
particle = self.particles[x][y]
if not particle:
@ -252,22 +260,6 @@ class Simulation:
self.active_particles.add((new_x, new_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):
# Instead of creating particles, store wind zone data
wind_zone = {
@ -316,6 +308,53 @@ class Simulation:
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):
"""Quick neighbor lookup without full spatial grid"""
@ -393,22 +432,13 @@ class Simulation:
particle = self.particles[x][y]
if not particle:
continue
# Check for melting with proper attribute validation
if (hasattr(particle, 'melt') and hasattr(particle, 'melt_temperature')
and particle.melt_temperature is not None):
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)
if particle.temperature > 1700:
# Transition to gas
particle.is_gas = True
particle.temperature = 1700
particle.velocity = [random.uniform(-1, 1), random.uniform(-1, 1)]
particle.temperature < 1400
particle.is_gas = False
# Temperature spread to neighbors
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.
"""Create a new particle with full property support"""
particle_type = self.current_particle_type.lower()
# Check if the particle is within the grid boundaries
if particle_type in self.particle_properties:
grid_x = x // self.particle_size
grid_y = y // self.particle_size
if 0 <= grid_x < self.width and 0 <= grid_y < self.height:
properties = self.particle_properties[particle_type]
position = (grid_x, grid_y)
@ -451,10 +483,11 @@ class Simulation:
particle_type=particle_type,
properties=properties
)
# Add to the grid
if 0 <= grid_x < len(self.particles) and 0 <= grid_y < len(self.particles[0]):
self.particles[grid_x][grid_y] = new_particle
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.
@ -462,10 +495,16 @@ class Simulation:
for dx 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
self.create_particle(center_x + dx * self.particle_size,
center_y + dy * self.particle_size)
self.create_particle(center_x + dx * 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.
"""Handle only gravity and basic particle movement"""
self.spatial_grid.clear()
@ -484,7 +523,7 @@ class Simulation:
continue
# 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:
new_x, new_y = x, y + 1
else:
@ -526,7 +565,7 @@ class Simulation:
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"""
new_active_particles = set()
@ -535,6 +574,27 @@ class Simulation:
if not particle:
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
if particle.particle_type == 'wall':
new_active_particles.add((x, y))
@ -607,8 +667,9 @@ class Simulation:
continue
else:
# Regular particle physics
particle.velocity[0] += (fx / particle.mass) * dt
particle.velocity[1] += (fy / particle.mass) * dt
mass = max(particle.mass, 0.001)
particle.velocity[0] += (fx / mass) * dt
particle.velocity[1] += (fy / mass) * dt
if particle.liquid:
# Enhanced liquid spreading
@ -642,6 +703,8 @@ class Simulation:
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"""
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 dy in range(-brush_size, brush_size + 1):
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.active_particles.discard((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
@ -669,64 +736,6 @@ class Simulation:
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):
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
@ -736,8 +745,29 @@ class Simulation:
self.dormant_particles.discard(key)
self.particle_movement_counter[key] = 0
def simulate_step(self, dt):
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, engine_settings):
"""Run a single step of the simulation"""
active_list = list(self.active_particles)
batch_size = 1000
@ -751,11 +781,10 @@ class Simulation:
# Update particle positions and physics
self.apply_gravity(dt)
self.apply_physics(dt)
self.apply_physics(dt, engine_settings)
# Handle state changes and interactions
self.handle_temperature(dt)
self.handle_particle_interactions(dt)
self.burning()
self.spread_fire()