Как заставить мобильную игру работать в 60 кадров/с? Методы рендеринга

TRUETECH занимается разработкой, поддержкой и обслуживанием мобильных приложений iOS, Android, PWA. Имеем большой опыт и экспертизу для публикации мобильных приложений в популярные маркеты Google Play, App Store, Amazon, AppGallery и другие.

Разработка и поддержка любых видов мобильных приложений:

Информационные и развлекательные мобильные приложения
Новостные приложения, игры, справочники, онлайн-каталоги, погодные, фитнес и здоровье, туристические, образовательные, социальные сети и мессенджеры, квиз, блоги и подкасты, форумы, агрегаторы
Мобильные приложения электронной коммерции
Интернет-магазины, B2B-приложения, маркетплейсы, онлайн-обменники, кэшбэк-сервисы, биржи, дропшиппинг-платформы, программы лояльности, доставка еды и товаров, платежные системы
Мобильные приложения для управления бизнес-процессами
CRM-системы, ERP-системы, управление проектами, инструменты для команды продаж, учет финансов, управление производством, логистика и доставка, управление персоналом, системы мониторинга данных
Мобильные приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, платформы предоставления электронных услуг, платформы кешбека, видеохостинги, тематические порталы, платформы онлайн-бронирования и записи, платформы онлайн-торговли

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

Услуги, которые мы предлагаем
Показано 1 из 1Все 1734 услуг
Как заставить мобильную игру работать в 60 кадров/с? Методы рендеринга
Сложный
~3-5 дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_mobile-applications_feedme_467_0.webp
    Разработка мобильного приложения для компании FEEDME
    858
  • image_mobile-applications_xoomer_471_0.webp
    Разработка мобильного приложения для компании XOOMER
    743
  • image_mobile-applications_rhl_428_0.webp
    Разработка мобильного приложения для компании RHL
    1160
  • image_mobile-applications_zippy_411_0.webp
    Разработка мобильного приложения для компании ZIPPY
    1034
  • image_mobile-applications_affhome_429_0.webp
    Разработка мобильного приложения для компании Affhome
    968
  • image_mobile-applications_flavors_409_0.webp
    Разработка мобильного приложения для компании FLAVORS
    562

Почему мобильная игра не держит 60 FPS?

На Samsung Galaxy A52 (Adreno 618) игра выдаёт 28–32 FPS при целевых 60. На Xiaomi Redmi Note 11 с Helio G96 и Mali-G57 — стабильные 55–60 FPS. Разная производительность на похожих по цене устройствах — типичная ситуация в мобильном геймдеве. Причина почти всегда в том, что оптимизация проводилась на флагмане, а mid-range GPU с другой архитектурой даёт совсем другую картину. Наша оптимизация рендеринга мобильной игры начинается с анализа bottleneck на конкретных чипах. Мы помогли заказчикам сократить затраты на GPU вдвое, избежав дорогих переделок.

Мы специализируемся на кросс-платформенной оптимизации: работаем с Unity, Unreal Engine и собственных движках. За плечами — более 15 проектов, где мы доводили FPS до стабильных 60 на целевых устройствах. В результате оптимизации рендеринга мобильной игры мы добивались прироста в 20–30% без потери качества. Ключ к стабильной производительности — понимание архитектуры мобильных GPU. В отличие от настольных, они используют tile-based rendering, что накладывает особые требования к шейдерам и батчингу.

Как определить bottleneck на мобильном GPU?

Прежде чем оптимизировать — понять что является bottleneck. В Unity: Frame Debugger + Profiler. Включаем Profile GPU в Android Player Settings и смотрим Profiler → GPU. Если GPU-время на кадр близко к 16ms (60 FPS) а CPU-время значительно меньше — GPU-bound. Если наоборот — CPU-bound.

