Skip to content
E
Egmatic
оптимизация 2d игрпроизводительность игрывызовы отрисовкиобъединение спрайтовпул объектов60 fps

Как оптимизировать 2D-игру: путь до стабильных 60 FPS

2D-игра роняет кадры по одной из четырёх причин: draw calls, аллокации каждый кадр, невидимое, симуляция по FPS. Сначала измерьте, потом режьте.

Vladislav Kovnerov9 июля 2026 г.10 мин

2D-игра, которая заикается, почти никогда не объясняется чем-то загадочным. Стоит её измерить — и кадровый бюджет укажет на одного из четырёх привычных подозреваемых: слишком много вызовов отрисовки, сжигающих время процессора на накладных расходах; выделение памяти в каждом кадре, запускающее паузы сборки мусора; работа над объектами, которых камера не видит; и симуляция, привязанная к частоте кадров, из-за чего игра бежит быстрее или медленнее на разных машинах. Лечится это одинаково в любом движке: сначала измерить, затем бить по этим четырём по порядку.

Кадр при 60 FPS располагает бюджетом в 16,67 миллисекунды. Всё — ввод, симуляцию, отрисовку — нужно успеть за это время, иначе кадр пропускается и игрок видит рывок. Оптимизация — это дисциплина, которая помогает уложить игру в этот бюджет и удержать её там. Это руководство — практическая версия, написанная для 2D-игр и опирающаяся на то, как рендереры и среда выполнения .NET работают в действительности, а не на выдуманные проценты ускорения.

Если замедление происходит при сборке, а не во время игры, это другая проблема — см. наше руководство по ускорению сборки игры. Эта статья — о кадре, который вы отдаёте игроку.

Первый шаг всегда такой: профилировать, а не угадывать

Самая дорогая ошибка в оптимизации — менять код без измерений. У большинства игр есть одна-две функции, которые доминируют в кадре, и редко это те функции, которые вы подозреваете. Решение — сначала их найти.

ИнструментЧто показываетКогда обращаться
График времени кадраСтоимость каждого кадра в миллисекундах, с пикамиНачинать всегда отсюда — выявляет рывки, которые прячет средний счётчик FPS
Профилировщик процессораВремя в каждой функцииКогда кадр ограничен процессором и нужен виновник
Профилировщик видеокарты / отладчик кадраВызовы отрисовки, перерисовку, стоимость шейдеровКогда кадр ограничен видеокартой или скоростью заполнения
Профилировщик памятиВыделения на кадр, события сборки мусораКогда игра заикается, а счётчик FPS выглядит нормально

Читайте цифры, а не свою интуицию. Функция, которая «кажется тяжёлой», но занимает 0,1 мс, — не ваша проблема; скучный цикл на 6 мс — вот он. Каждая оптимизация ниже имеет смысл только там, где профилировщик говорит, что она нужна.

Сократите вызовы отрисовки через объединение спрайтов и атласы

Каждый вызов отрисовки — это одна команда от процессора к видеокарте: «нарисуй эту геометрию с этим материалом в такой позиции». Цена — не само рисование (современные видеокарты рисуют миллионы пикселей мгновенно), а фиксированные накладные расходы на подготовку каждого вызова. Сцена, рисующая тысячу спрайтов по одному, тратит почти весь кадровый бюджет на эти накладные расходы, а видеокарта в это время простаивает в ожидании следующей команды.

Объединение спрайтов (sprite batching) решает задачу, собирая множество спрайтов с одним материалом в один вызов отрисовки. Чтобы объединение работало, спрайты должны иметь общую текстуру — отсюда и парный приём: атлас спрайтов — одна большая текстура, в которую плотно упаковано множество мелких спрайтов. С атласом целый уровень из тайлов, кадры анимации персонажа или рой одинаковых врагов можно свести к одному или нескольким вызовам отрисовки вместо сотен.

СценаБазовый подход (вызов на спрайт)С атласом и объединением
500 тайлов~500 вызовов отрисовки~1 вызов
Анимированный персонаж (8 кадров)1 на видимый кадр1
Шквал снарядов (300 штук)~300~1 (общий атлас)

Практические правила: держите спрайты, которые появляются рядом, на одном атласе; не разрывайте группы переключением текстуры или режима наложения посреди сцены; упаковывайте атласы размерами, кратными степени двойки, где этого требует платформа. Это задокументированное поведение любого крупного 2D-рендерера, а не специфические для движка хитрости.

Не выделяйте память в каждом кадре: используйте пул объектов

В среде со сборкой мусора, такой как .NET (её используют и Unity, и MonoGame), занятую память рано или поздно приходится возвращать. Этим занимается сборщик мусора, и когда он запускается, то приостанавливает игру на несколько миллисекунд — достаточно, чтобы пропустить кадр и выдать видимый рывок. Секрет прост: перестать выделять память во время игры, а главный виновник — создание и уничтожение объектов на горячем участке.

Пул объектов — стандартный ответ. Вместо того чтобы создавать новую пулю при выстреле и уничтожать её при попадании, вы держите заранее созданный набор пуль. Выстрел берёт неактивную пулю, включает её и сбрасывает позицию. Попадание выключает пулю и возвращает её в пул. Никаких выделений, никакого уничтожения, никакой сборки мусора.

