Загрузка картинок и 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Не имеет значения