SpellSinger - разработчик игр на Unity
СтатистикаПривет! Я Денис и я делаю игры на Unity. В этом блоге я делюсь своим опытом в геймдеве и не только. Навигация в закрепе: https://t.me/spellsingergames/3 Написать в личку: @sinedsem
- Последний пост
- 14 авг.
- Последнее чтение
- 17:43
- Постов за неделю
- 1
- Всего постов
- 35
- Тип
- открытый
- Язык
- русский
- Категория
- Технологии
- В каталоге с
- 12 авг.
- 1/24сутки в ленте
- 180
- 1/48двое суток
- 206
- 1/72трое суток
- 222
Оценка по просмотрам недавних постов: пост набирает почти всё за первые сутки.
Посты
Шестой эпизод подкаста ГЕЙМДЕВ ПОД ПИВОМ уже во вторник, в 22:30МСК! Хорошенечко отоспитесь перед этим днём, чтобы не пропустить интервью с Денисом SpellSingerGames, разработчиком БУРИТЬ КОПАТЬ! Узнаем насколько это сложно вдвоём поддерживать разработку игры на десятках платформ и при этом балансировать с воспитанием детей ^^
видео или голосовое, без подписи
Полетим? Бурить-Копать! сейчас переживает переломный момент. Мы не обновляли игру на площадках с декабря прошлого года, так как все это время работали над большим обновлением игры. В нем была куча связанных изменений, которые не позволяли релизить частично. И, есть надежда, что после этих изменений игра начнёт зарабатывать. Обновленная версия игры уже доступна на Пикабу Играх и в Играх ВКонтакте. К сожалению, Яндекс Игры отстают из-за модерации. И уже есть некоторые результаты (усредняю числа, точнее смотрите на графиках) 1) Среднее количество просмотров Rewarded рекламы на игрока на Пикабу выросло с 3 до 25 (синий график 🟦) 2) Общее количество показов Rewarded ВКонтакте выросло с 20 до 400 в день при том же DAU (зеленый график 🟩) К сожалению, маленький DAU и неумение GameAnalytics правильно замерять плейтайм в вебе не позволяет оценить другие метрики. Прямо сейчас у нас проходит QA с издателем в GooglePlay, после чего будет вторая тестовая проливка траффика. Первая проливка траффика была полтора года назад (на оооочень старой версии) и тогда мы получили D1 - 22% и ROI < 1. Сейчас есть все шансы выйти в плюс по закупке. Исправляем баги, найденные QA, сжимаем кулачки и ждем модерации в ЯИ и закупки в GP. #бурить_копать
ИИ-агенты и API сайтов В Яндекс Играх все инап-покупки нужно конфигурировать в админке. И хотя они и поддерживают групповую загрузку CSV, на данный момент из CSV подтягиваются только английская и русская локализация покупок. Сейчас у нас в игре 13 языков, значит 10* локализаций для каждого продукта нужно вставлять руками. У нас 9 продуктов. 90 раз нажать Ctrl+C/Ctrl+V. И ни разу не ошибиться в неудобном UI. Но есть вариант проще: 1) Открываем панель разработчика в браузере, вкладка Network 2) Редактируем 1 любой продукт 3) Находим запрос, который UI отправил на изменение продукта. 4) Копируем запрос ПКМ > Copy as cURL (bash) 5) Отдаём этот запрос как пример вашему любимому ИИ-агенту вместе с переводами продуктов на все языки в любом формате, просим заполнить все данные 6) ИИ сам поймет структуру запроса по примеру, возьмёт ваши креды (токен сессии или куки или whatever) и обновит все переводы. И, конечно, это работает не только с ЯИ. Всё, что ты можешь сделать через UI, ты можешь автоматизировать через ИИ-агента. * ЯИ не различают упрощённый и традиционный китайские языки #ai
видео или голосовое, без подписи
Про сейвы в Играх ВКонтакте Как-то мы получили жалобу от игрока в ВК добрый вечер. Все обновилось. Игра началась с самого начала. У меня были покупки и прокачка на макс. обидно как то. Странно, подумали мы. Повезло, что к этому времени у нас уже были реализованы бэкапы сейвов на наш сервер, и, не без труда, но мы смогли восстановить игроку прогресс. Потом мы получиили ещё одну аналогичную жалобу. И ещё. В какой-то момент, мы реализовали возможность восстановить сейв через промокод (загружаем сейв на сервер, игрок вводит промокод и игра затирает локальный сейв тем, что получила с сервера). Стало легче восстанавливать прогресс, но причину не удавалось обнаружить. Чтобы найти причину, мы добавляли подробные дебаг логи работы системы сохранений, и в какой-то момент удалось поймать ошибку. Суть ошибки была, что сохранение «испорчено». Надо сказать, что сохранения у нас шифруются (спасибо автору видео), и было непонятно, как именно оно испорчено. Добавили логирование сохранения до шифрования, и спустя какое-то время и ещё одного несчастного игрока в ВК выяснилось, что: В VKWebAppStorageSet Сохраняются только первые 4096 символов, для сериализованной строки — 2236 символов. Что я естественно не заметил при интеграции с ВК. Или может заметил, но не подумал на тот момент, что наши сейвы станут такими большими. Причем, страдали именно игроки с большим прогрессом, так как сейв разрастается по мере получения игроком новых портретов, скинов и апгрейдов. Технически проблема решается не очень сложно, при сейве надо просто делить строку на части по 4096 символов, и отдельным ключом сохранять количество частей. При чтении сейва сначала запрашиваешь количество частей, потом все нужные части. Плюс можно сделать оптимизацию, и первым запросом к VKWebAppStorageGet сразу запрашивать количество частей и первую часть - с высокой вероятностью, весь сейв поместится в первую часть. Но у меня есть ряд риторических вопросов к ВК: 1) Почему вообще есть такое ограничение? Яндекс разрешает 200 КБ, и это выглядит разумной экономией места, но 4096 символов, серьезно? 2) Почему длина сериализованной строки должна быть ещё меньше, всего 2236 символов? В чем для вас разница? 3) Что вообще такое сериализованная строка? В документации про это ни слова. Как я должен понять, чем они отличаются? 4) Пока искал, что такое сериализованная строка, наткнулся на этот пост. Серьезно? До 2023 года данные хранились всего год? #игры_вконтакте
Как у нас дела? В конце прошлого года я писал, что после сокращения собираюсь немного посидеть без работы, поработать над игрой и в начале 2026 года выйти на работу. Но судьба распорядилась иначе. В январе моя жена попала в больницу, и в феврале, на сроке 6 месяцев из 9, её билд аварийно завершился. Не смотря на статус "глубоко недоношенная" и экстремально низкую массу тела при рождении, дочка жива и здорова, но требует повышенного внимания и более частых посещений специалистов. Я успел пообщаться с 4 компаниями, получил 2 оффера, но решил не выходить на работу, так как не потяну сейчас работу на полный день. Зато у меня осталось время, чтобы работать над "Бурить-Копать!", чем я и не переставал заниматься всё это время. Мы почти доделали гигантское обновление игры, в котором исправили фундаментальные проблемы игры (долгие игровые сессии, скучное повторяющееся начало и слабая рекламная монетизация), выполнили все рекомендации издателя и внесли кучу долгожданных улучшений (например, бессмертие на пару секунд после воскрешения). Нам осталось поправить баланс, протестировать и от-Полли-шить игру. После этого будем выкладывать обновление на всех текущих площадках и готовится к новому раунду тестов с издателем.
Принимаем? #яндекс_игры
Сломались сохранения на Huawei Мы получили вот такую жалобу от игрока: Периодически играю в бурить копать и столкнулся с тем, что в последнее время не сохраняется прогресс. То есть, я играю, покупаю улучшения. Все хорошо. Потом выхожу, и когда захожу снова через некоторое время(через час-2)снова появляется уведомление о том, что получены ящики за вход в игру и прогресс тот же который был в первый заход. Сейчас даже включил облачные сохранения, но и после этого прогресс не сохранился Было непонятно, что с этим делать, поэтому решено было смотреть логи. Встал вопрос, как максимально дружелюбно для игрока собрать логи? С помощью Unity события Application.logMessageReceived настроил сборку логов в файл, а с помощью плагина UnityNativeShare добавил кнопку "отправить логи", которая вызывает стандартное меню "поделиться" андроида. Сбилдил игру, выложил APK на наш сайт и попросил игрока обновить игру и отправить логи. В логах были успешные записи о авторизации, чтении и записи сохранения в файл и в Google Play Services. Это насторожило, ведь игрок сказал, что у него Huawei без сервисов Google. Предположил, что проблема в сервисах Google (получаем всегда старое сохранение, новое не записывается). Так как используется автоматическое разрешение конфликтов по времени сохранения, добавил в логи информацию о времени из метаданных сохранения. Отправил новый билд игроку, получил от него новые логи. В логах виду, что время загруженного сохранения 01.01.1970 0:00:00. Хотя такого не может быть по контракту самого API гугл сервисов. /// <summary> /// A timestamp corresponding to the last modification to the underlying saved game. If the /// saved game is newly created, this value will correspond to the time the first Open /// occurred. Otherwise, this corresponds to time the last successful write occurred (either by /// CommitUpdate or Resolve methods). /// </summary> /// <value>The last modified timestamp.</value> DateTime LastModifiedTimestamp { get; } Уточняю у игрока, как такое может быть, что игра успешно соединяется с сервисами Google, хотя он пишет, что у него их нет. Получаю ответ Да, у меня установлено приложение GBox для установки приложений, которых нет в Rustore и AppGallery, а так же MicroG который идут в комплекте к GBox Понятно. На устройстве какая-то кривая заглушка для сервисов Google, которая некорректно работает. Добавляю проверку на валидность даты в метаданных сохранения, и, если дата кривая, игнорирую сохранения от сервисов Google и использую только локальные файлы (как если бы сервисы Google полностью отсутствовали). Игрок подтвердил, что проблема решена. #android
Как у нас дела? Итоги 2025 года Год начался с подготовки к тестовому закупу траффика с издателем. Мы отполлишили туториал, встроили нужные SDK, прошли QA и в феврале нам закупили примерно 4000 инсталлов в Google Play. Retention D1 был 22%, ROI отрицательный и нас отправили на доработку, основной задачей которой было поднять D1. В марте и апреле мы делали редактор шаблонов. В принципе, без него можно было обойтись, но интереса в игру он точно добавил, и мы верим, что в будущем он окупится. С мая по август делали огромный кусок работы по добавлению инапов. Мы ввели в игру оборудование (скины с уникальными свойствами) и портреты для лидербордов. Они покупаются за новую хард валюту - реликвии, которые можно купить за донат, или выбить с маленьким шансом из сундуков. Добавили и сами сундуки. В июле добавили в свой бэкенд поддержку покупок, это обязательно для ВКонтакте, заодно добавили возможность на лету менять цены, описание и награду за покупки. В мае победили в номинации лучшая браузерная игра на онлайн фестивале Игропром, выиграли стенд на оффлайн Игропроме, а также фичеринг на главной странице площадки Пикабу Игры. В конце августа добавляли в игру интеграцию c Game Push, чтобы выложить игру на Пикабу Игры. В сентябре всей командой ездили на конференцию Game Push и на Игропром. В октябре и ноябре реализовали возвратные механики (ежедневная награды: за вход и за опыт), в основе которых лежат оборудование и портреты. Также в ноябре получили несколько жалоб/запросов на добавление облачных сохранений на андроид, и запилили облачные сохранения на своём бэкенде с авторизацией по одноразовому коду из email. Сохранения через Google Play Services тоже добавили. В какой-то момент в течение этого года у нас поменялся продюсер от издателя, и вместе с продюсером поменялись ожидания от нас. Нам подсказали уйти в сторону более агрессивной рекламной монетизации и сократить время сессии. Для этого было решено полностью переделать систему умений и систему построения мира игры - мир будет поделен на этапы. В декабре мы приступили к этой задаче. Про перформанс на площадках ВКонтакте сдулся, и мы почти не получаем новых игроков, DAU 70, MAU 850 Пикабу бодрячком, площадка хоть и молодая, но игроки имеются. DAU 150, MAU 1700 Яндекс весь год падал, дошло до 40-50 DAU, но примерно через месяц после добавления инапов тренд на спад закончился, и мы потихоньку растём. Сейчас DAU 250, MAU 3500. RuStore - DAU 20, MAU 180 Google Play (только РФ) - DAU 20, MAU 180 Личное Летом моему сыну исполнилось два года, и 1 сентября он пошел в детский сад. Ждем, пока пройдет иммунизация и он станет реже болеть. 6 сентября я узнал, что стану папой во второй раз. 2 октября встретил свои 30 лет, в качестве подарка купив себе новый комп. Время компиляции проекта сократилось с 15 секунд до примерно 5. А 3 октября меня без предупреждения и парашюта сократили на работе. Я решил сразу не искать новую работу, а дать себе 1-2 месяца на то, чтобы доделать игру по советам издателя. К сожалению, из-за больничных ребенка и токсикоза жены не удалось взять ударный темп, и вот уже три месяца я сижу без источников дохода и без значимого прогресса по игре. Как бы мне не хотелось оставаться безработным, в январе открою CV и начну ходить по собесам. Кстати, никому java-помидор не нужен?
«injecting the DiContainer is usually a sign of bad practice» (from here) Цитата из документации к Zenject полностью звучит так: note that injecting the DiContainer is usually a sign of bad practice, since there is almost always a better way to design your code such that you don't need to reference DiContainer directly (the exception being custom factories, but even in that case it's often better to inject a factory into your custom factory. Once again, best practice with dependency injection is to only reference the DiContainer in the "composition root layer" which includes any custom factories you might have as well as the installers. However there are exceptions to this rule. Да, я понимаю, что использование контейнера напрямую это не стандартный способ, но вот утверждать, что это плохая практика и почти всегда есть способ сделать лучше, я не готов. Приведу пример. У меня есть обучающая цепочка заданий. У каждого задания есть возможность что-то сделать с игрой в начале задания и при его завершении. В коде это выглядит так: public abstract class QuestCallback : ScriptableObject { public abstract void Invoke(DiContainer diContainer, QuestData quest); } Даже с недоделанным до конца туториалом, у меня уже накопилось с десяток имплементаций QuestCallback, каждому из которых требуется та или иная зависимость. Какая альтернатива использованию DiContainer в этой ситуации? Явно перечислить все необходимые зависимости в параметрах метода QuestCallback.Invoke? И для каждой новой имплементации дописывать ещё параметры? А разве мы не нарушаем этим самым Open–closed principle? И чем плох мой вариант? В статье и её переводе объясняется, почему Service Locator - это антипаттерн, но вот только есть ли вариант лучше? #программирование #нерешенное
Выключаем бесячие логи от Google Play Services (Unity Jar Resolver) Я добавил в игру GooglePlayGamesPlugin и теперь при каждой компиляции и изменении любого ассета мне в лог Unity вываливаются 5 сообщений (см. картинку) [Здесь изначально был хак с помощью кода, но, как выяснилось, есть решение проще. Спасибо Максиму Ельникову из комментариев к этому посту] Чтобы отключить эти логи, нужно выключить галку Verbose Logging в Assets → External Dependency Manager → Version Handler → Settings Правда, у меня почему-то отключение галки не сработало, возможно какой-то баг, поэтому я поменял настройку в файле настроек проекта ProjectSettings/GvhProjectSettings.xml: <projectSetting name="Google.VersionHandler.VerboseLoggingEnabled" value="False" /> #android
Чиним проблему с сертификатами на Android 7 Мой единственный тестовый Android девайс, кроме основного телефона - это старенький Meizu Pro 7 на Android 7.0. Оказалось, на нем не работают запросы на наш бэкенд. Это связано с ошибкой CA: Cert verify failed. Certificate is not correctly signed by a trusted CA Решение довольно легко гуглится в интернете, правда оно немного устарело. Сейчас Unity не даёт класть файлы просто в папку Assets/Plugins/Android/res/ Exception: OBSOLETE - Providing Android resources in Assets/Plugins/Android/res was removed, please move your resources to an AAR or an Android Library. Поэтому нужно собрать zip архив из файлов, описанных в статье, поменять ему расширение на *.aar и закинуть его в Assets/Plugins/Android. Ну или взять мой aar, который я прикрепил к этому посту.
Enforce full type name qualification в Rider Эта настройка в Rider может сделать вашу разработку чуть удобнее, особенно когда вы пишете код для разных платформ (WebGL/Android). Например, чтобы вызвать JS код, нужно импортировать тип System.Runtime.InteropServices: #if UNITY_WEBGL using System.Runtime.InteropServices; #endif public static class WebUtils { #if UNITY_WEBGL [DllImport("__Internal")] private static extern bool SyncIdbfs(); #endif } Важно не забыть также обернуть using в #if UNITY_WEBGL, иначе при переключении платформы на другую, импорт станет неиспользуемым и может быть удалён при оптимизации. Но можно сделать удобнее, и в настройках Rider, в разделе Editor > General > Auto Import, в поле Enforce full type name qualification прописать тип System.Runtime.InteropServices.DllImportAttribute. После этого ваш код будет выглядеть следующим образом: public static class WebUtils { #if UNITY_WEBGL [System.Runtime.InteropServices.DllImportAttribute("__Internal")] private static extern bool SyncIdbfs(); #endif } Мне кажется, такой вариант выглядит лаконичнее и удобнее. Я также прописал себе тип GamePush.*, и обращаюсь к классам GP по полному имени: GamePush.GP_Ads.ShowSticky();
Сел в лужу с I2 Localization Почти два года назад, когда я выбирал ассет для локализации Бурить-Копать!, Unity Localization ещё была в разработке, а чуть ли не самым популярным был ассет I2 Localization. У него была куча положительных отзывов, и киллер фича - синхронизация переводов с Google Sheets. Я купил его и стал использовать в проекте. Не все в нем было идеально, в 4-5 местах пришлось доработать напильником. Пока допиливал, обратил внимание на очень плохое качество кода. Но ассет был уже куплен, и особых причин искать альтернативы не было. Некоторое время назад у меня стали подвисать операции обновления локальных данных из гугл таблиц. Я грешил на Роскомнадзор/VPN, но в конечном итоге запросы проходили. А вчера всё сломалось окончательно. Решил разобраться, в чем дело. Выяснил, что запросы к script.google.com работают в браузере и в Postman'e, но почему-то зависают и падают с таймаутом в Unity. Точную причину выяснить не получилось, и LLM не особо помог. Решил проверить, а что там с отзывами в Unity Asset Store. А там - куча единиц и жалоб на то, что ассет не поддерживается и не работает. Первая жалоба на сломанную интеграцию с Google была год назад, но у меня всё работало ещё буквально пару недель назад. То есть я буквально не могу добавить новый перевод через удобную таблицу - только через инспектор ассета. Переводить сейчас проект на другую локализацию - это огромная задача по времени. Понадобится заменить все компоненты на всех сценах и префабах, а также переделать все переводы на новый формат категорий и работы с числами. В качестве временного решения, написал простенький прокси сервер на Python, который проксирует запросы из Unity к Google. Это работает. Но задача на переезд на другой ассет уже в бэклоге. Итого: - минус $45 - минус время на допиливание ассета под себя - время на переезд на другую локализацию
Поймал вчера баг у себя в коде, хочу предостеречь вас. Что выведет в консоль следующий код (при условии, что другие скрипты могут менять значение Foo)? (опрос в следующем посте) public class Demo : MonoBehaviour { public bool Foo { get; set; } private…
видео или голосовое, без подписи
Поймал вчера баг у себя в коде, хочу предостеречь вас. Что выведет в консоль следующий код (при условии, что другие скрипты могут менять значение Foo)? (опрос в следующем посте) public class Demo : MonoBehaviour { public bool Foo { get; set; } private IEnumerator Start() { yield return new WaitUntil(() => Foo); Debug.Log(Foo); } }
Исправил зависимости в Яндекс Mobile Ads SDK В RuStore для рекламы мы используем РСЯ без медиации. При апгрейде yandex-mobileads-lite с версии 7.7.0 до версии 7.16.0 я заметил, что в пакете появились зависимости на библиотеки рекламных сетей (файл YandexMobileadsMediationDependencies.xml), необходимые для медиации, но лишние для работы только с РСЯ. Я написал об этом в поддержку РСЯ и получил ответ Сейчас Unity-плагин РСЯ использует базовый пакет "yandex-mobileads-lite-7.16.0.unitypackage" как для простой интеграции, так и для работы медиации. Во втором случае также нужно подключить нужные адаптеры, например, "mobileads-google-mediation-7.16.0.unitypackage". Файл с зависимостями нужен для корректной работы SDK. Пришлось немного поспорить Мне кажется, это неправильно. Если я не буду использовать медиацию, зачем мне тащить в проект com.yandex.android:mobileads-mediation и все его зависимости на все сети? Обещали разобраться. И вот вчера, спустя 20 дней, мне пришел ответ Спасибо за ожидание. Мы учли ваше пожелание, данный вопрос будет решен в релизной версии SDK. +1 доброе дело в копилку ✅ #репорчу_баги
Приходится править Game Push SDK API покупок в Game Push не предоставляет уникального ID/токена покупки. Списывать (консумить) покупку необходимо по ID продукта. Это плохая реализация, она не позволяет отличить две покупки одного и того же товара, что может привести к ошибкам. Также из-за этого не получится правильно дедуплицировать покупки в случае неполадок с сетью. Я завёл фиче реквест для Game Push, если вы согласны со мной, прошу поддержать лайком: https://gamepush.canny.io/feature-requests/p/consume-purchases-buy-purchase-id-token-id-id Пока что пришлось самому сделать костыль в коде Game Push, а именно 1) в _gpUnity.jspre вызывать CallPaymentsPurchase, передавая result.purchase._id 2) в GP_Payments.cs добавить поле _id в класс FetchPlayerPurchases Буду использовать их _id в качестве временного решения. #веб_игры #game_push #репорчу_баги