Python + pygame — небольшие оптимизации

от автора

Предисловие

Приветствую IT-специалистов, решивших прочитать мою статью, я — python разработчик любитель (изучающий язык уже 5 лет, но не имеющий коммерческого опыта), который решил создать игру на очень медленном языке программирования. В процессе разработки я сталкиваюсь с различными проблемами и в мой мозг приходят весьма интересные решения для оптимизации, которыми я бы хотел поделиться с теми, кто тоже захотел написать крутую игру на python.

Оптимизация делится на оптимизацию времени исполнения и на оптимизацию использования памяти, в этой статье рассматриваются оптимизации, нацеленные в первую очередь на первый пункт. Так же хотел бы отметить, что методы оптимизации, описанные в статье могут быть несовместимыми либо с вашим проектом либо друг с другом, поэтому внимательно изучите тему каждой оптимизации, возможно вы сможете модифицировать метод под свои условия и получите более хорошие результаты.

1 — оптимизация мат. вычислений путём jit-компиляции

Python знаменит своим количеством библиотек для самых разных потребностей, одной из таких библиотек является numba — библиотека для jit-компиляции.

JIT-компиляция (Just In Time) — метод компиляции фрагментов кода прямо во время исполнения программы для повышения скорости работы (моё определение).

Всё что нужно сделать, это вынести математические вычисления в отдельные функции, которые нужно всего лишь задекорировать с помощью numba.njit:

import numba  @numba.njit(cache=True) def linear_interpolation(X0: int, X1: int, p: float) -> float:   return x0 + (x1-x0)*p # example linear interpolation method - math calculations

Обратите внимание, на то, что я передал параметр cache=True в декоратор, а также использовал аннотации в коде — это ускоряет время компиляции при повторных использованиях функции.

2 — установка разрешённых событий

Следующим небольшим решением для оптимизации будет установка разрешённых событий в pygame.event. Дело в том, что по умолчанию система ивентов pygame отслеживает достаточно много событий, часть которых вы, возможно, даже не используете. Чтобы убрать их обработку и не выполнять ненужные действия достаточно лишь одной дополнительной строки кода:

import pygame as pg pg.event.set_allowed([pg.QUIT]) # в список поместите события, которые отслеживаются в вашем коде

3 — оптимизация отрисовки

В pygame есть метод blit, который помогает отрисовать то, что вам нужно, там, где вам нужно. Однако, использование этого метода слишком много раз за цикл может сказаться на производительности. В качестве решения этой проблемы можно «собирать» текстуры в текстурные чанки и рисовать сразу их, уменьшая количество вызовов blit на каждом кадре

Пример неоптимизированного кода:

import pygame as pg  window = pg.display.set_mode((500, 500))  img50x50 = pg.image.load("image.png") # изображение 50 на 50 пикселей  while True:     window.fill((0, 0, 0))     # проверка на QUIT event через генератор     [pg.quit() & exit(0) for event in pg.event.get() if event.type == pg.QUIT]      for x in range(10):         for y in range(10):             window.blit(img50x50, (x*50, y*50))     pg.display.flip()

в данном примере метод blit вызывается 100 раз за кадр, а в вашем коде он может вызываться ещё чаще, ниже пример «сборки» текстур:

import pygame as pg  window = pg.display.set_mode((500, 500))  img50x50 = pg.image.load("image.png") # изображение 50 на 50 пикселей  # собираем текстуры на одну большую текстуру chunked = pg.Surface((500, 500)) for x in range(10):     for y in range(10):         chunked.blit(img50x50, (x*50, y*50))  while True:     window.fill((0, 0, 0))     [pg.quit() & exit(0) for event in pg.event.get() if event.type == pg.QUIT] # проверка на QUIT event     window.blit(chunked, (0, 0)) # вызываем blit 1 раз для отрисовки     pg.display.flip()

Во втором примере blit вызывается всего 1 раз, пусть и для более большой текстуры, ЭТО ПОЧТИ В 100 РАЗ БЫСТРЕЕ ЧЕМ В ПЕРВОМ ПРИМЕРЕ!

4 — использование numpy для отрисовки

Numpy — библиотека для более эффективных вычислений, которая помогает вычислять много и быстро, так почему же не использовать её в pygame для отрисовки?:

import pygame as pg import numpy as np from time import perf_counter from numba import njit  img = pg.image.load("test.png") sou = pg.Surface((150, 150))  @njit(cache=True) def blit(sou_arr, dest_arr, pos) -> np.ndarray:     X: int = pos[0]+dest_arr.shape[0]     Y: int = pos[1]+dest_arr.shape[1]      sou_arr[pos[0]:X, pos[1]:Y] = dest_arr      return sou_arr  x = 10 y = 20  C = 1_000_000 print("test blit") start = perf_counter() for i in range(C):     sou.blit(img, (10, 20))   # преобразуем Surface в numpy массивы для работы sou_arr = pg.surfarray.array3d(sou) dest_arr = pg.surfarray.array3d(img) blit(sou_arr, dest_arr, (10, 20)) #вызовем функцию 1 раз для того, чтобы numba её скомпилировала и время компиляции не попало в тесты  blit_perf = perf_counter() - start print("test created blit") start = perf_counter() for i in range(C):     sou = blit(sou_arr, dest_arr, (10, 20)) created_perf = perf_counter() - start print(f"buit-in blit perf: {blit_perf}\n created blit perf: {created_perf}, performance up: {blit_perf/created_perf}x")

данный кусок кода является неким тестированием встроенной функции blit и написанной на скорую руку функции, «рисующей на массивах». лично мой ПК показал следующие результаты:

buit-in blit perf: 33.87224140000035 created blit perf: 6.5419135999982245, performance up: 5.177726804586587x

обратите внимание, что для такого метода отрисовки требуется преобразование поверхности в ndarray, так же при попытке отрисовать текстуру, вылезающую за пределы поверхности, на которой мы рисуем, будет получено исключение и текстура отображена не будет, но это легко исправить дополнительными if (кода, который рисует вылезающие текстуры, здесь не будет, думаю вы и сами сможете его написать при желании)

Заключение

Я не претендую на то, что придумал феноменальные методы, ускоряющие код в триллионы раз и позволяющие запускать киберпанк на офисном ПК, я всего лишь поделился с вами методами, которые использую сам. Возможно, эти методы уже где-то были описаны и упомянуты, но лично я пришёл к этим методам самостоятельно, читая документации pygame, numpy, numba.


ссылка на оригинал статьи https://habr.com/ru/articles/774562/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *