- PVSM.RU - https://www.pvsm.ru -
Теги: #monogame #gamedev #оптимизация #csharp #графика #индиразработка
Введение: Проблема падающих FPS и желание красоты
Каждый разработчик 2D-игр, мечтающий о живом, населённом мире, рано или поздно упирается в суровую реальность: рендеринг множества уникальных анимированных персонажей — это дорого. Хочется дать игроку кастомизацию, смену снаряжения, разнообразие врагов, но классический подход «нарисовать каждый спрайт отдельно» ведёт к сотням draw calls и падению производительности.
В своей инди-игре (симуляторе жизни в мире фэнтези-жуков) я столкнулся с этой проблемой в полный рост. Мне нужны были десятки NPC на экране, каждый — с возможной сменой брони и оружия. Решением стала гибридная система рендеринга, основанная на разделении персонажей по уровням детализации и использовании инстансинга. В статье я разберу её архитектуру, код и полученные выгоды.
Часть 1: Анализ проблемы — почему «в лоб» не работает
Допустим, у персонажа 10 слоёв: тело, голова, ноги, броня, шлем, оружие в каждой руке и т.д. Если рендерить каждый слой отдельным вызовом SpriteBatch.Draw(), для 50 NPC мы получим 50 × 10 = 500 draw calls. При целевом значении в 60 FPS у нас есть всего ~16.6 мс на кадр. Если один draw call занимает ~0.1 мс, только на отрисовку персонажей уйдёт 50 мс — это в три раза больше бюджета!
Ключевой инсайт: Не всем персонажам нужна одинаковая детализация. Фоновому горожанину или стае врагов не требуется динамическая смена снаряжения. А главному герою — требуется.
Часть 2: Гибридная архитектура — 3 уровня детализации
Я разделил всех персонажей в игре на три типа, каждый со своим пайплайном рендеринга.
1. Однослойные (Single-Layer) — для массовки
Кто: Фоновые NPC, животные, простые враги.
Подход: Предварительный рендеринг. В редакторе или на этапе загрузки игры их анимации (со всеми слоями) рендерятся в единые спрайтшиты.
Преимущество: 1 draw call на персонажа. Максимальная производительность.
Код:
csharp
public class SingleLayerCharacter
{
public Texture2D AtlasTexture; // Единый атлас с анимациями
public Rectangle CurrentFrame;
public Vector2 Position;
// Упрощённый скелет для базовой анимации (опционально)
public void Draw(SpriteBatch spriteBatch)
{
// ВСЕ такие персонажи могут быть отрисованы в одном вызове
// Begin/End, если использовать сортировку
spriteBatch.Draw(AtlasTexture, Position, CurrentFrame, Color.White);
}
}
2. Трёхслойные (Three-Layer) — для ключевых объектов
Кто: Главный герой, компаньоны, важные NPC, боссы.
Подход: Динамическая сборка. Базовый скелет + отдельные текстуры для тела, одежды/брони, оружия. Слои накладываются в шейдере.
Преимущество: Динамическая кастомизация при сохранении приемлемой производительности (~3 draw call'а).
Код (структура):
csharp
public class ThreeLayerCharacter
{
public Skeleton Skeleton; // Общий скелет
public Texture2D BaseTexture; // Слой 1: Тело
public Texture2D ClothingTexture;// Слой 2: Одежда/броня
public Texture2D WeaponTexture; // Слой 3: Оружие
public bool HasClothing, HasWeapon;
// Матрицы костей скелета рассчитываются один раз
}
3. Инстансинг (Instanced) — для оптимизации групп
Что: Техника для рендеринга множества одинаковых по геометрии (скелету), но разных по текстуре и трансформациям объектов.
Применение: Идеально для рендеринга группы трёхслойных персонажей (например, отряд солдат-жуков).
Ключевая идея: Передаём данные (матрицы скелета, текстуры слоёв) в шейдер массивами и рисуем всю группу за 1-2 draw call.
Часть 3: Сердце системы — шейдер для инстансинга слоёв
Вот упрощённая концепция шейдера (HLSL), который делает эту магию возможной:
hlsl
// Входные данные для ОДНОГО инстанса (персонажа)
struct InstanceData
{
float4x4 BoneMatrices[MAX_BONES]; // Матрицы трансформации костей его скелета
float3 LayerIndices; // Индексы текстур в атласе для Base, Clothing, Weapon
};
StructuredBuffer<InstanceData> instanceBuffer : register(t0); // Массив всех персонажей
Texture2DArray layerAtlasArray : register(t1); // Атлас всех возможных слоёв
VertexShaderOutput VS(VertexInput input, uint instanceID : SV_InstanceID)
{
InstanceData data = instanceBuffer[instanceID];
// Применяем трансформации костей к вершине, используя data.BoneMatrices
// ...
}
float4 PS(VertexShaderOutput input) : SV_TARGET
{
// Выбираем цвет из соответствующего слоя, используя input.layerUV и input.layerIndex
float4 baseColor = layerAtlasArray.Sample(samplerState, float3(input.uv, input.baseIndex));
float4 clothingColor = layerAtlasArray.Sample(samplerState, float3(input.uv, input.clothingIndex));
float4 weaponColor = layerAtlasArray.Sample(samplerState, float3(input.uv, input.weaponIndex));
// Накладываем слои (можно добавить blend modes)
float4 finalColor = baseColor;
if (input.hasClothing) finalColor = blend(finalColor, clothingColor);
if (input.hasWeapon) finalColor = blend(finalColor, weaponColor);
return finalColor;
}
Часть 4: Результаты и сравнение производительности
Давайте переведём теорию в цифры. Допустим, сцена: 1 игрок + 4 компаньона + 45 врагов/горожан.
Наивный подход (10 слоёв на всех):50 NPC × 10 layers = 500 draw calls. ➜ ~50 мс (20 FPS)
Наша гибридная система:
45 однослойных: 45 × 1 = 45 (можно свести в 1 batch call)
5 трёхслойных: 5 × 3 = 15 (можно свести в 1 instanced call для одинаковых скелетов)
Итого: ~2-4 draw calls. ➜ ~0.4-0.8 мс ( >1000 FPS)
Выигрыш — на два порядка.
Часть 5: Практические советы и подводные камни
Инструментарий: Для такого пайплайна критически важен редактор. Я связываю DragonBones (скелетная анимация) с самописным инструментом для сборки атласов и конфигурации слоёв.
Память vs. Производительность: Предварительный рендеринг однослойных спрайтов съедает память, но экономит GPU. Кэшируйте результат, чтобы не рендерить одинаковых NPC повторно.
Сложность отладки: Когда персонаж — это 3 текстуры, трансформируемые 20 костями в шейдере, отладка визуальных артефактов сложнее. Ведите лог состояний.
Не все движки одинаковы: Подход оптимизирован под MonoGame с его низкоуровневым доступом к графическому пайплайну. В Unity или Godot есть свои встроенные системы инстансинга и композитных спрайтов.
Заключение: Прагматизм побеждает
Оптимизация в геймдеве — это поиск компромисса. Описанная система не претендует на абсолютную техническую новизну, но является прагматичным решением реальной проблемы соло- или инди-разработчика. Она позволяет сохранить и визуальное богатство, и высокий FPS, делегируя сложность заранее подготовленному пайплайну создания контента и умному шейдеру.
Для сообщества вопрос: Сталкивались ли вы с похожими задачами в своих 2D-проектах? Какие альтернативные или более изящные подходы к рендерингу кастомизируемых персонажей вы бы предложили?
Автор: LyalenkovMA
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/442888
Ссылки в тексте:
[1] Источник: https://habr.com/ru/articles/988696/?utm_campaign=988696&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.