На Unreal Engine: stat GPU в консоли, ProfileGPU команда, RenderDoc для захвата кадра. r.ScreenPercentage 50 — быстрый тест: если FPS резко вырос при снижении разрешения вдвое — точно GPU-bound. Команды stat unit, stat drawcalls, r.ShowFlag.Rendering 1 дают breakdown по CPU/GPU и количеству draw calls.

Почему draw calls критичны для mid-range устройств?

На мобильных GPU draw call overhead выше чем на консолях/PC. 500+ draw calls в кадре — красная зона для mid-range Android. Каждый уникальный материал = отдельный draw call. Каждый MeshRenderer с уникальным материалом — ещё один.

Static batching (Unity): объекты с одинаковым материалом объединяются в один mesh. Требование — одинаковый Material asset (не просто одинаковые настройки). Mark as Static в Inspector. Работает автоматически при сборке.

GPU Instancing: для повторяющихся объектов (трава, деревья, враги одного типа):

// Материал должен поддерживать instancing
material.enableInstancing = true;

// Рисуем 1000 экземпляров одним draw call
Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 1000);

SRP Batcher (Unity URP/HDRP): автоматически батчит объекты с разными материалами но одинаковым шейдером. Включается в URP Asset → SRP Batcher = enabled. Самый простой способ сократить draw calls без ручного batching.

Как мы оптимизируем рендеринг

Шейдеры под tile-based GPU

Мобильные GPU (Adreno, Mali, PowerVR, Apple) используют Tile-Based Immediate Mode Rendering. Экран делится на тайлы, каждый рендерится целиком в быстром on-chip памяти. Это означает:

  • Framebuffer fetch — чтение из текущего framebuffer внутри тайла — практически бесплатно. Используем для deferred lighting: gl_LastFragData в GLSL (GLES extension EXT_shader_framebuffer_fetch).
  • Depth pre-pass на мобиле — часто лишний overhead, TBIMR и так эффективно работает с depth test внутри тайла.
  • Discard в фрагментном шейдере (alpha-test, clip) — на tile-based GPU убивает ранний depth test для целого тайла. Заменяем alpha-blend или alpha-to-coverage там где возможно.

Precision qualifiers в GLSL/Metal

// МЕДЛЕННО — highp везде по умолчанию
uniform highp mat4 ModelMatrix;
varying highp vec2 TexCoord;

// БЫСТРО — минимально необходимая точность
uniform highp mat4 ModelMatrix;    // матрицы — highp обязательно
varying mediump vec2 TexCoord;     // UV-координаты — mediump достаточно
varying lowp vec4 VertexColor;     // цвет — lowp

На Mali GPU переход с highp на mediump для texture samplers — от 10 до 25% прирост производительности фрагментного шейдера.

ALU vs Texture Fetch

На большинстве мобильных GPU texture fetch дешевле чем тяжёлые ALU-вычисления (sin, pow, sqrt). Предварительно запечённые lookup таблицы в текстуре быстрее чем вычисление в шейдере:

// Медленно: вычисляем fresnel в шейдере
float fresnel = pow(1.0 - dot(viewDir, normal), 5.0);

// Быстро: lookup texture
float fresnel = texture2D(fresnelLUT, vec2(dot(viewDir, normal), roughness)).r;
Совет по профилированию на MaliДля Mali GPU используйте инструмент Streamline Performance Analyzer (ARM DS-5) или AGI (Android GPU Inspector). Обращайте внимание на контроллеры: Fragment ALU cycles, Fragment texture cycles, и Memory bandwidth. Это поможет точно определить, узкое место — ALU, текстуры или пропускная способность.

Dynamic resolution и adaptive performance

Рендерить в нативном разрешении iPhone 15 Pro (2556×1179) — избыточно для мобильной игры с интенсивным рендерингом. Стандартная практика — render scale 0.7–0.85 от нативного с последующим upscale.

Unity URP Dynamic Resolution:

ScalableBufferManager.ResizeBuffers(0.75f, 0.75f); // 75% от нативного

