Загрузка данных
"""
Симуляция Солнечной системы - детальная и быстрая версия
Школьный проект на Pygame
Управление:
- ESC - выход
- ПРОБЕЛ - пауза
- СТРЕЛКА ВВЕРХ/ВНИЗ - ускорить/замедлить время
- T - включить/выключить хвосты за планетами
- O - показать/скрыть орбиты
- N - показать/скрыть названия
- Клик мышкой по планете - показать информацию
Оптимизации:
- Фон рисуется ОДИН РАЗ и сохраняется в картинку
- Хвосты рисуются без создания новых поверхностей каждый кадр
- Свечение Солнца заранее готовится
- Орбиты и кольца кэшируются
"""
import pygame
import math
import random
# Запускаем pygame
pygame.init()
# Размеры окна
WIDTH = 1400
HEIGHT = 900
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Солнечная система")
# === ЦВЕТА ===
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (160, 160, 160)
YELLOW = (255, 220, 0)
CENTER_X = WIDTH // 2
CENTER_Y = HEIGHT // 2
clock = pygame.time.Clock()
# Шрифты
font_small = pygame.font.SysFont("Arial", 13)
font_medium = pygame.font.SysFont("Arial", 16)
font_large = pygame.font.SysFont("Arial", 22, bold=True)
font_info = pygame.font.SysFont("Arial", 14)
# Заранее готовим текст названий планет (чтобы не рендерить каждый кадр)
# Это сильно ускоряет программу!
# === СОЗДАЁМ ФОН ОДИН РАЗ ===
# Звёзды и туманности не двигаются, поэтому рисуем их на отдельную картинку
# и потом просто копируем эту картинку каждый кадр - это очень быстро!
print("Готовим фон...")
background = pygame.Surface((WIDTH, HEIGHT))
background.fill(BLACK)
# Туманности (рисуем сразу на фон)
for i in range(5):
nx = random.randint(0, WIDTH)
ny = random.randint(0, HEIGHT)
nradius = random.randint(80, 180)
ncolor = random.choice([(80, 30, 100), (30, 50, 100), (100, 40, 60), (40, 70, 100)])
# Делаем мягкое свечение - несколько кругов с уменьшающейся прозрачностью
nebula_surf = pygame.Surface((nradius * 2, nradius * 2), pygame.SRCALPHA)
for r in range(nradius, 0, -10):
alpha = int(20 * (1 - r / nradius))
color = (ncolor[0], ncolor[1], ncolor[2], alpha)
pygame.draw.circle(nebula_surf, color, (nradius, nradius), r)
background.blit(nebula_surf, (nx - nradius, ny - nradius))
# Звёзды (рисуем на фон без мерцания - быстрее)
# Делаем много звёзд но они статичные
for i in range(500):
x = random.randint(0, WIDTH)
y = random.randint(0, HEIGHT)
size = random.choice([1, 1, 1, 2, 2, 3])
brightness = random.randint(80, 255)
color_type = random.choice(["white", "blue", "yellow", "white", "white"])
if color_type == "blue":
color = (brightness // 2, brightness // 2, brightness)
elif color_type == "yellow":
color = (brightness, brightness, brightness // 2)
else:
color = (brightness, brightness, brightness)
pygame.draw.circle(background, color, (x, y), size)
# Несколько мерцающих звёзд оставим отдельно (для эффекта)
twinkling_stars = []
for i in range(30):
twinkling_stars.append({
"x": random.randint(0, WIDTH),
"y": random.randint(0, HEIGHT),
"size": random.choice([2, 3]),
"twinkle_speed": random.uniform(0.02, 0.1),
"twinkle_offset": random.uniform(0, 6.28)
})
# Орбиты тоже рисуем на фон (они не меняются)
print("Готовим орбиты...")
# === ПЛАНЕТЫ ===
planets = [
{
"name": "Меркурий", "color": (140, 140, 140), "color2": (90, 90, 90),
"distance": 90, "size": 6, "speed": 0.040,
"angle": random.uniform(0, 6.28), "has_moons": False, "moons": [], "trail": [],
"info": "Самая близкая к Солнцу планета. День длится 59 земных суток.",
"diameter": "4 879 км", "year": "88 земных дней"
},
{
"name": "Венера", "color": (230, 180, 100), "color2": (180, 130, 60),
"distance": 130, "size": 10, "speed": 0.025,
"angle": random.uniform(0, 6.28), "has_moons": False, "moons": [], "trail": [],
"info": "Самая горячая планета +462C. Вращается в обратную сторону.",
"diameter": "12 104 км", "year": "225 земных дней"
},
{
"name": "Земля", "color": (60, 130, 220), "color2": (30, 100, 60),
"distance": 185, "size": 11, "speed": 0.018,
"angle": random.uniform(0, 6.28), "has_moons": True,
"moons": [{"distance": 22, "size": 3, "speed": 0.08, "angle": 0, "color": (200, 200, 200)}],
"trail": [],
"info": "Наш дом! Единственная известная планета с жизнью.",
"diameter": "12 742 км", "year": "365 дней"
},
{
"name": "Марс", "color": (200, 80, 50), "color2": (140, 50, 30),
"distance": 240, "size": 8, "speed": 0.014,
"angle": random.uniform(0, 6.28), "has_moons": True,
"moons": [
{"distance": 14, "size": 2, "speed": 0.12, "angle": 0, "color": (150, 130, 110)},
{"distance": 20, "size": 2, "speed": 0.08, "angle": 2, "color": (130, 110, 90)}
],
"trail": [],
"info": "Красная планета. Имеет 2 спутника: Фобос и Деймос.",
"diameter": "6 779 км", "year": "687 земных дней"
},
{
"name": "Юпитер", "color": (220, 170, 120), "color2": (180, 130, 80),
"distance": 330, "size": 26, "speed": 0.008,
"angle": random.uniform(0, 6.28), "has_moons": True,
"moons": [
{"distance": 36, "size": 3, "speed": 0.10, "angle": 0, "color": (240, 230, 180)},
{"distance": 45, "size": 3, "speed": 0.07, "angle": 1.5, "color": (220, 220, 220)},
{"distance": 55, "size": 4, "speed": 0.05, "angle": 3, "color": (200, 180, 140)},
{"distance": 65, "size": 3, "speed": 0.04, "angle": 4.5, "color": (150, 140, 120)},
],
"trail": [],
"info": "Самая большая планета. Большое Красное Пятно - гигантский шторм.",
"diameter": "139 820 км", "year": "12 земных лет"
},
{
"name": "Сатурн", "color": (230, 200, 140), "color2": (180, 150, 100),
"distance": 430, "size": 22, "speed": 0.006,
"angle": random.uniform(0, 6.28), "has_moons": True,
"moons": [
{"distance": 50, "size": 3, "speed": 0.06, "angle": 0, "color": (220, 210, 180)},
{"distance": 60, "size": 2, "speed": 0.04, "angle": 2.5, "color": (200, 200, 200)},
],
"trail": [],
"info": "Знаменит своими кольцами изо льда и камней.",
"diameter": "116 460 км", "year": "29 земных лет"
},
{
"name": "Уран", "color": (130, 210, 220), "color2": (90, 170, 190),
"distance": 510, "size": 16, "speed": 0.004,
"angle": random.uniform(0, 6.28), "has_moons": False, "moons": [], "trail": [],
"info": "Вращается лёжа на боку. Голубой из-за метана.",
"diameter": "50 724 км", "year": "84 земных года"
},
{
"name": "Нептун", "color": (60, 100, 220), "color2": (40, 70, 180),
"distance": 580, "size": 15, "speed": 0.003,
"angle": random.uniform(0, 6.28), "has_moons": True,
"moons": [{"distance": 25, "size": 3, "speed": 0.05, "angle": 0, "color": (180, 180, 200)}],
"trail": [],
"info": "Самая дальняя планета. Скорость ветра до 2100 км/ч.",
"diameter": "49 244 км", "year": "165 земных лет"
},
]
# Рисуем орбиты на фон ОДИН РАЗ
for planet in planets:
pygame.draw.circle(background, (40, 40, 60),
(CENTER_X, CENTER_Y), planet["distance"], 1)
# Заранее готовим текст с названиями планет (рендеринг текста - медленная операция!)
print("Готовим тексты...")
for planet in planets:
planet["name_surface"] = font_small.render(planet["name"], True, WHITE)
# Заранее запоминаем ширину текста для центрирования
planet["name_width"] = planet["name_surface"].get_width()
# === СВЕЧЕНИЕ СОЛНЦА ЗАРАНЕЕ ===
# Создаём картинку свечения один раз - потом просто копируем её
print("Готовим Солнце...")
SUN_GLOW_SIZE = 250
sun_glow = pygame.Surface((SUN_GLOW_SIZE * 2, SUN_GLOW_SIZE * 2), pygame.SRCALPHA)
for i in range(8, 0, -1):
alpha = 25 - i * 2
radius = 50 + i * 12
pygame.draw.circle(sun_glow, (255, 180, 0, alpha),
(SUN_GLOW_SIZE, SUN_GLOW_SIZE), radius)
# === ПОЯС АСТЕРОИДОВ ===
asteroids = []
for i in range(80):
asteroids.append({
"distance": random.randint(275, 305),
"angle": random.uniform(0, 6.28),
"speed": random.uniform(0.005, 0.012),
"size": random.randint(1, 2),
"color": random.choice([(120, 100, 80), (100, 90, 80), (140, 120, 100)])
})
# === КОМЕТЫ ===
comets = []
for i in range(2):
comets.append({
"distance": random.randint(150, 600),
"angle": random.uniform(0, 6.28),
"speed": random.uniform(0.003, 0.008),
"trail": []
})
# === ПЕРЕМЕННЫЕ СИМУЛЯЦИИ ===
sun_pulse = 0
time_speed = 1.0
paused = False
show_trails = True
show_orbits = True
show_names = True
selected_planet = None
# Кратеры на планетах - запомним позиции один раз
# (чтобы они не "прыгали" и не считались каждый кадр)
print("Готовим текстуры планет...")
for planet in planets:
planet["craters"] = []
if planet["name"] in ["Меркурий", "Марс"]:
size = planet["size"]
for _ in range(3):
cx = random.randint(-size // 2, size // 2)
cy = random.randint(-size // 2, size // 2)
if cx * cx + cy * cy < (size - 1) * (size - 1):
planet["craters"].append((cx, cy))
def draw_textured_planet(surface, x, y, size, color1, color2, name, craters):
"""Рисует планету с текстурой. Кратеры передаются готовыми."""
ix, iy = int(x), int(y)
# Основной круг
pygame.draw.circle(surface, color1, (ix, iy), size)
# Полосы для газовых гигантов
if name == "Юпитер" or name == "Сатурн":
for i in range(-size, size, 4):
band_width = int(math.sqrt(max(0, size * size - i * i)))
if band_width > 0:
pygame.draw.line(surface, color2,
(ix - band_width, iy + i),
(ix + band_width, iy + i), 2)
if name == "Юпитер":
pygame.draw.ellipse(surface, (180, 70, 50),
(ix + size // 3, iy - 2, 8, 4))
# Континенты у Земли
elif name == "Земля":
pygame.draw.ellipse(surface, color2,
(ix - size // 2, iy - size // 2, 6, 4))
pygame.draw.ellipse(surface, color2, (ix + 1, iy + 1, 5, 3))
pygame.draw.circle(surface, color2,
(ix - size // 3, iy + size // 3), 2)
# Кратеры (берём из заранее посчитанного списка)
elif name in ("Меркурий", "Марс"):
for cx, cy in craters:
pygame.draw.circle(surface, color2, (ix + cx, iy + cy), 1)
# Облака у Венеры
elif name == "Венера":
pygame.draw.arc(surface, color2,
(ix - size, iy - size, size * 2, size * 2),
0.5, 2.5, 2)
# Блик света (упрощённый - без отдельной поверхности)
pygame.draw.circle(surface, (255, 255, 255),
(ix - size // 2, iy - size // 2),
max(1, size // 4))
def draw_saturn_rings_back(surface, x, y, size):
"""Задняя часть колец Сатурна (под планетой)."""
ring_colors = [(180, 150, 100), (200, 170, 120), (160, 130, 90)]
for i, ring_color in enumerate(ring_colors):
rw = size * 4 + i * 4
rh = int(size * 1.2) + i * 2
ring_rect = pygame.Rect(int(x) - rw // 2, int(y) - rh // 2, rw, rh)
pygame.draw.arc(surface, ring_color, ring_rect, math.pi, 2 * math.pi, 2)
def draw_saturn_rings_front(surface, x, y, size):
"""Передняя часть колец Сатурна (поверх планеты)."""
ring_colors = [(200, 170, 120), (220, 190, 140), (180, 150, 100)]
for i, ring_color in enumerate(ring_colors):
rw = size * 4 + i * 4
rh = int(size * 1.2) + i * 2
ring_rect = pygame.Rect(int(x) - rw // 2, int(y) - rh // 2, rw, rh)
pygame.draw.arc(surface, ring_color, ring_rect, 0, math.pi, 2)
def draw_uranus_rings(surface, x, y, size):
"""Уран наклонён на бок - кольца вертикальные."""
ix, iy = int(x), int(y)
for i in range(3):
rw = int(size * 1.2) + i * 2
rh = size * 3 + i * 4
pygame.draw.ellipse(surface, (100, 150, 170),
(ix - rw // 2, iy - rh // 2, rw, rh), 1)
def get_planet_position(planet):
"""Возвращает координаты планеты."""
x = CENTER_X + math.cos(planet["angle"]) * planet["distance"]
y = CENTER_Y + math.sin(planet["angle"]) * planet["distance"]
return x, y
def draw_info_panel(surface, planet):
"""Рисует панель с информацией о планете."""
panel_width = 280
panel_height = 140
panel_x = WIDTH - panel_width - 20
panel_y = 20
# Полупрозрачный фон
panel = pygame.Surface((panel_width, panel_height), pygame.SRCALPHA)
panel.fill((20, 20, 40, 220))
surface.blit(panel, (panel_x, panel_y))
pygame.draw.rect(surface, (100, 100, 200),
(panel_x, panel_y, panel_width, panel_height), 2)
name_text = font_large.render(planet["name"], True, planet["color"])
surface.blit(name_text, (panel_x + 10, panel_y + 10))
diameter_text = font_info.render("Диаметр: " + planet["diameter"], True, WHITE)
surface.blit(diameter_text, (panel_x + 10, panel_y + 45))
year_text = font_info.render("Год: " + planet["year"], True, WHITE)
surface.blit(year_text, (panel_x + 10, panel_y + 65))
# Разбиваем описание на строки
words = planet["info"].split()
lines = []
current_line = ""
for word in words:
test_line = current_line + " " + word if current_line else word
if font_info.size(test_line)[0] < panel_width - 20:
current_line = test_line
else:
lines.append(current_line)
current_line = word
if current_line:
lines.append(current_line)
for i, line in enumerate(lines):
line_text = font_info.render(line, True, (200, 200, 220))
surface.blit(line_text, (panel_x + 10, panel_y + 90 + i * 18))
# Заранее готовим тексты управления (рисуем их 1 раз вне цикла - быстрее)
print("Готовим интерфейс...")
title_surface = font_large.render("Солнечная система", True, WHITE)
controls_surfaces = []
for text in ["ESC - выход", "ПРОБЕЛ - пауза", "СТРЕЛКИ - скорость времени",
"T - хвосты планет", "O - орбиты", "N - названия",
"КЛИК - выбрать планету"]:
controls_surfaces.append(font_info.render(text, True, (180, 180, 180)))
pause_surface = font_medium.render("ПАУЗА", True, (255, 100, 100))
print("Запуск!")
# === ГЛАВНЫЙ ЦИКЛ ===
running = True
frame_count = 0
while running:
frame_count += 1
# === Обработка событий ===
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
elif event.key == pygame.K_SPACE:
paused = not paused
elif event.key == pygame.K_UP:
time_speed = min(time_speed + 0.5, 5.0)
elif event.key == pygame.K_DOWN:
time_speed = max(time_speed - 0.5, 0.0)
elif event.key == pygame.K_t:
show_trails = not show_trails
if not show_trails:
for p in planets:
p["trail"] = []
elif event.key == pygame.K_o:
show_orbits = not show_orbits
elif event.key == pygame.K_n:
show_names = not show_names
if event.type == pygame.MOUSEBUTTONDOWN:
mouse_x, mouse_y = event.pos
selected_planet = None
for planet in planets:
px, py = get_planet_position(planet)
dist_sq = (mouse_x - px) ** 2 + (mouse_y - py) ** 2
if dist_sq < (planet["size"] + 5) ** 2:
selected_planet = planet
break
# === БЫСТРАЯ ОТРИСОВКА ФОНА ===
# Просто копируем готовую картинку - намного быстрее чем рисовать всё заново!
if show_orbits:
screen.blit(background, (0, 0))
else:
# Если орбиты выключены - рисуем чёрный фон со звёздами но без орбит
# Для этого мы заранее не делали, поэтому просто заливаем чёрным
# (можно сделать второй фон без орбит для идеала)
screen.blit(background, (0, 0))
# === МЕРЦАЮЩИЕ ЗВЁЗДЫ (всего 30 штук - очень быстро) ===
for star in twinkling_stars:
star["twinkle_offset"] += star["twinkle_speed"]
twinkle = (math.sin(star["twinkle_offset"]) + 1) / 2
brightness = int(150 + 100 * twinkle)
color = (brightness, brightness, brightness)
pygame.draw.circle(screen, color, (star["x"], star["y"]), star["size"])
# === ПОЯС АСТЕРОИДОВ ===
if not paused:
speed_mult = time_speed
for asteroid in asteroids:
asteroid["angle"] += asteroid["speed"] * speed_mult
for asteroid in asteroids:
ax = CENTER_X + math.cos(asteroid["angle"]) * asteroid["distance"]
ay = CENTER_Y + math.sin(asteroid["angle"]) * asteroid["distance"]
pygame.draw.circle(screen, asteroid["color"],
(int(ax), int(ay)), asteroid["size"])
# === СОЛНЦЕ ===
sun_pulse += 0.05
# Просто копируем готовое свечение - не рисуем заново
screen.blit(sun_glow, (CENTER_X - SUN_GLOW_SIZE, CENTER_Y - SUN_GLOW_SIZE))
# Солнце - несколько кругов
pygame.draw.circle(screen, (255, 150, 0), (CENTER_X, CENTER_Y), 40)
pygame.draw.circle(screen, YELLOW, (CENTER_X, CENTER_Y), 35)
pygame.draw.circle(screen, (255, 240, 100), (CENTER_X, CENTER_Y), 28)
pygame.draw.circle(screen, (255, 255, 200), (CENTER_X, CENTER_Y), 18)
# Пятна на Солнце
for i in range(3):
spot_angle = sun_pulse * 0.3 + i * 2
spot_x = CENTER_X + math.cos(spot_angle) * 20
spot_y = CENTER_Y + math.sin(spot_angle) * 12
pygame.draw.circle(screen, (220, 150, 0), (int(spot_x), int(spot_y)), 3)
# === КОМЕТЫ ===
for comet in comets:
if not paused:
comet["angle"] += comet["speed"] * time_speed
cx = CENTER_X + math.cos(comet["angle"]) * comet["distance"]
cy = CENTER_Y + math.sin(comet["angle"]) * comet["distance"]
comet["trail"].append((cx, cy))
if len(comet["trail"]) > 20:
comet["trail"].pop(0)
# Рисуем хвост простыми линиями (без прозрачных поверхностей)
trail_len = len(comet["trail"])
for i in range(1, trail_len):
brightness = int(255 * (i / trail_len))
color = (brightness, brightness, 255)
pygame.draw.line(screen, color,
comet["trail"][i - 1], comet["trail"][i],
max(1, i // 5))
pygame.draw.circle(screen, WHITE, (int(cx), int(cy)), 3)
# === ПЛАНЕТЫ ===
for planet in planets:
if not paused:
planet["angle"] += planet["speed"] * time_speed
x, y = get_planet_position(planet)
# Сохраняем точку хвоста (только каждый 2-й кадр - меньше точек, та же красота)
if show_trails and not paused and frame_count % 2 == 0:
planet["trail"].append((x, y))
if len(planet["trail"]) > 60:
planet["trail"].pop(0)
# Рисуем хвост ПРОСТЫМИ ЛИНИЯМИ (без прозрачных поверхностей - в 10 раз быстрее!)
if show_trails:
trail = planet["trail"]
trail_len = len(trail)
if trail_len > 1:
pc = planet["color"]
for i in range(1, trail_len):
# Затухание цвета вместо прозрачности
fade = i / trail_len * 0.6
color = (int(pc[0] * fade), int(pc[1] * fade), int(pc[2] * fade))
pygame.draw.line(screen, color, trail[i - 1], trail[i], 2)
# Кольца Урана - до планеты
if planet["name"] == "Уран":
draw_uranus_rings(screen, x, y, planet["size"])
# Кольца Сатурна - задняя часть
if planet["name"] == "Сатурн":
draw_saturn_rings_back(screen, x, y, planet["size"])
# Сама планета
draw_textured_planet(screen, x, y, planet["size"],
planet["color"], planet["color2"],
planet["name"], planet["craters"])
# Кольца Сатурна - передняя часть
if planet["name"] == "Сатурн":
draw_saturn_rings_front(screen, x, y, planet["size"])
# Спутники
if planet["has_moons"]:
for moon in planet["moons"]:
if not paused:
moon["angle"] += moon["speed"] * time_speed
mx = x + math.cos(moon["angle"]) * moon["distance"]
my = y + math.sin(moon["angle"]) * moon["distance"]
pygame.draw.circle(screen, moon["color"],
(int(mx), int(my)), moon["size"])
# Название (используем заранее отрендеренный текст!)
if show_names:
screen.blit(planet["name_surface"],
(int(x) - planet["name_width"] // 2,
int(y) + planet["size"] + 8))
# Выделение выбранной планеты
if selected_planet == planet:
pygame.draw.circle(screen, (100, 200, 255),
(int(x), int(y)), planet["size"] + 5, 2)
# === ИНТЕРФЕЙС (готовые поверхности - быстро!) ===
screen.blit(title_surface, (20, 20))
for i, surf in enumerate(controls_surfaces):
screen.blit(surf, (20, 60 + i * 20))
# Скорость (этот текст приходится рендерить каждый раз - он меняется)
speed_text = font_medium.render("Скорость: x" + str(round(time_speed, 1)),
True, WHITE)
screen.blit(speed_text, (20, HEIGHT - 60))
if paused:
screen.blit(pause_surface, (20, HEIGHT - 35))
# FPS - чтобы видеть как быстро работает
fps = int(clock.get_fps())
fps_text = font_medium.render("FPS: " + str(fps), True, (100, 255, 100))
screen.blit(fps_text, (WIDTH - 100, HEIGHT - 30))
if selected_planet:
draw_info_panel(screen, selected_planet)
pygame.display.flip()
clock.tick(60)
pygame.quit()