Massive changes to the repository have been made.

feat: Performance optimizations and UI improvements

- Added dormant state tracking for static particles
- Fixed settings menu particle spawn overlap
- Optimized particle batch processing
- Added brush cursor visualization
- Improved UI interaction zones
- Enhanced gas particle effects
- Added glow toggle functionality disabled by default due to performance impact
- Fixed particle rendering issues
- Debug Overlay Disabled by default
- FPS counter added semi seperate from Debug Overlay, Turn this one off when using Debug Overlay
- Improved particle rendering performance

Performance improvements focus on reducing unnecessary calculations for static particles while maintaining core simulation mechanics. UI changes prevent unwanted particle spawning during menu interactions.
This commit is contained in:
Stan44 2024-12-27 07:39:31 -06:00
parent 33404edcc1
commit 986750c722
14 changed files with 865 additions and 612 deletions

11
.gitignore vendored
View File

@ -1,9 +1,8 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
__pycache__/
# C extensions
*.so
@ -160,9 +159,17 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Custom Ignores
current_stations.html
forecast_data.json
openapi.json
__pycache__/sim.cpython-312.pyc
__pycache__/sim.cpython-312.pyc
__pycache__/rendering.cpython-312.pyc
sandpypi.dist/
sandpypi.build/
sandpypi.onefile-build/
sandpypi.exe
sandpypi.7z
unittest/
.7z

View File