Unreal Mobile Super Resolution (MSR) — встроенный temporal upscaler для мобильных платформ с Unreal 5.1+. r.Mobile.TemporalAA 1. Даёт качество близкое к нативному при значительно меньшей GPU-нагрузке.

Adaptive Performance (Samsung Game SDK + Unity): автоматически снижает нагрузку при перегреве. Доступны метрики теплового статуса и производительности через UnityEngine.AdaptivePerformance.

Кейс: 40 → 58 FPS на Adreno 618

Из нашей практики: runner-игра на Galaxy A52 — 40 FPS. Профилирование через AGI показало: Fragment ALU 87%, fragment bandwidth — перегружен. Три изменения:

  1. Шейдер воды: заменили pow(fresnel, 5.0) на LUT-текстуру → -8ms GPU
  2. highpmediump для всех texture samplers → -4ms GPU
  3. Dynamic resolution 0.80 вместо нативного → -6ms GPU

Итого: с 40 до 58 FPS без изменения визуального стиля. На Pro-устройствах — без изменений, они держали 60 FPS с запасом.

Метрика До После Снижение
FPS 40 58 +18 (45%)
GPU time (ms) 25 16.5 34%
Draw calls 780 210 73%

Процесс работы

  1. Аналитика: сбор логов, профилирование на целевых устройствах, определение bottleneck.
  2. Проектирование: выбор методов оптимизации (batching, шейдеры, dynamic resolution).
  3. Реализация: внедрение изменений в код и ассеты.
  4. Тестирование: прогон на 5+ различных устройствах, сравнение FPS и качества.
  5. Деплой: подготовка релизной сборки, настройка Adaptive Performance.

Сроки: профилирование и анализ занимают 2–3 дня. Оптимизация шейдеров, batching, dynamic resolution — от 1 до 3 недель в зависимости от состояния проекта.

Что вы получите

  • Подробный отчёт с анализом рендеринга и узкими местами
  • Оптимизированные шейдеры с минимальной точностью (mediump/lowp)
  • Настроенный automatic batching (static batching, SRP Batcher, instancing)
  • Конфигурацию Dynamic Resolution с учётом целевых устройств
  • Инструкцию по сборке и деплою
  • Гарантию стабильности результата на согласованном пуле устройств
Bottleneck Симптомы Инструменты диагностики Типичные решения
CPU-bound CPU time > 16ms, много физики, скриптов Unity Profiler, Unreal stat unit Оптимизация кода, сжатие ассетов
GPU-bound GPU time > 16ms, высокий fill rate GPU Profiler, RenderDoc Снижение разрешения, шейдеры, LOD
Draw calls >500 draw calls, высокая batch count Frame Debugger, stat drawcalls Static batching, GPU instancing, SRP
Bandwidth Высокое использование memory bandwidth GPU counters (Mali, Adreno) Сжатие текстур, mipmaps, alpha

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

Оптимизация мобильных приложений: cold start, память, батарея, FPS, профилирование

Приложение с временем холодного старта 4+ секунды теряет пользователей ещё до первого экрана. Android Vitals в Google Play Console прямо влияют на ранжирование в поиске: приложения с плохими метриками получают меньший organic reach. Apple аналогично мониторит crash rate и время запуска через MetricKit. Оптимизация — это не «сделать быстрее», а понять где именно теряется время и что с этим делать.

Cold Start: где убивается время до первого кадра

Cold start — запуск приложения, когда процесс не существует в памяти. На Android это время от нажатия на иконку до Activity.onWindowFocusChanged(hasFocus = true). На iOS — от tap до viewDidAppear первого экрана.

Android: main thread перегружен при инициализации

Application.onCreate() — главный враг быстрого старта на Android. Разработчики инициализируют здесь всё подряд: Firebase, Analytics, базу данных, HTTP-клиент, DI-контейнер. Каждый SDK добавляет 20–200 мс на main thread.

Инструмент для диагностики: Android Studio Profiler → App Startup. Показывает граф инициализации с временем каждого компонента. Альтернатива — Tracing.beginSection("MyInitTag") в коде + systrace.

