Я студент. Год делал игру один между парами и выпустил её. Рассказываю, что чуть не убило релиз

Привет. Я студент, и последний год между парами и по вечерам делал свою первую игру. На прошлой неделе она вышла в Google Play. Это Garlictos, survivor-рогалик в духе Brotato, сделанный один на Godot 4.

Сразу честно: это именно survivor-рогалик, и я не делаю вид, что изобрёл жанр. Оружие стреляет само, игрок управляет только движением, 20 волн, магазин между ними. Единственное, где я попробовал добавить своё, — это объединение оружия. Шесть слотов, два одинаковых ствола сливаются в более сильный уровень, а когда сумка забита, игра начинает объединять автоматически. Из-за этого каждый заход в магазин сводится к одному вопросу: брать третий пистолет ради слияния или ломать сборку ради дробовика, который только что выпал.

Теперь главное, ради чего я пишу этот пост. Всё работало идеально в редакторе. Потом я поставил APK на реальный дешёвый телефон, и игра развалилась. Три вещи, которые съели у меня недели:

1. Shader instance uniforms имеют жёсткий потолок. Я использовал их для вспышки при попадании по врагу. На 20 врагах выглядело отлично. На 200 врагах из пула лог забился ошибкой Too many instances using shader instance variables, max 4096, и экран начал хаотично мигать. На gl_compatibility это реальный аппаратный лимит, а пулы объектов пробивают его мгновенно. Решение: один общий ShaderMaterial на всех врагов, а тот, кого ударили, на 0,15 секунды переключается на свою копию.

2. Physics interpolation оставляет шлейфы на всём, что двигаешь в _process. Я перешёл на физику 30 Гц с интерполяцией, что дало большой выигрыш по CPU. Но всё, что я позиционировал вручную, начало размазываться по экрану. Гильзы были хуже всего: выглядело так, будто вспышки выстрелов срабатывают в случайных углах экрана. Решение: physics_interpolation_mode = OFF на таких узлах и reset_physics_interpolation() через deferred после того, как ставишь объект из пула.

3. 2D-батчинг ломается от безобидных вещей. Спрайт тени с show_behind_parent тихо разрезал все батчи и разогнал меня до 80 draw call. Замена на абсолютный z_index -50 всё починила. Ещё outline_size 8 у Label оказался дорогим: тень в 2 пикселя выглядит почти так же, а стоит втрое дешевле.

В итоге draw calls упали с 204 примерно до 67 за полный забег. Это была разница между «играть невозможно» и «играется нормально» на дешёвых телефонах.

И ещё одно решение, которое все называли глупым: в игре нет ни одной принудительной рекламы. Ни между волнами, ни баннеров, ни таймеров и энергии. Есть только добровольная реклама за награду, например чтобы возродиться, и одна необязательная покупка в поддержку разработки, за которой ничего не закрыто. Возможно, это ошибка с точки зрения денег. Но я не хотел делать то, что сам бы удалил через минуту.

Апдейт через пару недель после релиза, раз уж пост тут остаётся: вышла 1.7, и кое-что из написанного выше уже не так честно звучит. Добавил два интерстишла — после 8 волны и когда возвращаешься в меню после смерти, не чаще раза в 3 минуты. Реворд-реклама для возрождения осталась как была, добровольная, баннеров всё ещё нет. Кто берёт пакет поддержки, тот интерстишлы вообще не видит. Решил дописать это прямо тут, а не делать вид что ничего не поменялось. Заодно фризы на слабых телефонах ушли процентов на 90, 6 волна перестала быть стеной, возрождений теперь 3 а не 1.

Новый трейлер, если интересно посмотреть как это выглядит сейчас: https://www.youtube.com/watch?v=3GNlJ_Z0nb4

Игра бесплатная, работает оффлайн, около 117 МБ, Android 7.0 и выше. Языки пока английский и турецкий, русского пока нет.

https://play.google.com/store/apps/details?id=com.garlictos.omer&referrer=utm_source%3Ddtf%26utm_medium%3Dpost%26utm_campaign%3Dlaunch

Если попробуете, самый полезный фидбек для меня такой: на какой волне вы умерли в первый раз. Я тестировал её столько раз, что совсем перестал понимать, сложная она или лёгкая. По технической части готов ответить на любые вопросы.

1133
33
33
22
11
11