За этот модуль вы научились загружать картинки, вращать их, листать кадры взрыва и заставлять играть звуки. Но всё это лежало в отдельных кусочках по разным урокам. Сегодня соберём всё вместе: получим три полных файла — make_assets.py, sprites.py и main.py — и одну новую мелочь, которая наведёт порядок в громкости.
Громкость: один регулятор на всё
Представьте звуковой микшер на концерте. Есть общий регулятор громкости и отдельные ползунки у гитары, барабанов и вокала. Итоговая громкость каждого инструмента — это общий уровень, умноженный на его собственный. А кнопка «Mute» просто обнуляет всё сразу. В «Комете» то же самое: общий уровень, уровень выстрела, взрыва, подбора кристалла и музыки. В прошлом уроке всё было проще и грубее: громкость выстрела и музыки стояла числами прямо в коде (0,4 и 0,3), а клавиша M ставила музыку на паузу и проверяла muted перед каждым эффектом. Теперь заменим это одним расчётом громкости.
Официально громкость в pygame — число от 0.0 (тишина) до 1.0 (максимум). Её задают методом Sound.set_volume(...) для коротких эффектов и функцией pygame.mixer.music.set_volume(...) для музыки. Расчёт — чистая арифметика, поэтому вынесем его в logic.py, где нет pygame и где его можно проверить без окна.
Функция effect_volume(master, effect, muted) получает общий уровень master, уровень конкретного звука effect и флаг muted. Если звук выключен, она возвращает 0.0. Иначе перемножает уровни, зажимает результат в диапазон от 0 до 1 (знакомая clamp) и округляет до трёх знаков.
def effect_volume(master, effect, muted):
if muted:
return 0.0
return round(clamp(master * effect, 0.0, 1.0), 3)
Разбор по строкам
if muted: return 0.0— выключенный звук сразу получает нулевую громкость, считать больше нечего.master * effect— общий уровень, умноженный на уровень этого звука: при0.8и0.5получится0.4.clamp(..., 0.0, 1.0)— даже если кто-то ошибся и поставил уровень 2, громкость не выйдет за пределы от 0 до 1.round(..., 3)— округляем до тысячных, чтобы не тащить в игру длинные хвосты вроде0.1108889.
Проверьте функцию отдельно, без окна:
python -c "from logic import effect_volume; print(effect_volume(0.8, 0.5, False), effect_volume(0.8, 0.9, True), effect_volume(2, 1, False))"
0.4 0.0 1.0
План сборки
Меняются четыре файла. Таблица показывает, что где происходит, а ниже каждый файл дан целиком, чтобы вы могли просто сверить свой с образцом.
| Файл | Что в нём |
|---|---|
logic.py | Добавляется effect_volume. Остальное не меняется. |
make_assets.py | Рисует 4 картинки и 6 кадров взрыва, синтезирует 4 звука. Запускается один раз. |
sprites.py | Картинки и звуки берутся из assets/: load_image, load_frames, load_sound. Метеор вращается, добавлен класс Explosion. |
main.py | Подключены взрывы, звуки, музыка и клавиша M. Громкости ставятся через effect_volume. |
Практика: собираем проект
Работаем в папке kometa/, виртуальное окружение включено.
Шаг 1. make_assets.py целиком
Этот скрипт рисует все картинки и синтезирует все звуки. Он не часть игры: запускаете его один раз, и он наполняет папку assets/. Замените файл целиком:
import math
import random
import wave
from array import array
from pathlib import Path
import pygame
from logic import ship_points
ASSETS = Path(__file__).parent / "assets"
EXPLOSION_COUNT = 6
RATE = 22050
def make_ship():
image = pygame.Surface((48, 40), pygame.SRCALPHA)
pygame.draw.circle(image, (255, 170, 60), (24, 35), 4)
pygame.draw.circle(image, (255, 230, 140), (24, 35), 2)
points = ship_points(24, 22, 20)
pygame.draw.polygon(image, (90, 200, 255), points)
pygame.draw.polygon(image, (255, 255, 255), points, 2)
pygame.draw.line(image, (190, 235, 255), (24, 8), (24, 26), 2)
pygame.draw.circle(image, (20, 60, 110), (24, 20), 4)
return image
def make_meteor():
image = pygame.Surface((64, 64), pygame.SRCALPHA)
pygame.draw.circle(image, (120, 124, 140), (32, 32), 31)
for cx, cy, r in [(22, 24, 8), (42, 40, 10), (40, 18, 5)]:
pygame.draw.circle(image, (90, 94, 110), (cx, cy), r)
pygame.draw.circle(image, (70, 74, 90), (cx, cy), r, 2)
pygame.draw.circle(image, (70, 74, 90), (32, 32), 31, 3)
pygame.draw.circle(image, (175, 180, 198), (20, 16), 4)
return image
def make_crystal():
image = pygame.Surface((32, 40), pygame.SRCALPHA)
points = [(16, 2), (30, 20), (16, 38), (2, 20)]
pygame.draw.polygon(image, (60, 220, 170), points)
pygame.draw.polygon(image, (150, 255, 225), [(16, 2), (2, 20), (16, 20)])
pygame.draw.polygon(image, (30, 150, 120), [(16, 20), (30, 20), (16, 38)])
pygame.draw.polygon(image, (255, 255, 255), points, 2)
return image
def make_laser():
image = pygame.Surface((8, 28), pygame.SRCALPHA)
pygame.draw.rect(image, (255, 90, 120), (0, 0, 8, 28), border_radius=4)
pygame.draw.rect(image, (255, 230, 235), (3, 3, 2, 22), border_radius=1)
return image
def make_explosion_frame(index, count):
t = index / (count - 1)
radius = 10 + int(t * 36)
alpha = int(255 - t * 204)
image = pygame.Surface((96, 96), pygame.SRCALPHA)
center = (48, 48)
pygame.draw.circle(image, (255, 120, 40, alpha), center, radius)
pygame.draw.circle(image, (255, 200, 80, alpha), center, int(radius * 0.7))
pygame.draw.circle(image, (255, 245, 200, alpha), center, int(radius * 0.35))
return image
def save(image, name):
pygame.image.save(image, ASSETS / name)
def tone_samples(freq, duration, rate, volume):
count = int(duration * rate)
samples = []
for i in range(count):
t = i / rate
value = math.sin(2 * math.pi * freq * t) * volume
samples.append(int(value * 32767))
return samples
def noise_samples(duration, rate, volume):
count = int(duration * rate)
samples = []
for _ in range(count):
value = random.uniform(-1, 1) * volume
samples.append(int(value * 32767))
return samples
def fade_out(samples):
count = len(samples)
result = []
for i in range(count):
k = 1 - i / count
result.append(int(samples[i] * k))
return result
def note_freq(semitones):
return 440 * 2 ** (semitones / 12)
def save_wav(path, samples, rate):
with wave.open(str(path), "wb") as f:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(rate)
f.writeframes(array("h", samples).tobytes())
ASSETS.mkdir(exist_ok=True)
save(make_ship(), "ship.png")
save(make_meteor(), "meteor.png")
save(make_crystal(), "crystal.png")
save(make_laser(), "laser.png")
for i in range(EXPLOSION_COUNT):
save(make_explosion_frame(i, EXPLOSION_COUNT), "explosion_" + str(i) + ".png")
shot = fade_out(tone_samples(880, 0.15, RATE, 0.5))
boom = fade_out(noise_samples(0.5, RATE, 0.6))
pickup = tone_samples(660, 0.08, RATE, 0.4) + fade_out(tone_samples(990, 0.14, RATE, 0.4))
melody = [0, 3, 7, 3, 0, 3, 7, 12, 10, 7, 5, 7, 3, 0, 3, 0]
music = []
for n in melody:
note = tone_samples(note_freq(n), 0.25, RATE, 0.3)
music = music + fade_out(note)
save_wav(ASSETS / "shot.wav", shot, RATE)
save_wav(ASSETS / "boom.wav", boom, RATE)
save_wav(ASSETS / "pickup.wav", pickup, RATE)
save_wav(ASSETS / "music.wav", music, RATE)
print("Картинки и звуки сохранены в", ASSETS)
Разбор по строкам
ASSETS,EXPLOSION_COUNT,RATE— папка для файлов, число кадров взрыва и число отсчётов звука в секунду.make_ship,make_meteor,make_crystal,make_laser— рисуют картинки на прозрачных поверхностях.make_explosion_frame(index, count)— один кадр взрыва: чем больше номер, тем крупнее и прозрачнее круг.saveиsave_wav— записывают картинку и звук в файл.tone_samples,noise_samples,fade_out,note_freq— строительные блоки звука: тон, шум, затухание, частота ноты.ASSETS.mkdir(exist_ok=True)— создаёт папку, если её ещё нет; дальше идут сохранения картинок, кадров взрыва и четырёх звуков.
Запустите и посмотрите, что получилось:
python make_assets.py
ls assets
На Windows вместо ls assets напишите dir assets. В конце должна появиться строка Картинки и звуки сохранены в ..., а в папке — 14 файлов:
boom.wav explosion_1.png explosion_5.png pickup.wav
crystal.png explosion_2.png laser.png ship.png
explosion_0.png explosion_3.png meteor.png shot.wav
explosion_4.png music.wav
Пока скрипт не запускался, этих файлов нет.
Папка assets обязательна
Пока make_assets.py не запускался, игра не стартует: load_image не найдёт файл и сообщит о FileNotFoundError. Если вы изменили рисунок или мелодию, запустите скрипт снова — он перезапишет файлы.
Шаг 2. sprites.py целиком
Теперь спрайты: все картинки берём из файлов, метеор вращается, взрыв листает кадры. Замените файл целиком:
import random
from pathlib import Path
import pygame
from logic import direction_from_keys, frame_index, step_ship
ASSETS = Path(__file__).parent / "assets"
METEOR_MIN_SIZE = 20
METEOR_MAX_SIZE = 48
PLAYER_SPEED = 360
PLAYER_RADIUS = 16
CRYSTAL_RADIUS = 14
CRYSTAL_SPEED = 90
OFFSCREEN_MARGIN = 40
LASER_SPEED = 700
EXPLOSION_FRAME_TIME = 0.06
def load_image(name):
return pygame.image.load(ASSETS / name).convert_alpha()
def load_sound(name):
return pygame.mixer.Sound(str(ASSETS / name))
def load_frames(prefix, count):
frames = []
for i in range(count):
frames.append(load_image(f"{prefix}_{i}.png"))
return frames
class Player(pygame.sprite.Sprite):
def __init__(self, x, y, bounds):
super().__init__()
self.image = load_image("ship.png")
self.rect = self.image.get_rect()
self.radius = PLAYER_RADIUS
self.pos = pygame.Vector2(x, y)
self.bounds = bounds
self.rect.center = (round(self.pos.x), round(self.pos.y))
def update(self, dt, keys):
left = keys[pygame.K_LEFT] or keys[pygame.K_a]
right = keys[pygame.K_RIGHT] or keys[pygame.K_d]
up = keys[pygame.K_UP] or keys[pygame.K_w]
down = keys[pygame.K_DOWN] or keys[pygame.K_s]
dx, dy = direction_from_keys(left, right, up, down)
x, y = step_ship(self.pos.x, self.pos.y, dx, dy, PLAYER_SPEED, dt, self.bounds)
self.pos.x, self.pos.y = x, y
self.rect.center = (round(self.pos.x), round(self.pos.y))
class Meteor(pygame.sprite.Sprite):
def __init__(self, width, height):
super().__init__()
self.size = random.randint(METEOR_MIN_SIZE, METEOR_MAX_SIZE)
self.radius = self.size // 2
self.original = pygame.transform.scale(load_image("meteor.png"), (self.size, self.size))
self.image = self.original
self.rect = self.image.get_rect()
self.height = height
x = random.randint(self.radius, width - self.radius)
self.pos = pygame.Vector2(x, -self.radius)
self.vel = pygame.Vector2(0, random.randint(150, 260))
self.angle = 0
self.spin = random.randint(-90, 90)
self.rect.center = (round(self.pos.x), round(self.pos.y))
def update(self, dt, *args):
self.pos += self.vel * dt
self.angle += self.spin * dt
self.image = pygame.transform.rotate(self.original, self.angle)
self.rect = self.image.get_rect(center=(round(self.pos.x), round(self.pos.y)))
if self.pos.y > self.height + OFFSCREEN_MARGIN:
self.kill()
class Crystal(pygame.sprite.Sprite):
def __init__(self, width, height):
super().__init__()
self.radius = CRYSTAL_RADIUS
self.image = load_image("crystal.png")
self.rect = self.image.get_rect()
self.height = height
x = random.randint(self.radius, width - self.radius)
self.pos = pygame.Vector2(x, -self.radius)
self.vel = pygame.Vector2(0, CRYSTAL_SPEED)
self.rect.center = (round(self.pos.x), round(self.pos.y))
def update(self, dt, *args):
self.pos += self.vel * dt
self.rect.center = (round(self.pos.x), round(self.pos.y))
if self.pos.y > self.height + OFFSCREEN_MARGIN:
self.kill()
class Laser(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
self.image = load_image("laser.png")
self.rect = self.image.get_rect()
self.rect.midbottom = (x, y)
self.pos = pygame.Vector2(self.rect.center)
self.vel = pygame.Vector2(0, -LASER_SPEED)
def update(self, dt, *args):
self.pos += self.vel * dt
self.rect.center = (round(self.pos.x), round(self.pos.y))
if self.pos.y < -OFFSCREEN_MARGIN:
self.kill()
class Explosion(pygame.sprite.Sprite):
def __init__(self, center, frames):
super().__init__()
self.frames = frames
self.elapsed = 0.0
self.image = frames[0]
self.rect = self.image.get_rect(center=center)
def update(self, dt, *args):
self.elapsed += dt
index = frame_index(self.elapsed, EXPLOSION_FRAME_TIME, len(self.frames), False)
if index is None:
self.kill()
else:
self.image = self.frames[index]
Разбор по строкам
from logic import direction_from_keys, frame_index, step_ship— функцииlogic.py, которые нужны спрайтам;ship_pointsздесь больше не нужен, корабль нарисован заранее.ASSETS = Path(__file__).parent / "assets"— путь к папке рядом с файлом, откуда бы вы ни запустили игру.load_image,load_sound,load_frames— три загрузчика: картинка, звук и список кадровexplosion_0.png...explosion_5.png.CRYSTAL_RADIUSиCRYSTAL_SPEEDостались: картинка кристалла теперь из файла, но радиус для кругов столкновения и скорость падения по-прежнему нужны.Meteor.original,angle,spin— исходная картинка, текущий угол и скорость вращения. Каждый кадр изoriginalповорачивается заново, поэтому картинка не портится.Explosion— хранитelapsed;frame_indexпо нему выбирает кадр, а когда кадры кончились (None), спрайт делаетkill().
Шаг 3. main.py целиком
Главный файл. Новое: effect_volume в импортах, константы громкости, звуки и музыка до цикла, функции apply_volumes, play_sound и explode, клавиша M, вызовы play_sound в нужных местах цикла. Из прошлого урока уходят отдельные set_volume(0.4) и set_volume(0.3), проверка muted внутри play_sound и пара pause() / unpause(): всё это теперь делает apply_volumes. Замените файл целиком:
import random
import pygame
from logic import Cooldown, SpawnTimer, blink_visible, effect_volume, wrap
from sprites import ASSETS, Crystal, Explosion, Laser, Meteor, Player, load_frames, load_sound
WIDTH = 960
HEIGHT = 640
FPS = 60
TITLE = "Комета"
BG_COLOR = (12, 14, 32)
WHITE = (255, 255, 255)
BOX_COLOR = (80, 255, 120)
CIRCLE_COLOR = (255, 220, 80)
SHIP_HALF = 24
SHIP_MIN_Y = HEIGHT // 2
SHIP_MAX_Y = HEIGHT - 40
SHIP_BOUNDS = (SHIP_HALF, SHIP_MIN_Y, WIDTH - SHIP_HALF, SHIP_MAX_Y)
STAR_SPEED = 40
SPAWN_INTERVAL = 0.8
CRYSTAL_INTERVAL = 2.5
SHOT_COOLDOWN = 0.25
START_LIVES = 3
INVULNERABLE_TIME = 1.5
BLINK_PERIOD = 0.1
METEOR_SCORE = 10
CRYSTAL_SCORE = 25
EXPLOSION_COUNT = 6
MASTER_VOLUME = 0.8
EFFECT_LEVELS = {"shot": 0.5, "boom": 0.9, "pickup": 0.7}
MUSIC_LEVEL = 0.4
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption(TITLE)
clock = pygame.time.Clock()
font = pygame.font.Font(None, 32)
explosion_frames = load_frames("explosion", EXPLOSION_COUNT)
sounds = {
"shot": load_sound("shot.wav"),
"boom": load_sound("boom.wav"),
"pickup": load_sound("pickup.wav"),
}
muted = False
pygame.mixer.music.load(str(ASSETS / "music.wav"))
pygame.mixer.music.play(-1)
def apply_volumes():
for name, sound in sounds.items():
sound.set_volume(effect_volume(MASTER_VOLUME, EFFECT_LEVELS[name], muted))
pygame.mixer.music.set_volume(effect_volume(MASTER_VOLUME, MUSIC_LEVEL, muted))
def play_sound(name):
sounds[name].play()
apply_volumes()
def draw_text(surface, text, pos, color=WHITE):
image = font.render(text, True, color)
surface.blit(image, pos)
stars = []
for _ in range(120):
stars.append([random.randint(0, WIDTH), random.randint(0, HEIGHT), random.randint(1, 3)])
player = Player(WIDTH // 2, HEIGHT - 80, SHIP_BOUNDS)
all_sprites = pygame.sprite.Group()
meteors = pygame.sprite.Group()
crystals = pygame.sprite.Group()
lasers = pygame.sprite.Group()
spawn_timer = SpawnTimer(SPAWN_INTERVAL)
crystal_timer = SpawnTimer(CRYSTAL_INTERVAL)
shot_cooldown = Cooldown(SHOT_COOLDOWN)
def explode(center):
all_sprites.add(Explosion(center, explosion_frames))
play_sound("boom")
score = 0
lives = START_LIVES
invulnerable = 0.0
show_boxes = False
running = True
while running:
dt = clock.tick(FPS) / 1000
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
elif event.key == pygame.K_F1:
show_boxes = not show_boxes
elif event.key == pygame.K_m:
muted = not muted
apply_volumes()
keys = pygame.key.get_pressed()
invulnerable = max(0.0, invulnerable - dt)
shot_cooldown.update(dt)
if keys[pygame.K_SPACE] and shot_cooldown.ready():
nose_x, nose_y = player.rect.midtop
laser = Laser(nose_x, nose_y)
all_sprites.add(laser)
lasers.add(laser)
shot_cooldown.start()
play_sound("shot")
for _ in range(spawn_timer.update(dt)):
meteor = Meteor(WIDTH, HEIGHT)
all_sprites.add(meteor)
meteors.add(meteor)
for _ in range(crystal_timer.update(dt)):
crystal = Crystal(WIDTH, HEIGHT)
all_sprites.add(crystal)
crystals.add(crystal)
player.update(dt, keys)
all_sprites.update(dt, keys)
hits = pygame.sprite.spritecollide(player, meteors, True, pygame.sprite.collide_circle)
for meteor in hits:
explode(meteor.rect.center)
if hits and invulnerable <= 0:
lives -= 1
invulnerable = INVULNERABLE_TIME
caught = pygame.sprite.spritecollide(player, crystals, True, pygame.sprite.collide_circle)
score += CRYSTAL_SCORE * len(caught)
if caught:
play_sound("pickup")
shots = pygame.sprite.groupcollide(lasers, meteors, True, True)
for hit_meteors in shots.values():
score += METEOR_SCORE * len(hit_meteors)
for meteor in hit_meteors:
explode(meteor.rect.center)
if lives <= 0:
print("Игра окончена. Очки:", score)
running = False
for star in stars:
star[1] = wrap(star[1] + STAR_SPEED * star[2] * dt, 0, HEIGHT)
screen.fill(BG_COLOR)
for x, y, size in stars:
pygame.draw.circle(screen, WHITE, (x, y), size)
all_sprites.draw(screen)
if blink_visible(invulnerable, BLINK_PERIOD):
screen.blit(player.image, player.rect)
if show_boxes:
for sprite in all_sprites:
pygame.draw.rect(screen, BOX_COLOR, sprite.rect, 2)
pygame.draw.rect(screen, BOX_COLOR, player.rect, 2)
pygame.draw.circle(screen, CIRCLE_COLOR, player.rect.center, player.radius, 1)
for meteor in meteors:
pygame.draw.circle(screen, CIRCLE_COLOR, meteor.rect.center, meteor.radius, 1)
draw_text(screen, "Очки: " + str(score), (16, 12))
draw_text(screen, "Жизни: " + str(lives), (16, 44))
pygame.display.flip()
pygame.quit()
Разбор по строкам
MASTER_VOLUME,EFFECT_LEVELS,MUSIC_LEVEL— все уровни громкости в одном месте: общий, отдельный для каждого эффекта и для музыки.sounds— словарь звуков: ключ-имя, значение — загруженныйSound. Загружаем один раз до цикла.pygame.mixer.music.play(-1)— музыка запускается один раз до цикла и сама повторяется (-1— бесконечно).apply_volumes()— проходит по всем звукам и ставит каждомуset_volume(effect_volume(...)), потом то же для музыки. Вызывается при старте и при каждом нажатии M.play_sound(name)— теперь просто проигрывает звук: проверятьmutedне нужно, у выключенного звука громкость 0.0.explode(center)— создаётExplosionв центре метеора и играет"boom". Зовётся и при столкновении с кораблём, и при попадании лазера.play_sound("shot")— сразу после создания лазера и запуска перезарядки: один звук на один выстрел, а не на кадр.if caught: play_sound("pickup")— звук кристалла один раз за кадр, где что-то поймано.elif event.key == pygame.K_m— переключаетmutedи пересчитывает громкости. Музыка при этом не останавливается, а становится беззвучной, поэтому после включения играет с того же места.
Когда играть звук
Звуки играют ровно там, где происходит событие: выстрел — рядом с созданием лазера, взрыв — в explode, кристалл — после проверки caught. Правило простое: событие случилось один раз — play() вызвали один раз.
Шаг 4. Запуск
python main.py
Что должно получиться: фоном играет мелодия; корабль, метеоры, кристалл и лазер — настоящие картинки; метеоры вращаются; при попадании лазера или столкновения на месте метеора вспыхивает и гаснет взрыв с глухим шумом. Выстрел щёлкает высоким тоном, кристалл звенит. Клавиша M выключает всё сразу, повторное нажатие возвращает звук.
Проверьте себя
- В папке
assets/14 файлов: четыре картинки, шесть кадров взрыва и четыре звука. - Метеоры вращаются в разные стороны, а края картинок остаются чистыми (прозрачными).
- Каждый взрыв проигрывается один раз и исчезает; на экране не накапливаются старые кадры.
- Зажатый Пробел даёт «пулемёт» по одному выстрелу каждые 0,25 с, а не сплошной гул.
- Клавиша M выключает и музыку, и эффекты, повторное нажатие всё возвращает. Музыка не начинается заново.
Частые ошибки
FileNotFoundError при запуске игры
- Что видно: в терминале ошибка с именем файла вроде
assets/ship.png, окно не открывается. - Почему так: не запускали
make_assets.py, либо он упал с ошибкой раньше, чем сохранил файлы, либо в имени файла опечатка. - Как исправить: перейдите в папку
kometa/, выполнитеpython make_assets.py, затемpython main.py.
Музыка начинается заново и звучит «заикаясь»
- Что видно: мелодия постоянно обрывается на первой ноте.
- Почему так:
pygame.mixer.music.play(-1)стоит внутри цикла, и музыка перезапускается каждый кадр. - Как исправить: вызывайте
loadиplay(-1)один раз до цикла.
Громкость больше единицы или звук не выключается
- Что видно: звук хрипит, либо после M что-то продолжает играть.
- Почему так: в
set_volumeподставили число вне диапазона от 0 до 1, или забыли вызватьapply_volumes()после сменыmuted. - Как исправить: считайте громкость только через
effect_volume(она зажимает результат) и вызывайтеapply_volumes()при каждом переключении.
Метеор вращается и «дрожит» или растёт
- Что видно: картинка расплывается и становится больше с каждым кадром.
- Почему так: поворачивали уже повёрнутую картинку (
self.image), а не исходную. - Как исправить: храните
originalи каждый кадр поворачивайте именно её.
Что важно запомнить
- Ассеты создаются отдельным скриптом
make_assets.pyодин раз; игра только загружает файлы изassets/. - Файлы и звуки грузим до цикла, один раз; в цикле только показываем и вызываем
play(). - Громкость — число от 0 до 1.
effect_volume(master, effect, muted)перемножает уровни, зажимает результат и приmutedдаёт 0.0. - Музыка включается один раз (
play(-1)), а выключается нулевой громкостью: она не теряет место. - Событие произошло один раз — звук сыграли один раз: рядом с выстрелом, взрывом, подбором.
Игра выглядит и звучит как настоящая, но она стартует сразу, без меню, и так же сразу заканчивается. В следующем модуле добавим меню, паузу и экран «Игра окончена»: научимся хранить состояние игры.