Решение: App Startup Library (Jetpack) с явным графом зависимостей инициализаторов. Компоненты, нужные только в конкретных сценариях, инициализируются лениво — by lazy {} или initializer с флагом lazyInit. Firebase Analytics, например, не нужен до первого пользовательского действия — его инициализацию можно отложить.

ContentProvider-ы, автоматически добавляемые SDK через AndroidManifest merge, тоже запускаются при старте. tools:node="remove" в манифесте позволяет отключить конкретный провайдер и инициализировать SDK вручную в нужный момент.

Ещё одна грабля: Room.databaseBuilder().build() на main thread. Это синхронная операция создания/открытия файла БД — на медленных устройствах занимает 50–300 мс. Переносим в coroutine с Dispatchers.IO, в ViewModel через viewModelScope.launch.

iOS: Dyld linking и +load

На iOS cold start делится на pre-main (до вызова main()) и post-main. Pre-main — время загрузки dylib, rebase/binding, Objective-C runtime initialization и выполнения +load методов.

Xcode Instruments → App Launch template показывает время pre-main и post-main раздельно. DYLD_PRINT_STATISTICS=1 в схеме запуска выводит детальное время загрузки в консоль.

Что убивает pre-main:

  • Много динамических библиотек (каждая dylib — накладные расходы на линковку). CocoaPods добавляет отдельную dylib на каждый pod. Решение: Swift Package Manager со статической линковкой (type: .static) или use_frameworks! :linkage => :static в CocoaPods.
  • +load методы в Objective-C — выполняются синхронно при загрузке класса, до main(). Сторонние SDK могут злоупотреблять этим. +initialize — ленивый аналог, вызывается при первом обращении к классу.

Post-main — application(_:didFinishLaunchingWithOptions:). Та же история что на Android: синхронная инициализация всего. lazy var для сервисов, которые не нужны немедленно. SwiftUI @StateObject инициализирует объект только когда View появляется — это уже встроенная ленивость.

Целевые метрики (App Store рекомендации): cold start < 400 мс для простых приложений, < 2 секунды для сложных. Warm start (процесс в памяти, но Activity/Scene пересоздаётся) — < 1 секунда.

Память: утечки, OOM, excessive pressure

Утечка памяти в iOS — retention cycle: объект A держит ссылку на B, B держит на A, ни один не освобождается. Классика: Timer с self в замыкании без [weak self]. Timer удерживает замыкание, замыкание удерживает self (ViewController), ViewController не освобождается при закрытии. Instruments → Leaks или Memory Graph Debugger в Xcode — находит живые объекты, которых не должно быть.

На Android garbage collector управляет памятью, но утечки всё равно случаются. Activity или Fragment, удерживаемые через статическую ссылку, singleton, или Handler/Runnable после onDestroy — классика. LeakCanary — обязательный инструмент в debug-сборке. Добавляется одной зависимостью debugImplementation "com.squareup.leakcanary:leakcanary-android" и автоматически детектирует утечки с полным стектрейсом.

OutOfMemoryError чаще всего происходит из-за загрузки изображений. Bitmap в памяти занимает ширина × высота × 4 байта. Изображение 4000×3000 px — 48 МБ в памяти, независимо от размера файла на диске. Glide / Coil правильно обрабатывают это: загружают с даунсемплингом под размер View, кешируют в LRU-кеш. Загружать в ImageView без Glide/Coil через BitmapFactory.decodeFile — путь к OOM на устройствах с 2 ГБ RAM.

На Flutter Dart VM имеет свой GC, но нативные ресурсы (изображения, текстуры) не управляются Dart GC. Image.network кеширует изображения в памяти без автоматического освобождения при выходе из дерева виджетов — при длинных списках с картинками используем cached_network_image с правильным memCacheWidth/memCacheHeight.

FPS и UI Performance

