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
2
.gitignore
vendored
2
.gitignore
vendored
@ -173,3 +173,5 @@ sandpypi.exe
|
||||
sandpypi.7z
|
||||
unittest/
|
||||
.7z
|
||||
.zip
|
||||
livenotes.txt
|
||||
@ -1 +1,2 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
@ -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],
|
||||
|
||||
112
rendering.py
112
rendering.py
@ -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))
|
||||
|
||||
|
||||
260
sandpypi.py
260
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.
|
||||
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()
|
||||
|
||||
@ -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
251
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.
|
||||
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()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user