Помещайте в пул всё, что часто появляется и исчезает:

  • Снаряды и частицы
  • Врагов в волновой игре
  • Всплывающие числа урона и текст
  • Источники звука для повторяющихся эффектов

Более глубокая привычка — вообще избегать выделений в каждом кадре: не пользоваться LINQ и склейкой строк в часто выполняемых циклах, избегать упаковки значимых типов и переиспользовать коллекции вместо создания новых. Кадр, который ничего не выделяет, идёт плавно; кадр, который выделяет по несколько килобайт сотню раз, рано или поздно дёрнется.

Не рисуйте то, чего игрок не видит

Отрисовка объектов вне экрана — это впустую проделанная работа, которую видеокарте всё равно приходится принимать, а процессору — отправлять. Два простых приёма убирают большую её часть:

  • Отсечение (culling): пропускайте объекты целиком, если они вне поля камеры. Если враг за тремя экранами отсюда, ему незачем рисоваться в этом кадре.
  • Дисциплина слоёв и порядка отрисовки: группируйте статичный фоновый арт так, чтобы его можно было нарисовать один раз или закэшировать, а не обрабатывать заново каждый кадр.

Та же логика применима к логике, а не только к отрисовке. Врагу вне экрана не нужно полное обновление ИИ каждый кадр; удалённую систему частиц можно обновлять вдвое реже. Тратьте бюджет там, куда смотрит игрок.

Отвяжите симуляцию от частоты кадров

Если ваша игра двигает объекты по принципу «сдвинуть позицию на скорость в каждом кадре», то на мониторе 144 Гц игра бежит быстрее, а на устройстве с 30 FPS — медленнее. Это уже не оптимизация, а корректность, но напрямую влияет на воспринимаемую плавность: фиксированная частота симуляции в сочетании с интерполяцией выглядит и ощущается стабильно на любом дисплее.

Установившийся приём — фиксированный шаг с аккумулятором: симуляция продвигается фиксированными порциями (скажем, 60 раз в секунду) вне зависимости от частоты обновления экрана, а рендерер интерполирует между двумя последними состояниями симуляции для текущего кадра. Широко цитируемая статья Гленна Фидлера «Fix Your Timestep!» — классическое объяснение этого метода. Выгода двойная: детерминированная, воспроизводимая физика и игра, которая одинаково ощущается на любой машине.

Всегда держите в голове кадровый бюджет

Чтобы удержать 60 FPS, кадр должен укладываться в 16,67 мс. Это число и есть вся игра. Вот примерно как здоровый кадр 2D-игры его тратит:

ЭтапТипичная доля из 16,67 мсЧто ломается
Ввод и симуляция2–5 мсСлишком много активных объектов, выделения в кадре
Отсечение и подготовка групп1–3 мсНет объединения спрайтов, частые переключения текстур
Отрисовка на видеокарте и выдача кадра2–6 мсБольшая перерисовка, крупные прозрачные частицы
Запасвсё остальноеРывки съедают его первым

Колонка «запас» — в ней весь смысл. Кадр, использующий 16 мс, не имеет места для всплеска, поэтому следующее выделение памяти, следующий взрыв частиц или очередной запуск сборщика мусора его уронит. Старайтесь тратить заметно меньше бюджета, а не впритык к нему.

Ошибки, которые сводят всё на нет

  • Оптимизировать до измерений. Вы будете настраивать не то.
  • Гнаться за средним FPS. Плавный график времени кадра важнее высокого числа. Следите за всплесками.
  • Игнорировать сборку мусора. Главная причина «FPS высокий, а всё равно заикается».
  • Оптимизировать редко выполняемый код. Меню, которое грузится раз в минуту, не нуждается в пуле.
  • Привязывать скорость игры к частоте кадров. Это ломает ощущение на разных дисплеях и перечёркивает все остальные усилия.
  • Добавлять память вместо правки логики. Больший кэш помогает, только если он прогрет; холодный кэш импортирует всё заново.

Роль Egmatic

Egmatic — это 2D-редактор и движок, построенный на базе MonoGame. Этот выбор важен для производительности по двум честным причинам. Во-первых, MonoGame — компактная среда выполнения C#: она даёт прямой контроль над конвейером отрисовки без тяжёлого слоя абстракции между вами и «железом», поэтому при плотном кадровом бюджете узким местом оказывается ваш собственный код, а не движок, который вам сопротивляется. Во-вторых, живой предпросмотр Egmatic позволяет поменять значение и тут же увидеть влияние на время кадра, превращая профилирование из медленного цикла «правка — сборка — запуск» в быструю обратную связь — ту самую привычку, которую рекомендует вся остальная статья.

Egmatic нацелен на десктоп, мобильные устройства и консоли через базу MonoGame, поэтому та же работа по оптимизации сохраняется на всех платформах. Если вы только знакомитесь с лежащим в основе движком, наше руководство о MonoGame объясняет, что это за база и почему она компактна.

Заключение

Плавная 2D-игра — не плод секретных приёмов. Она рождается из измерений в рамках бюджета 16,67 мс, сокращения вызовов отрисовки через атласы и объединение спрайтов, устранения выделений памяти в каждом кадре с помощью пула объектов, отсечения того, чего камера не видит, и ведения симуляции фиксированным шагом. Делайте эти пять вещей в таком порядке, всегда опираясь на профилировщик, а не на интуицию, — и кадровый бюджет перестанет быть борьбой и станет привычкой.

Похожие статьи