60 FPS — 16.67 мс на кадр. 120 FPS (ProMotion) — 8.33 мс. Всё что занимает больше на main thread — джанк.

Типичные причины просадок FPS:

На iOS: синхронная декодировка изображений в cellForRowAt. Когда ячейка таблицы появляется, UIImage(contentsOfFile:) декодирует JPEG/PNG на main thread — видно как заторможенный скролл на длинных списках. Решение: UIImage.preparingForDisplay() (iOS 15+) или ImageIO с kCGImageSourceCreateThumbnailWithTransform в background queue, результат через DispatchQueue.main.async.

На Android: RecyclerView.Adapter.onBindViewHolder с синхронными операциями. Базы данных, файловая система, синхронные сетевые запросы на main thread — StrictMode.ThreadPolicy с detectAll().penaltyLog() в debug-сборке покажет все нарушения.

На Flutter: build() метод вызывается часто, он должен быть дешёвым. setState() на верхнем виджете пересобирует всё дерево. const конструкторы, RepaintBoundary, разбиение на мелкие виджеты с локальным стейтом — основные инструменты. Flutter DevTools → Performance показывает janky frames (красные) с причинами.

Профилирование Compose: Recomposition Highlighter и трассировка через Trace.beginSection в @Composable. remember для дорогих вычислений, derivedStateOf для computed values, LazyColumn вместо Column + forEach для длинных списков.

Батарея: Wake locks, WorkManager, сетевые запросы

Приложение в топе по расходу батареи — пользователь видит это в настройках и удаляет. Android Battery Historian (из ADB bug report) показывает детальный timeline: wake locks, wakeups, network activity, sensor usage.

Основные потребители энергии:

  • Постоянный GPS (разбираем в maps-geo)
  • Polling сети каждые N секунд вместо push
  • Holding wake lock дольше необходимого
  • Excessive AlarmManager wakeups

WorkManager с Constraints — правильный способ планировать фоновые задачи: setRequiredNetworkType, setRequiresBatteryNotLow, setRequiresCharging. ОС батчирует задачи и выполняет в удобное время.

На iOS BGTaskScheduler с BGProcessingTaskRequest (для тяжёлых задач при зарядке) и BGAppRefreshTaskRequest (для лёгких обновлений) — система решает когда выполнять, разработчик только регистрирует и реализует логику.

Батчинг сетевых запросов: вместо 10 отдельных запросов в течение минуты — один батч запрос. Меньше радио-активностей (LTE radio потребляет много при инициализации соединения), меньше wakeups.

Инструменты профилирования

Платформа Инструмент Что показывает
iOS Xcode Instruments (Time Profiler) CPU, call stack, горячие методы
iOS Allocations Живые объекты, пики памяти
iOS Leaks Retention cycles
iOS MetricKit Производственные метрики (crash rate, hang rate, launch time)
Android Android Profiler CPU, Memory, Network, Energy
Android Systrace / Perfetto System-level трейсы
Android LeakCanary Утечки памяти
Android Battery Historian Энергопотребление
Flutter Flutter DevTools Recomposition, frame rendering, memory
Flutter Dart Observatory Dart VM profiling

MetricKit на iOS — особенно ценен: реальные данные с устройств пользователей, а не симулятора. MXMetricManager получает агрегированные метрики раз в сутки: MXAppLaunchMetric, MXHangDiagnostic, MXCPUExceptionDiagnostic. Диагностики по hang и CPU-exceptions содержат стектрейс с реального устройства — золото для диагностики production-проблем.

Процесс оптимизации

Начинаем с измерения, не с предположений. Инструменты выше дают цифры: конкретное время cold start, конкретный объём памяти, конкретные кадры с просадкой. Потом — приоритизация по impact: что больше всего влияет на пользовательский опыт именно в этом приложении.

Аудит производительности существующего приложения: 3–5 рабочих дней. Реализация оптимизаций — от недели до двух месяцев в зависимости от запущенности проблем и архитектуры кода.