Загрузка картинок и blit

Пора заменить серые коробки на настоящие картинки. Загрузка изображений в Pygame — пара строк, но в них прячется важная оптимизация.
Суть: картинку грузят через pygame.image.load, превращают в быстрый формат через convert/convert_alpha и рисуют через screen.blit. Прозрачность даёт convert_alpha.

Серые прямоугольники отлично работают для прототипа, но игру хочется видеть красивой. Картинка в Pygame называется Surface — это «поверхность», набор пикселей, который можно нарисовать на экране. Загрузить её просто: pygame.image.load("hero.png"). Но есть нюанс, который отличает плавную игру от тормозящей.

Когда ты загружаешь PNG, он хранится в формате файла, не очень удобном для быстрой отрисовки. Метод convert() перегоняет картинку в формат экрана, и рисоваться она будет в разы быстрее. Если у картинки есть прозрачные области (а у спрайтов почти всегда есть), используй convert_alpha() — он сохранит прозрачность. Это не каприз: без convert игра из десятка спрайтов может ощутимо тормозить.

Нарисовать картинку на экране — это blit (от block transfer, «перенос блока пикселей»). Ты говоришь: возьми вот эту поверхность и впечатай её в окно в точке (x, y).

Как работает под капотом

blit копирует пиксели одной поверхности на другую. Позиция (x, y) — это левый верхний угол картинки на экране. Прозрачные пиксели при convert_alpha пропускаются, поэтому видно фон под спрайтом:

   load("hero.png")  -- читаем файл с диска
         |
         v
   convert_alpha()   -- быстрый формат + прозрачность
         |
         v
   screen.blit(img, (x, y))  -- впечатываем в окно
         |
         v
   видно картинку, прозрачные пиксели = фон

Полный пример загрузки и отрисовки (читаем):

# грузим один раз ДО цикла, а не каждый кадр!
hero_img = pygame.image.load("hero.png").convert_alpha()
bg_img = pygame.image.load("bg.png").convert()

while running:
    screen.blit(bg_img, (0, 0))       # фон в углу
    screen.blit(hero_img, (350, 250)) # герой
    pygame.display.flip()

Картинку часто нужно отмасштабировать или повернуть — это тоже Surface-операции (pygame.transform.scale, rotate). Логику выбора кадра спрайта (например, какой кадр анимации показать) можно проверить без графики. Попробуй сам — простой выбор кадра по времени:

# Какой кадр анимации показать в зависимости от времени
FRAMES = ["шаг1", "шаг2", "шаг3", "шаг4"]
FRAME_TIME = 0.1   # секунд на кадр

def current_frame(elapsed):
    index = int(elapsed / FRAME_TIME) % len(FRAMES)
    return FRAMES[index]

for t in (0.0, 0.05, 0.12, 0.25, 0.41):
    print(f"t={t:.2f}s -> кадр {current_frame(t)}")

Лист спрайтов и анимация

Хранить каждый кадр анимации отдельным файлом неудобно — их могут быть сотни. Поэтому художники складывают все кадры в одну большую картинку — лист спрайтов (sprite sheet), как полоску кадров киноплёнки. В коде мы вырезаем нужный кадр методом subsurface(область) или передаём прямоугольник-вырез прямо в blit третьим аргументом. Каждый кадр анимации — это просто другой прямоугольник на той же картинке, и мы переключаем их по таймеру, как листали флипбук в первом уроке.

Такой подход экономит и память, и время загрузки: один файл вместо сотни, одна команда convert вместо множества. А смена кадров сводится к арифметике: номер кадра равен времени, поделённому на длительность кадра, по модулю количества кадров — ровно та формула, которую ты запускал выше. Анимация, которая выглядит сложной магией, на деле оказывается выбором прямоугольника по часам. Это отличный пример того, как в геймдеве за «крутым эффектом» почти всегда прячется простая, проверяемая логика.

Заведи привычку собирать все ассеты в словарь при старте игры: images = {"hero": load("hero.png"), "coin": load("coin.png")}. Тогда по всему коду ты обращаешься к картинке по понятному имени, а не таскаешь переменные, и легко видишь полный список ресурсов игры в одном месте. Пути к файлам собирай через os.path.join и считай их от расположения программы, а не от текущей папки запуска — иначе игра, запущенная из другого каталога или упакованная в exe, не найдёт свои картинки. Это одна из самых частых причин загадочного «у меня работает, а у друга нет». Аккуратная загрузка ассетов в начале — скучная, но крайне благодарная привычка, экономящая часы будущей отладки.

Частые ошибки

  • Грузить картинку внутри цикла — диск читается каждый кадр, игра дико тормозит. Грузи один раз до цикла.
  • Забыть convert_alpha() — прозрачный PNG нарисуется с чёрным квадратом вокруг.
  • Вызвать convert до создания окна — ошибка, ведь формат экрана ещё неизвестен.

Best practices

  • Грузи все ассеты один раз при старте, складывай в словарь или переменные.
  • Для спрайтов с прозрачностью — всегда convert_alpha(), для фона — convert().
  • Держи картинки в папке assets/ и собирай путь через os.path.join.

Итог: load → convert_alpha → blit. Грузи один раз, рисуй каждый кадр — и серые коробки превращаются в настоящих героев.

Проверьте себя
1. Зачем после image.load вызывают convert_alpha()?
AЧтобы уменьшить файл
BЧтобы ускорить отрисовку и сохранить прозрачность
CЧтобы поменять цвет
DЭто необязательно никогда
2. Где правильно загружать картинку героя?
AВнутри игрового цикла, каждый кадр
BОдин раз до цикла
CВ обработчике QUIT
DНе имеет значения
py
Курс по теме
Пройдите курс «Python с нуля» — по шагам, с проверкой
8 уроков · ~14 ч · теория, упражнения и экзамен с бейджем
Открыть курс →