Skip to content

Commit fc0114c

Browse files
committed
Refactor code | Add some feature
Refactor: Annotation, fix bugs Feature: Hotkeys, now can change engine param from ui
1 parent ca6dd1a commit fc0114c

12 files changed

Lines changed: 445 additions & 209 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
__pycache__
2-
.vscode/
2+
.vscode/
3+
.pytest_cache

gravity_simulator/main.py

Lines changed: 213 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,47 +17,92 @@
1717
- Boundary collision handling with energy damping
1818
- Inelastic merging of colliding bodies
1919
20-
Usage:
21-
python main.py
22-
2320
Controls:
2421
• Left click: Create a new celestial body at mouse position
2522
• Mass/Radius inputs: Configure properties for new bodies
2623
• Hover over object: View detailed physical properties
24+
• Hover over object + "Delete": Delete celestial body
25+
• Num/*: Decrease G by 10, Num/*: Increase G by 10
26+
• Num-: Decrease dt by 0.01, Num+: Increase dt by 0.01
27+
• Spacebar: Pause simulation (dt = 0)
2728
"""
2829

2930
import math
3031
import random
32+
import os
3133

34+
from typing import Optional, List
3235
import pygame
3336

3437
from gravity_simulator.physics import PhysicsEngine
3538
from gravity_simulator.objects import Object
36-
from gravity_simulator.ui import InputBox, draw_grid, find_object_under_mouse
39+
from gravity_simulator.ui import InputBox, find_object_under_mouse
3740
from gravity_simulator.config import (SCREEN_WIDTH, SCREEN_HEIGHT, DEFAULT_MASS,
38-
DEFAULT_RADIUS, GRID_SIZE, GRID_COLOR)
39-
from gravity_simulator.utils import random_color, safe_float_convert
41+
DEFAULT_RADIUS)
42+
from gravity_simulator.utils import apply_color_tint, safe_float_convert
43+
4044

4145
pygame.init()
4246

47+
icon: Optional[pygame.Surface] = None
4348
try:
44-
import os
4549
icon_path = os.path.join("resources", "icons", "gravity.png")
4650
if os.path.exists(icon_path):
4751
icon = pygame.image.load(icon_path)
4852
pygame.display.set_icon(icon)
4953
except pygame.error as e:
5054
print(f"Failed to load icon: {e}")
5155

56+
57+
background: Optional[pygame.Surface] = None
58+
background_offset_x: int = 0
59+
background_offset_y: int = 0
60+
61+
try:
62+
background_path = os.path.join("resources", "background", "back1.png")
63+
if os.path.exists(background_path):
64+
background = pygame.image.load(background_path)
65+
bg_width, bg_height = background.get_size()
66+
scale_factor = max(SCREEN_WIDTH / bg_width, SCREEN_HEIGHT / bg_height)
67+
new_width = int(bg_width * scale_factor)
68+
new_height = int(bg_height * scale_factor)
69+
background = pygame.transform.smoothscale(background, (new_width, new_height))
70+
background_offset_x = (SCREEN_WIDTH - new_width) // 2
71+
background_offset_y = (SCREEN_HEIGHT - new_height) // 2
72+
else:
73+
print(f"Background image not found at: {background_path}")
74+
except pygame.error as e:
75+
print(f"Failed to load background: {e}")
76+
77+
5278
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
5379
pygame.display.set_caption("Physics Simulation")
5480
clock = pygame.time.Clock()
55-
running = True
81+
running: bool = True
5682
font = pygame.font.Font(None, 24)
5783
input_font = pygame.font.Font(None, 32)
5884

5985
engine = PhysicsEngine(G=100.0, dt=0.05)
6086

87+
planet_textures: List[pygame.Surface] = []
88+
texture_names = ["ice.png", "lava.png", "moon.png", "terran.png"]
89+
90+
for texture_name in texture_names:
91+
try:
92+
texture_path = os.path.join("resources", "objects", texture_name)
93+
if os.path.exists(texture_path):
94+
texture = pygame.image.load(texture_path)
95+
texture = texture.convert_alpha()
96+
planet_textures.append(texture)
97+
print(f"Loaded texture: {texture_name}")
98+
else:
99+
print(f"Texture not found: {texture_path}")
100+
except pygame.error as e:
101+
print(f"Failed to load texture {texture_name}: {e}")
102+
103+
print(f"Successfully loaded {len(planet_textures)} textures")
104+
105+
61106
mass_input = InputBox(
62107
position=(SCREEN_WIDTH - 130, 10),
63108
size=(140, 32),
@@ -68,9 +113,30 @@
68113
size=(140, 32),
69114
text=str(DEFAULT_RADIUS)
70115
)
116+
g_input = InputBox(
117+
position=(SCREEN_WIDTH - 130, 90),
118+
size=(140, 32),
119+
text="100.0"
120+
)
121+
dt_input = InputBox(
122+
position=(SCREEN_WIDTH - 130, 130),
123+
size=(140, 32),
124+
text="0.05"
125+
)
126+
127+
original_dt: float = engine.dt
128+
is_paused: bool = False
129+
71130

131+
def draw_background(surface: pygame.Surface) -> None:
132+
"""Draw background image"""
133+
if background:
134+
surface.blit(background, (background_offset_x, background_offset_y))
135+
else:
136+
surface.fill((0, 0, 0))
72137

73-
def create_object(x, y):
138+
139+
def create_object(x: float, y: float) -> None:
74140
"""Create a new celestial body at the specified position
75141
76142
Args:
@@ -80,29 +146,85 @@ def create_object(x, y):
80146
mass = safe_float_convert(mass_input.text, DEFAULT_MASS, 1, 10000)
81147
radius = safe_float_convert(radius_input.text, DEFAULT_RADIUS, 1, 100)
82148

149+
base_image = random.choice(planet_textures) if planet_textures else None
150+
83151
new_object = Object(
84152
x=x,
85153
y=y,
86154
mass=mass,
87155
radius=radius,
88156
vx=random.uniform(-10, 10),
89-
vy=random.uniform(-10, 10)
157+
vy=random.uniform(-10, 10),
158+
base_image=base_image
90159
)
91160

92-
new_object.color = random_color()
161+
new_object.color = (
162+
random.randint(100, 255),
163+
random.randint(100, 255),
164+
random.randint(100, 255)
165+
)
93166

94167
engine.add_particle(new_object)
95168

96169

97-
def draw_object_info(surface, hovered, text_font):
170+
def update_physics_parameters() -> None:
171+
"""Update physics engine parameters from input boxes"""
172+
new_g = safe_float_convert(g_input.text, 100.0, 0.1, 10000.0)
173+
engine.G = new_g
174+
175+
new_dt = safe_float_convert(dt_input.text, 0.05, 0.0, 1.0)
176+
engine.dt = new_dt
177+
178+
179+
def adjust_physics_parameters(g_delta: float = 0, dt_delta: float = 0) -> None:
180+
"""Adjust physics parameters by delta values and update input boxes
181+
182+
Args:
183+
g_delta: Change in G value
184+
dt_delta: Change in dt value
185+
"""
186+
global is_paused, original_dt
187+
188+
if is_paused and dt_delta != 0:
189+
is_paused = False
190+
original_dt = engine.dt
191+
192+
new_g = max(0.1, engine.G + g_delta)
193+
engine.G = new_g
194+
g_input.text = str(round(new_g, 1))
195+
196+
new_dt = max(0.0, engine.dt + dt_delta)
197+
engine.dt = new_dt
198+
dt_input.text = str(round(new_dt, 3))
199+
if new_dt == 0.0:
200+
is_paused = True
201+
202+
203+
def toggle_pause() -> None:
204+
"""Toggle simulation pause state"""
205+
global is_paused, original_dt
206+
207+
if is_paused:
208+
engine.dt = original_dt
209+
dt_input.text = str(round(original_dt, 3))
210+
is_paused = False
211+
else:
212+
original_dt = engine.dt
213+
engine.dt = 0.0
214+
dt_input.text = "0.0"
215+
is_paused = True
216+
217+
218+
def draw_object_info(surface: pygame.Surface, hovered: Optional[Object],
219+
text_font: pygame.font.Font) -> None:
98220
"""Render information panel for the hovered celestial body
99221
100222
Args:
101223
surface: Pygame surface to draw on
102224
hovered: Currently hovered celestial body (or None)
103225
text_font: Font for rendering text
104226
"""
105-
if hovered_object:
227+
if hovered:
106228
info_text = [
107229
f"Mass: {hovered.mass:.1f}",
108230
f"Radius: {hovered.radius:.1f}",
@@ -122,17 +244,46 @@ def draw_object_info(surface, hovered, text_font):
122244
surface.blit(text_surface, (20, 20 + i * 25))
123245

124246

125-
def draw_ui_labels(surface, text_font):
126-
"""Draw UI labels for mass and radius inputs
247+
def draw_ui_labels(surface: pygame.Surface, text_font: pygame.font.Font) -> None:
248+
"""Draw UI labels for all input boxes
127249
128250
Args:
129251
surface: Pygame surface to draw on
130252
text_font: Font for rendering text
131253
"""
132254
mass_label = text_font.render("Mass:", True, (255, 255, 255))
133255
radius_label = text_font.render("Radius:", True, (255, 255, 255))
256+
g_label = text_font.render("G:", True, (255, 255, 255))
257+
dt_label = text_font.render("dt:", True, (255, 255, 255))
258+
134259
surface.blit(mass_label, (SCREEN_WIDTH - 200, 15))
135260
surface.blit(radius_label, (SCREEN_WIDTH - 211, 55))
261+
surface.blit(g_label, (SCREEN_WIDTH - 180, 95))
262+
surface.blit(dt_label, (SCREEN_WIDTH - 185, 135))
263+
264+
265+
def draw_hotkey_info(surface: pygame.Surface, text_font: pygame.font.Font) -> None:
266+
"""Draw hotkey information panel
267+
268+
Args:
269+
surface: Pygame surface to draw on
270+
text_font: Font for rendering text
271+
"""
272+
hotkey_text = [
273+
"Hotkeys:",
274+
"Num/* : G ±10",
275+
"Num-/+: dt ±0.01",
276+
"Space: Pause",
277+
"Delete: Delete Object"
278+
]
279+
280+
text_height = len(hotkey_text) * 20 + 10
281+
pygame.draw.rect(surface, (0, 0, 0), (10, SCREEN_HEIGHT - 120, 220, text_height))
282+
pygame.draw.rect(surface, (100, 100, 100), (10, SCREEN_HEIGHT - 120, 220, text_height), 1)
283+
284+
for i, text in enumerate(hotkey_text):
285+
text_surface = text_font.render(text, True, (200, 200, 200))
286+
surface.blit(text_surface, (15, SCREEN_HEIGHT - 112 + i * 20))
136287

137288

138289
while running:
@@ -148,34 +299,68 @@ def draw_ui_labels(surface, text_font):
148299

149300
mass_input.handle_event(event)
150301
radius_input.handle_event(event)
302+
g_input.handle_event(event)
303+
dt_input.handle_event(event)
151304

152305
if event.type == pygame.MOUSEBUTTONDOWN:
153306
if event.button == 1: # left button
154-
if not mass_input.is_hovered(event.pos) and not radius_input.is_hovered(event.pos):
307+
input_boxes = [mass_input, radius_input, g_input, dt_input]
308+
clicked_on_input = any(box.is_hovered(event.pos) for box in input_boxes)
309+
310+
if not clicked_on_input:
155311
create_object(mouse_x, mouse_y)
156312

157313
elif event.type == pygame.KEYDOWN:
158314
if event.key == pygame.K_DELETE:
159315
if hovered_object:
160316
engine.remove_particle(hovered_object)
317+
elif event.key == pygame.K_RETURN:
318+
update_physics_parameters()
319+
elif event.key == pygame.K_SPACE:
320+
toggle_pause()
321+
elif event.key == pygame.K_KP_MULTIPLY:
322+
adjust_physics_parameters(g_delta=10)
323+
elif event.key == pygame.K_KP_DIVIDE:
324+
adjust_physics_parameters(g_delta=-10)
325+
elif event.key == pygame.K_KP_MINUS:
326+
adjust_physics_parameters(dt_delta=-0.01)
327+
elif event.key == pygame.K_KP_PLUS:
328+
adjust_physics_parameters(dt_delta=0.01)
329+
330+
update_physics_parameters()
161331

162332
engine.update()
163333

164334
mass_input.update()
165335
radius_input.update()
336+
g_input.update()
337+
dt_input.update()
166338

167-
screen.fill((0, 0, 0))
168-
169-
draw_grid(screen, grid_size=GRID_SIZE, grid_color=GRID_COLOR)
339+
draw_background(screen)
170340

171341
for particle in engine.particles:
172342
if particle.active:
173-
pygame.draw.circle(
174-
screen,
175-
particle.color,
176-
(int(particle.x), int(particle.y)),
177-
int(particle.radius)
178-
)
343+
if hasattr(particle, 'base_image') and particle.base_image:
344+
scaled_size = int(particle.radius * 2)
345+
if scaled_size > 0:
346+
scaled_image = pygame.transform.smoothscale(
347+
particle.base_image,
348+
(scaled_size, scaled_size)
349+
)
350+
351+
colored_image = apply_color_tint(scaled_image, particle.color)
352+
353+
screen.blit(
354+
colored_image,
355+
(int(particle.x - particle.radius), int(particle.y - particle.radius))
356+
)
357+
else:
358+
pygame.draw.circle(
359+
screen,
360+
particle.color,
361+
(int(particle.x), int(particle.y)),
362+
int(particle.radius)
363+
)
179364

180365
if hasattr(particle, 'trail') and len(particle.trail) > 1:
181366
pygame.draw.lines(
@@ -190,8 +375,11 @@ def draw_ui_labels(surface, text_font):
190375

191376
mass_input.draw(screen)
192377
radius_input.draw(screen)
378+
g_input.draw(screen)
379+
dt_input.draw(screen)
193380

194381
draw_ui_labels(screen, font)
382+
draw_hotkey_info(screen, font)
195383

196384
pygame.display.flip()
197385
clock.tick(60)

0 commit comments

Comments
 (0)