@ -4,6 +4,29 @@ mostly a concept in python for falling sand simulation
i guess the goal is to make a python version of the powder toy which is a falling sand sim
the code is not finished yet, but i will update it as i go.
Main Features:
-------------
- Particle Physics
- Gravity and wind effects
- Temperature dynamics
- State transitions (melting, freezing, evaporation)
- Particle Interactions
- Collision detection
- Chemical reactions (e.g., water + sand = mud)
- Heat transfer between particles
- Special Effects
- Fire propagation
- Smoke generation
- Liquid spreading
- Optimization Features
- Spatial partitioning grid
- Dormant particle tracking
- Batch processing
"""
## **Current Features**
| **Working** | **Partial** | **Not Working/Implemented** |

1
__init__.py Normal file
View File

@ -0,0 +1 @@

Binary file not shown.

Binary file not shown.

View File

@ -1,331 +1,333 @@
{
"sand": {
"name": "Sand",
"size": 1,
"hardness": 0.5,
"color": [255, 255, 0, 255],
"velocity": 0.5,
"mass": 0.5,
"conductivity": 0,
"heat_capacity": 1,
"flamability": 0.8,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.5,
"viscosity": 0,
"pressure": 0,
"melt": "molten-Glass",
"melt_temperature": 1000,
"conductive": false,
"liquid": false,
"solid": true,
"is_gas": false
},
"water": {
"name": "Water",
"size": 1,
"hardness": 0.2,
"velocity": 0.3,
"conductivity": 1,
"heat_capacity": 1,
"color": [0, 0, 255, 255],
"mass": 1,
"flamability": 0,
"temperature": 22,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 1,
"viscosity": 1,
"pressure": 0.5,
"evaporate": "steam",
"evaporate_temperature": 145,
"freeze": "ice",
"freeze_temperature": 0,
"melt": "water",
"melt_temperature": 20,
"liquid": true,
"solid": false,
"is_gas": false
},
"steam": {
"name": "Steam",
"size": 1,
"hardness": 0.0,
"velocity": 0.2,
"conductivity": 1,
"heat_capacity": 1,
"color": [255, 255, 255, 255],
"mass": 0.01,
"flamability": 0,
"temperature": 100,
"solidify_temperature": 98,
"solidify": "water",
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.5,
"viscosity": 0.5,
"liquid": false,
"solid": false,
"is_gas": true,
"conductive": false
},
"ice": {
"name": "Ice",
"size": 1,
"hardness": 1000,
"velocity": 0.0,
"conductivity": 0,
"heat_capacity": 0,
"color": [75, 75, 75, 255],
"mass": 1,
"flamability": 0.0,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 1,
"viscosity": 1,
"liquid": false,
"solid": true,
"is_gas": false
},
"mud": {
"name": "Mud",
"size": 1,
"hardness": 0.4,
"velocity": 0.5,
"conductivity": 1,
"heat_capacity": 1,
"color": [139, 69, 19, 255],
"mass": 0.5,
"flamability": 0,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.5,
"viscosity": 1,
"liquid": false,
"solid": true,
"is_gas": false
},
"fire": {
"name": "Fire",
"size": 1,
"hardness": 0.1,
"velocity": 0.1,
"conductivity": 0,
"heat_capacity": 1,
"color": [255, 0, 0, 255],
"mass": 0.1,
"flamability": 1,
"temperature": 1000,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.1,
"viscosity": 0.1,
"liquid": false,
"solid": false,
"is_gas": true
},
"smoke": {
"name": "Smoke",
"size": 1,
"hardness": 0.1,
"velocity": 0.1,
"conductivity": 0,
"heat_capacity": 1,
"color": [115, 113, 95, 255],
"mass": 0.01,
"flamability": 0,
"temperature": 85,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.4,
"viscosity": 0.1,
"lifetime": 90,
"liquid": false,
"solid": false,
"is_gas": true
},
"wall": {
"name": "Wall",
"size": 1,
"hardness": 1000,
"velocity": 0.0,
"conductivity": 0,
"heat_capacity": 0,
"color": [75, 75, 75, 255],
"mass": 1,
"flamability": 0.1,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 1,
"viscosity": 1,
"liquid": false,
"solid": true,
"is_gas": false
},
"dirt": {
"name": "Dirt",
"size": 1,
"hardness": 0.5,
"velocity": 0.5,
"conductivity": 0,
"heat_capacity": 1,
"color": [139, 69, 19, 255],
"mass": 0.5,
"flamability": 0,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.5,
"viscosity": 0.5,
"liquid": false,
"solid": true,
"is_gas": false
},
"stone": {
"name": "Stone",
"size": 1,
"hardness": 0.7,
"velocity": 1.5,
"conductivity": 0,
"heat_capacity": 0,
"color": [128, 128, 128, 255],
"mass": 1,
"flamability": 0,
"melt": "molten-Stone",
"melt_temperature": 800,
"solidify": "stone",
"solidify_temperature": 799,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.5,
"viscosity": 0.5,
"liquid": false,
"solid": true,
"is_gas": false
},
"snow": {
"name": "Snow",
"size": 1,
"hardness": 0.1,
"velocity": 0.2,
"conductivity": 1,
"heat_capacity": 1,
"color": [255, 255, 255, 255],
"mass": 0.01,
"flamability": 0,
"melt": "water",
"melt_temperature": 10,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.1,
"viscosity": 0.01,
"liquid": false,
"solid": true,
"is_gas": false
},
"wood": {
"name": "Wood",
"size": 1,
"hardness": 0.5,
"velocity": 0.5,
"conductivity": 0,
"heat_capacity": 1,
"color": [139, 69, 19, 255],
"mass": 0.5,
"flamability": 0.8,
"burning_temperature": 250,
"burning_rate": 0.01,
"burning_color": [255, 0, 0, 255],
"burning": false,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.5,
"viscosity": 0.5,
"liquid": false,
"solid": true,
"is_gas": false
},
"burning-wood": {
"name": "Burning Wood",
"size": 1,
"hardness": 0.5,
"velocity": 0.5,
"conductivity": 0,
"heat_capacity": 1,
"color": [139, 69, 19, 255],
"mass": 0.5,
"flamability": 0.8,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.5,
"viscosity": 0.5,
"liquid": false,
"solid": true,
"is_gas": false
},
"air": {
"name": "Air",
"size": 1,
"hardness": 0.0,
"velocity": 0.0,
"conductivity": 0,
"heat_capacity": 1,
"color": [25, 25, 25, 25],
"mass": 0.0,
"flamability": 0,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.0,
"viscosity": 0.0,
"liquid": false,
"solid": false,
"is_gas": true
},
"lava": {
"name": "Lava",
"size": 1,
"hardness": 0.2,
"velocity": 0.5,
"conductivity": 0,
"heat_capacity": 1,
"color": [255, 45, 24, 255],
"mass": 0.3,
"flamability": 0,
"temperature": 2700,
"solidify": "molten-rock",
"solidify_temperature": 799,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.8,
"viscosity": 0.8,
"liquid": true,
"solid": false,
"is_gas": false
},
"sand": {
"name": "Sand",
"size": 1,
"hardness": 0.5,
"color": [255, 255, 0, 255],
"velocity": 0.5,
"mass": 0.5,
"conductivity": 0,
"heat_capacity": 1,
"flamability": 0.8,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.5,
"viscosity": 0,
"pressure": 0,
"melt": "molten-Glass",
"melt_temperature": 1700,
"conductive": false,
"liquid": false,
"solid": true,
"is_gas": false
},
"water": {
"name": "Water",
"size": 1,
"hardness": 0.2,
"velocity": 0.3,
"conductivity": 1,
"heat_capacity": 1,
"color": [0, 0, 255, 255],
"mass": 1,
"flamability": 0,
"temperature": 22,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 1,
"viscosity": 1,
"pressure": 0.5,
"evaporate": "steam",
"evaporate_temperature": 100,
"freeze": "ice",
"freeze_temperature": 0,
"conductive": true,
"liquid": true,
"solid": false,
"is_gas": false
},
"steam": {
"name": "Steam",
"size": 1,
"hardness": 0.0,
"velocity": 0.3,
"conductivity": 1,
"heat_capacity": 1,
"color": [255, 255, 255, 255],
"mass": 0.01,
"flamability": 0,
"temperature": 100,
"solidify_temperature": 98,
"solidify": "water",
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.5,
"viscosity": 0.5,
"liquid": false,
"solid": false,
"is_gas": true,
"conductive": false
},
"ice": {
"name": "Ice",
"size": 1,
"hardness": 1000,
"velocity": 0.0,
"conductivity": 0,
"heat_capacity": 0,
"color": [75, 75, 75, 255],
"mass": 1,
"flamability": 0.0,
"temperature": 0,
"melt": "water",
"melt_temperature": 0.05,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 1,
"viscosity": 1,
"liquid": false,
"solid": true,
"is_gas": false
},
"mud": {
"name": "Mud",
"size": 1,
"hardness": 0.4,
"velocity": 0.5,
"conductivity": 1,
"heat_capacity": 1,
"color": [139, 69, 19, 255],
"mass": 0.5,
"flamability": 0,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.5,
"viscosity": 1,
"liquid": false,
"solid": true,
"is_gas": false
},
"fire": {
"name": "Fire",
"size": 1,
"hardness": 0.1,
"velocity": 0.1,
"conductivity": 0,
"heat_capacity": 1,
"color": [255, 0, 0, 255],
"mass": 0.1,
"flamability": 1,
"temperature": 800,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.1,
"viscosity": 0.1,
"liquid": false,
"solid": false,
"is_gas": true
},
"smoke": {
"name": "Smoke",
"size": 1,
"hardness": 0.1,
"velocity": 0.07,
"conductivity": 0,
"heat_capacity": 1,
"color": [115, 113, 95, 255],
"mass": 0.01,
"flamability": 0,
"temperature": 85,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.4,
"viscosity": 0.1,
"lifetime": 90,
"liquid": false,
"solid": false,
"is_gas": true
},
"wall": {
"name": "Wall",
"size": 1,
"hardness": 1000,
"velocity": 0.0,
"conductivity": 0,
"heat_capacity": 0,
"color": [75, 75, 75, 255],
"mass": 1,
"flamability": 0,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 1,
"viscosity": 1,
"liquid": false,
"solid": true,
"is_gas": false
},
"dirt": {
"name": "Dirt",
"size": 1,
"hardness": 0.5,
"velocity": 0.5,
"conductivity": 0,
"heat_capacity": 1,
"color": [139, 69, 19, 255],
"mass": 0.5,
"flamability": 0,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.5,
"viscosity": 0.5,
"liquid": false,
"solid": true,
"is_gas": false
},
"stone": {
"name": "Stone",
"size": 1,
"hardness": 0.7,
"velocity": 1.5,
"conductivity": 0,
"heat_capacity": 0,
"color": [128, 128, 128, 255],
"mass": 1,
"flamability": 0,
"melt": "molten-Stone",
"melt_temperature": 800,
"solidify": "stone",
"solidify_temperature": 799,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.5,
"viscosity": 0.5,
"liquid": false,
"solid": true,
"is_gas": false
},
"snow": {
"name": "Snow",
"size": 1,
"hardness": 0.1,
"velocity": 0.2,
"conductivity": 1,
"heat_capacity": 1,
"color": [255, 255, 255, 255],
"mass": 0.01,
"flamability": 0,
"melt": "water",
"melt_temperature": 1,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.1,
"viscosity": 0.01,
"liquid": false,
"solid": true,
"is_gas": false
},
"wood": {
"name": "Wood",
"size": 1,
"hardness": 0.5,
"velocity": 0.5,
"conductivity": 0,
"heat_capacity": 1,
"color": [139, 69, 19, 255],
"mass": 0.5,
"flamability": 0.8,
"burning_temperature": 250,
"burning_rate": 0.01,
"burning_color": [255, 0, 0, 255],
"burning": false,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.5,
"viscosity": 0.5,
"liquid": false,
"solid": true,
"is_gas": false
},
"burning-wood": {
"name": "Burning Wood",
"size": 1,
"hardness": 0.5,
"velocity": 0.5,
"conductivity": 0,
"heat_capacity": 1,
"color": [139, 69, 19, 255],
"mass": 0.5,
"flamability": 1,
"temperature": 251,
"burning": true,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.5,
"viscosity": 0.5,
"liquid": false,
"solid": true,
"is_gas": false
},
"air": {
"name": "Air",
"size": 1,
"hardness": 0.0,
"velocity": 0.0,
"conductivity": 0,
"heat_capacity": 1,
"color": [25, 25, 25, 25],
"mass": 0.0,
"flamability": 0,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.0,
"viscosity": 0.0,
"liquid": false,
"solid": false,
"is_gas": true
},
"lava": {
"name": "Lava",
"size": 1,
"hardness": 0.2,
"velocity": 0.5,
"conductivity": 0,
"heat_capacity": 1,
"color": [255, 45, 24, 255],
"mass": 0.3,
"flamability": 0,
"temperature": 1400,
"solidify": "molten-rock",
"solidify_temperature": 799,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.8,
"viscosity": 0.8,
"liquid": true,
"solid": false,
"is_gas": false
},
"rock": {
"name": "Rock",
"size": 1,
@ -337,15 +339,11 @@
"mass": 0.8,
"flamability": 0,
"melt": "molten-rock",
"melt_temperature": 799,
"melt_temperature": 600,
"temperature": 0,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [
0,
0,
0
],
"explosion_color": [0, 0, 0],
"friction": 0.5,
"viscosity": 0.5,
"liquid": false,
@ -362,9 +360,9 @@
"color": [255, 140, 0, 255],
"mass": 0.8,
"flamability": 0,
"temperature": 799,
"melt": "lavaa",
"melt_temperature": 1200,
"temperature": 600,
"melt": "lava",
"melt_temperature": 1300,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0,0,0],
@ -376,70 +374,92 @@
"solidify": "rock",
"solidify_temperature": 200
},
"molten_stone": {
"name": "Molten Stone",
"size": 1,
"hardness": 0.2,
"velocity": 0.3,
"conductivity": 1,
"heat_capacity": 1,
"color": [255, 140, 0, 255],
"mass": 0.8,
"flamability": 0,
"temperature": 1200,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.8,
"viscosity": 0.8,
"liquid": true,
"solid": false,
"is_gas": false,
"solidify": "stone",
"solidify_temperature": 800
},
"molten_glass": {
"name": "Molten Glass",
"size": 1,
"hardness": 0.2,
"velocity": 0.4,
"conductivity": 0.8,
"heat_capacity": 1,
"color": [255, 200, 150, 200],
"mass": 0.6,
"flamability": 0,
"temperature": 1200,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.7,
"viscosity": 0.9,
"liquid": true,
"solid": false,
"is_gas": false,
"solidify": "glass",
"solidify_temperature": 800
},
"flame": {
"name": "Flame",
"size": 1,
"hardness": 0.0,
"velocity": 0.0,
"conductivity": 0,
"heat_capacity": 1,
"color": [255, 100, 0, 255],
"mass": 0.0,
"flamability": 0,
"temperature": 1000,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.0,
"viscosity": 0.0,
"liquid": false,
"solid": false,
"is_gas": true
}
"molten_stone": {
"name": "Molten Stone",
"size": 1,
"hardness": 0.2,
"velocity": 0.3,
"conductivity": 1,
"heat_capacity": 1,
"color": [255, 140, 0, 255],
"mass": 0.8,
"flamability": 0,
"temperature": 1200,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.8,
"viscosity": 0.8,
"liquid": true,
"solid": false,
"is_gas": false,
"solidify": "stone",
"solidify_temperature": 800
},
"molten_glass": {
"name": "Molten Glass",
"size": 1,
"hardness": 0.2,
"velocity": 0.4,
"conductivity": 0.8,
"heat_capacity": 1,
"color": [255, 200, 150, 200],
"mass": 0.6,
"flamability": 0,
"temperature": 600,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.7,
"viscosity": 0.9,
"liquid": true,
"solid": false,
"is_gas": false,
"solidify": "glass",
"solidify_temperature": 599
},
"glass": {
"name": "Glass",
"size": 1,
"hardness": 0.2,
"velocity": 0.4,
"conductivity": 0,
"heat_capacity": 1,
"color": [50, 45, 255, 100],
"mass": 0.6,
"flamability": 0,
"temperature": 20,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.7,
"viscosity": 0.9,
"liquid": false,
"solid": true,
"is_gas": false,
"melt": "molten-glass",
"melt_temperature": 1000
},
"flame": {
"name": "Flame",
"size": 1,
"hardness": 0.0,
"velocity": 0.0,
"conductivity": 0,
"heat_capacity": 1,
"color": [255, 100, 0, 255],
"mass": 0.0,
"flamability": 0,
"temperature": 1000,
"explosive": false,
"explosion_radius": 0,
"explosion_color": [0, 0, 0],
"friction": 0.0,
"viscosity": 0.0,
"liquid": false,
"solid": false,
"is_gas": true
}
}

View File

@ -1,4 +1,26 @@
"""
#File Name: rendering.py
Rendering class for the particle simulation.
This class is responsible for rendering the particles, UI elements, and debug information on the screen. It handles the setup of the display, pre-rendering of static UI elements, and the drawing of particles, buttons, and debug overlays.
The `draw_particles` function is the main method for rendering the particles on the screen. It takes the particle data, active particles, particle size, and particle colors as input, and renders the particles on the `particle_surface`. The `particle_surface` is then blitted onto the main screen.
The `draw_zoom_window` function is used to render a zoomed-in view of the particles around the mouse cursor. It creates a separate surface for the zoomed-in view and returns it.
The `draw_debug_overlay` function is responsible for rendering the debug information, such as FPS, mouse position, and particle information, on the screen.
The `draw_buttons` function handles the rendering of the category buttons, particle buttons, and other UI elements like the clear screen and settings buttons.
The `render_brush_cursor` function is used to draw a visual indicator for the current brush size.
The `draw_brush_size_slider` function renders a slider for adjusting the brush size.
The `draw_settings_menu` function creates a settings menu surface that can be displayed on the screen.
The `clear_screen` function is used to reset the simulation grid and clear the display surfaces.
"""
from settings import pygame, random, particle_properties, engine_settings
@ -11,14 +33,19 @@ class Rendering:
self.background.fill((0, 0, 0))
self.width = width
self.height = height
self.particle_surface = pygame.Surface((width, height), pygame.SRCALPHA)
self.particle_colors = {}
self.button_width, self.button_height = 30, 30
self.particle_properties = particle_properties
self.buttons = {}
self.clear_screen_button = pygame.Rect(915, 10, 50, 30)
self.load_buttons() # Load buttons dynamically
self.particle_colors = {}
self.particle_surface = pygame.Surface((width, height), pygame.SRCALPHA)
self.debug_surface = pygame.Surface((300, 150), pygame.SRCALPHA)
self.cached_fonts = {
'debug': pygame.font.SysFont(None, 24),
'button': pygame.font.SysFont(None, 20)
}
# Pre-render static UI elements
self.button_surfaces = {}
# Initialize categories
for name, properties in particle_properties.items():
if 'color' in properties:
self.particle_colors[name.lower()] = properties['color']
@ -36,7 +63,24 @@ class Rendering:
self.current_category = 'Solids'
self.category_buttons = {}
self.setup_category_menu()
self.setup_static_ui()
def setup_gpu_rendering(self):
# Initialize OpenGL context
pygame.display.gl_set_attribute(pygame.GL_ACCELERATED_VISUAL, 1)
self.screen = pygame.display.set_mode((self.width, self.height),
pygame.OPENGL | pygame.DOUBLEBUF)
def setup_static_ui(self):
for category in self.categories:
surf = pygame.Surface((80, 25))
surf.fill((150, 150, 150))
text = self.cached_fonts['button'].render(category, True, (0, 0, 0))
surf.blit(text, (5, 5))
self.button_surfaces[category] = surf
def setup_category_menu(self):
# Category buttons at the top
@ -60,25 +104,27 @@ class Rendering:
def draw_particles(self, particles, active_particles, particle_size, particle_colors): # this is the function that draws the particles
self.particle_surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
#self.particle_surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
self.particle_surface.fill((0, 0, 0, 0))
particle_batches = {}
for x, y in active_particles:
particle = particles[x][y]
if not particle:
continue
base_color = particle_colors.get(particle.particle_type, (255, 255, 255))
color = list(base_color)
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(100)
self.particle_surface.blit(glow_surface, (x * particle_size - glow_radius, y * particle_size - glow_radius))
p_type = particle.particle_type
if p_type not in particle_batches:
particle_batches[p_type] = []
rect = pygame.Rect(x * particle_size, y * particle_size,
particle_size, particle_size)
particle_batches[p_type].append(rect)
if particle.particle_type in particle_colors:
color = particle_colors[particle.particle_type]
else:
color = (255, 255, 255)
if engine_settings['enable_gas_effect']:
if particle.is_gas:
# Enhanced gas visibility
@ -94,17 +140,38 @@ class Rendering:
offset_y = random.randint(-1, 1)
rect = (x * particle_size + offset_x, y * particle_size + offset_y,
particle_size, particle_size)
rect = (x * particle_size, y * particle_size,
particle_size, particle_size)
else:
rect = (x * particle_size, y * particle_size,
particle_size, particle_size)
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))
"""#Potentially for future
for p_type, rects in particle_batches.items():
color = particle_colors.get(p_type, (255, 255, 255))
rect = (x * particle_size, y * particle_size,
particle_size, particle_size)
if len(rects) > 1:
pygame.draw.rect(self.particle_surface, color, rect)
else:
pygame.draw.rect(self.particle_surface, color, rect)
self.screen.blit(self.background, (0, 0))
self.screen.blit(self.particle_surface, (0, 0))"""
def draw_zoom_window(self, particles, particle_size, particle_colors, mouse_pos, zoom_factor=4):
def draw_zoom_window(self, particles, particle_size, particle_colors, mouse_pos, zoom_factor=4): # this is the function that draws the zoom window
print(f"Drawing zoom window.")
zoom_size = 100 # Size of zoom window
zoom_surface = pygame.Surface((zoom_size, zoom_size))
@ -132,101 +199,127 @@ class Rendering:
return zoom_surface
def draw_debug_overlay(self, fps, particles): # this is the function that draws the debug overlay
# Get mouse position and convert to grid coordinates
mouse_x, mouse_y = pygame.mouse.get_pos()
grid_x = mouse_x // 3
grid_y = mouse_y // 3
# Get particle info under cursor
particle_info = "None"
if 0 <= grid_x < len(particles) and 0 <= grid_y < len(particles[0]):
particle = particles[grid_x][grid_y]
def _get_particle_info(self, particles, x, y):
if 0 <= x < len(particles) and 0 <= y < len(particles[0]):
particle = particles[x][y]
if particle:
# Include more detailed particle information
particle_info = f"Type: {particle.particle_type}"
if hasattr(particle, 'temperature'):
particle_info += f" | Temp: {particle.temperature}°C"
if hasattr(particle, 'liquid'):
particle_info += f" | Liquid: {particle.liquid}"
if hasattr(particle, 'is_gas'):
particle_info += f" | Gas: {particle.is_gas}"
if hasattr(particle, 'solid'):
particle_info += f" | Solid: {particle.solid}"
if hasattr(particle, 'mass'):
particle_info += f" | Mass: {particle.mass}"
if hasattr(particle, 'velocity'):
particle_info += f" | Velocity: {particle.velocity}"
if hasattr(particle, 'friction'):
particle_info += f" | Friction: {particle.friction}"
attrs = ['temperature', 'liquid', 'is_gas', 'solid', 'mass', 'velocity', 'friction']
info = [f"Type: {particle.particle_type}"]
info.extend(f"{attr}: {getattr(particle, attr)}" for attr in attrs
if hasattr(particle, attr))
return " | ".join(info)
return "None"
# 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)
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))
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)
# 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}"
]
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))
y_offset += 25
self.screen.blit(self.debug_surface, (10, 10))
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}"
]
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))
y_offset += 25
def draw_buttons(self): # this is the function that draws the buttons
self.buttons = {}
# Draw category buttons vertically on right
# Draw category buttons on right
x_offset = self.width - 100
y_offset = 10
# Draw cached category buttons
for category, button in self.category_buttons.items():
color = (200, 200, 200) if category == self.current_category else (150, 150, 150)
pygame.draw.rect(self.screen, color, button)
font = pygame.font.SysFont(None, 20)
label = font.render(category, True, (0, 0, 0))
self.screen.blit(label, (button.x + 5, button.y + 5))
surf = self.button_surfaces[category]
if category == self.current_category:
surf.set_alpha(255)
else:
surf.set_alpha(200)
self.screen.blit(surf, button)
# Draw particle buttons for current category
y_offset = 150 # Start particle buttons below categories
for particle_type in self.categories[self.current_category]:
if particle_type in self.particle_properties:
color = self.particle_properties[particle_type].get('color', (255, 255, 255))
button_rect = pygame.Rect(x_offset, y_offset, 80, 25)
self.buttons[particle_type] = button_rect
pygame.draw.rect(self.screen, color, button_rect)
pygame.draw.rect(self.screen, (0, 0, 0), button_rect, 2)
font = pygame.font.SysFont(None, 20)
label = font.render(particle_type, True, (0, 0, 0))
self.screen.blit(label, (x_offset + 5, y_offset + 5))
# Use cached button surface
if particle_type in self.button_surfaces:
self.screen.blit(self.button_surfaces[particle_type], button_rect)
else:
# Create and cache button surface if not exists
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)
label = font.render(particle_type, True, (0, 0, 0))
button_surface.blit(label, (5, 5))
self.button_surfaces[particle_type] = button_surface
self.screen.blit(button_surface, button_rect)
y_offset += 30 # Stack buttons vertically
final_y_offset = y_offset + 10
# Draw clear screen button
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)
label = font.render("Clear", True, (255, 255, 255))
clear_surface.blit(label, (5, 5))
self.button_surfaces['clear'] = clear_surface
self.clear_screen_button = pygame.Rect(x_offset, y_offset + 10, 80, 25)
pygame.draw.rect(self.screen, (255, 0, 0), self.clear_screen_button)
font = pygame.font.SysFont(None, 24)
label = font.render("Clear", True, (255, 255, 255))
self.screen.blit(label, (self.clear_screen_button.x + 5, self.clear_screen_button.y + 5))
self.screen.blit(self.button_surfaces['clear'], self.clear_screen_button)
# Draw Settings menu directly below the buttons
# Draw Settings menu button
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)
label = font.render("Settings", True, (0, 0, 0))
settings_surface.blit(label, (5, 5))
self.button_surfaces['settings'] = settings_surface
self.settings_button = pygame.Rect(x_offset, y_offset + 50, 80, 25)
pygame.draw.rect(self.screen, (255, 255, 255), self.settings_button)
font = pygame.font.SysFont(None, 24)
label = font.render("Settings", True, (0, 0, 0))
self.screen.blit(label, (self.settings_button.x + 5, self.settings_button.y + 5))
self.screen.blit(self.button_surfaces['settings'], self.settings_button)
def render_brush_curser(self, x, y, radius): # this is the function that draws the brush curser but isn't used yet so unkown if works
# Draw a circle cursor for brushsize
pygame.draw.circle(self.screen, (255, 255, 255), (x, y), radius)
def render_brush_cursor(self, x, y, radius):
if engine_settings ['enable_cursor']:
# Draw outline circle
pygame.draw.circle(self.screen, (255, 255, 255), (x, y), radius, 1)
# Draw slightly transparent fill
cursor_surface = pygame.Surface((radius*2, radius*2), pygame.SRCALPHA)
pygame.draw.circle(cursor_surface, (255, 255, 255, 55), (radius, radius), radius)
self.screen.blit(cursor_surface, (x-radius, y-radius))
def draw_brush_size_slider(self, brush_size): # this is the function that draws the brush size slider

View File

@ -1,3 +1,4 @@
"""
#File Name: sandpypi.py
# Sandpypi by Stanton.
# Project name is a placeholder.
@ -5,6 +6,13 @@
# This is my most functional system for falling sand in python yet i took some things i learned in JS.
# This needs further optimizations to core performance sections.
The main function to run the Sandpypi program.
This function initializes the Pygame environment, creates the simulation and rendering objects, and enters the main event loop.
It handles user input events such as mouse clicks, mouse wheel scrolling, and keyboard presses.
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.
"""
from settings import pygame, engine_settings
from rendering import Rendering
from sim import Simulation
@ -31,8 +39,8 @@ def main(): # Main function to run the program
running = True
while running:
fps = clock.get_fps()
dt = clock.tick(60) / 1000
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]
@ -41,6 +49,12 @@ def main(): # Main function to run the program
for event in pygame.event.get():
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)
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
@ -48,45 +62,48 @@ def main(): # Main function to run the program
elif event.button == 1: # Left click
over_button = False
# Check settings button first
if rendering.settings_button.collidepoint(mouse_pos):
settings_visible = not settings_visible
over_button = True
if zoom_active:
zoom_locked = not zoom_locked
if zoom_locked:
zoom_pos = mouse_pos
# Check category buttons
for category, button in rendering.category_buttons.items():
if button.collidepoint(mouse_pos):
rendering.current_category = category
over_button = True
break
# Check particle buttons
if not over_button:
for particle_type, button in rendering.buttons.items():
if button.collidepoint(mouse_pos):
sim.current_particle_type = particle_type
over_button = True
break
# Check clear screen button
if rendering.clear_screen_button.collidepoint(mouse_pos):
rendering.clear_screen(sim)
over_button = True
if rendering.settings_button.collidepoint(event.pos):
settings_visible = not settings_visible
elif settings_visible:
# Handle settings toggles
mouse_pos = pygame.mouse.get_pos()
# Handle settings menu interactions
elif settings_visible and in_settings_area:
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]
over_button = True
elif not in_settings_area:
# Check category buttons
for category, button in rendering.category_buttons.items():
if button.collidepoint(mouse_pos):
rendering.current_category = category
over_button = True
break
# Check particle buttons
if not over_button:
for particle_type, button in rendering.buttons.items():
if button.collidepoint(mouse_pos):
sim.current_particle_type = particle_type
over_button = True
break
# Check clear screen button
if rendering.clear_screen_button.collidepoint(mouse_pos):
rendering.clear_screen(sim)
over_button = True
if not over_button:
if not over_button and not in_settings_area:
mouse_down_left = True
elif event.button == 3: # Right click
@ -136,11 +153,6 @@ def main(): # Main function to run the program
x, y = pygame.mouse.get_pos()
sim.create_particle(x, y)
sim.simulate_step(dt)
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)
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)
@ -149,11 +161,27 @@ def main(): # Main function to run the program
zoom_y = 10 if mouse_pos[1] > height/2 else height - 110
screen.blit(zoom_surface, (zoom_x, zoom_y))
# Draw Settings
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)
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()

View File

@ -1,14 +1,29 @@
"""
#File Name: settings.py
# Global settings and impoorts for the project.
Global settings and imports for the project.
This module defines various settings for the game engine, such as enabling or disabling the cursor, glow effect, gas effect, debug mode, and FPS display. It also provides a function to load particle properties from a JSON file.
The `engine_settings` dictionary contains the configurable settings for the game engine. These settings can be used to customize the behavior of the game.
The `load_particle_properties()` function attempts to load particle properties from a 'particles.json' file. If the file is not found or the JSON data is invalid, it returns an empty dictionary.
The `particle_properties` variable is initialized by calling `load_particle_properties()` when the module is imported.
"""
import pygame
import json
import random
import time
import numpy as np
engine_settings = {
'enable_glow': True,
'enable_gas_effect': True
'enable_cursor': True,
'enable_glow': False,
'enable_gas_effect': True,
'enable_debug': False,
'enable_fps': True
# 'settings': True/False
}

View File

@ -1,8 +0,0 @@
#this be a wip maybe gonna move heavy work loads to cython.
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules=cythonize("simulation_core.pyx"),
)

175
sim.py
View File

@ -1,7 +1,24 @@
"""
#File Name: sim.py
Particle-based Physics Simulation System
======================================
This module implements a 2D particle simulation with physics, interactions, and state changes.
Key Components:
--------------
1. Particle Class
- Handles individual particle properties and behaviors
- Supports multiple particle types (solid, liquid, gas)
- Manages temperature and state transitions
2. Simulation Class
- Core simulation engine
- Manages particle creation, movement and interactions
- Handles physics calculations and spatial partitioning
"""
#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
# Load particle properties from json so we know what particles we got and how they should be simulated.
@ -57,6 +74,9 @@ class Simulation:
# the main class of the simulation.
def __init__(self, width, height, x=0, y=0):
self.dormant_particles = set()
self.particle_movement_counter = {}
self.DORMANT_THRESHOLD = 10
self.x = x
self.y = y
self.new_x = 0
@ -100,15 +120,39 @@ class Simulation:
def update_spatial_grid(self): # this is where we update the spatial grid.
"""Update spatial grid for optimized collision detection"""
if len(self.active_particles) > 100: # Threshold for rebuild
self.spatial_grid.clear()
for x, y in self.active_particles:
cell_key = self.get_cell_key(x, y)
if cell_key not in self.spatial_grid:
self.spatial_grid[cell_key] = set()
self.spatial_grid[cell_key].add((x, y))
if len(self.active_particles) > 100:
self.spatial_grid = {}
cell_lists = {}
for x, y in self.active_particles:
cell_key = (x // self.cell_size, y // self.cell_size)
if cell_key not in cell_lists:
cell_lists[cell_key] = []
cell_lists[cell_key].append((x, y))
self.spatial_grid = {k: set(v) for k, v in cell_lists.items()}
def _check_dormant_state(self, x, y, particle):
key = (x, y)
if particle.particle_type == 'wall':
self.dormant_particles.add(key)
return True
if not hasattr(particle, 'last_position'):
particle.last_position = (x, y)
self.particle_movement_counter[key] = 0
return False
if particle.last_position == (x, y):
self.particle_movement_counter[key] = self.particle_movement_counter.get(key, 0) + 1
if self.particle_movement_counter[key] >= self.DORMANT_THRESHOLD:
self.dormant_particles.add(key)
return True
else:
particle.last_position = (x, y)
self.particle_movement_counter[key] = 0
self.dormant_particles.discard(key)
return False
def handle_phase_transitions(self, particle, x, y): # this is where we handle all the phase transitions.
"""Handle all phase transitions for a particle"""
@ -214,12 +258,12 @@ class Simulation:
particle = self.particles[x][y]
if not particle:
continue
if particle.temperature > 1100:
if particle.temperature > 100:
# Transition to gas
particle.is_gas = True
particle.temperature = 1100
particle.temperature = 100
particle.velocity = [random.uniform(-1, 1), random.uniform(-1, 1)]
particle.temperature < 1100
particle.temperature < 100
particle.is_gas = False
@ -228,8 +272,12 @@ class Simulation:
fx, fy = 0.0, 0.0 # Initialize forces
# Apply wind force
fx += self.wind[0] * (1.0 if not particle.is_gas else 0.5)
fy += self.wind[1] * (1.0 if not particle.is_gas else 0.5)
if particle.is_gas:
fx += self.wind[0] * 0.5
fy += self.wind[1] * 0.5
else:
fx += self.wind[0]
fy += self.wind[1]
# Apply drag force
drag = particle.viscosity * -1
@ -237,23 +285,25 @@ class Simulation:
fy += drag * particle.velocity[1]
# Check neighboring particles
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nx, ny = x + dx, y + dy
neighbors = self._get_quick_neighbors(x, y)
for nx, ny in neighbors:
if 0 <= nx < self.width and 0 <= ny < self.height:
neighbor = self.particles[nx][ny]
if neighbor:
# Temperature effects
if hasattr(neighbor, 'temperature') and hasattr(particle, 'temperature'):
if neighbor.temperature > particle.temperature:
fy += (neighbor.temperature - particle.temperature) * 0.1
# Gas pressure effects
if hasattr(neighbor, 'is_gas') and hasattr(particle, 'is_gas'):
if neighbor.is_gas and not particle.is_gas:
fx += dx * 0.1
fy += dy * 0.1
self._apply_neighbor_forces(particle, neighbor, fx, fy)
return fx, fy
def _get_quick_neighbors(self, x, y):
"""Quick neighbor lookup without full spatial grid"""
return [(x+dx, y+dy) for dx, dy in [(-1,0), (1,0), (0,-1), (0,1)]]
def _apply_neighbor_forces(self, particle, neighbor, fx, fy):
"""Optimized neighbor force calculation"""
if hasattr(neighbor, 'temperature') and hasattr(particle, 'temperature'):
temp_diff = neighbor.temperature - particle.temperature
fy += temp_diff * 0.05
def ignite_particle(self, particle): # this is where we ignite the particle.
"""Handle ignition and burning of flammable particles."""
@ -518,7 +568,7 @@ class Simulation:
# handle gas particles
if particle.is_gas:
# Gas-specific movement
dx = random.uniform(-1, 1)
dx = random.uniform(2, -1)
dy = random.uniform(-2, 0) # Bias upward
new_x = int(x + dx)
new_y = int(y + dy)
@ -540,7 +590,7 @@ class Simulation:
if particle.liquid:
# Enhanced liquid spreading
spread_chance = 0.8
spread_chance = 0.5
if random.random() < spread_chance:
dx = random.choice([-1, 1])
if (0 <= x + dx < self.width and
@ -606,14 +656,75 @@ class Simulation:
count += 1
return count
def get_active_particle_count(self):
"""Returns the number of active particles in the simulation for the Debug display and for performance analysis."""
pass
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)
particle.velocity[0] += (fx / particle.mass) * dt
particle.velocity[1] += (fy / particle.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]:
nx, ny = x + dx, y + dy
key = (nx, ny)
if key in self.dormant_particles:
self.dormant_particles.discard(key)
self.particle_movement_counter[key] = 0
def simulate_step(self, dt):
"""Run a single step of the simulation"""
active_list = list(self.active_particles)
batch_size = 1000
for i in range(0, len(active_list), batch_size):
batch = active_list[i:i + batch_size]
self._process_particle_batch(batch, dt)
# Update spatial grid only when needed
if len(self.active_particles) > 100:
self.update_spatial_grid()
# Update particle positions and physics
self.apply_gravity(dt)
self.apply_physics(dt)
@ -624,5 +735,3 @@ class Simulation:
self.burning()
self.spread_fire()
# Update spatial grid
self.update_spatial_grid()

View File

@ -1,36 +0,0 @@
# simulation_core.pyx
# Cython code for simulating the physics of the system
cimport cython
from libc.math cimport sqrt
cdef class CParticle:
cdef float x, y, vx, vy, mass, gravity
def __init__(self, float x, float y, float vx, float vy, float mass, float gravity):
self.x = x
self.y = y
self.vx = vx
self.vy = vy
self.mass = mass
self.gravity = gravity
cpdef void apply_gravity(self, float dt):
self.vy += self.gravity * dt
self.y += self.vy * dt
self.x += self.vx * dt
cdef class SimulationCore:
cdef list particles
def __init__(self):
self.particles = []
cpdef void add_particle(self, CParticle particle):
self.particles.append(particle)
cpdef void update(self, float dt):
cdef CParticle particle
for particle in self.particles:
particle.apply_gravity(dt)

View File

@ -1,7 +1,8 @@
{
"Template": {
"description": "This is a template for particles.",
"description2": "Remove this for your own particle mods to work"
"description": "Defines the properties and behavior of various particle types, including sand, water, and steam.",
"description2": "This template can be used as a starting point for creating custom particle mods.",
"description3": "Remove 'Template' for your own particle mods to work only particles like below will work."
},
"sand": {
"name": "Sand",