TGViewer
Channel Public Channel
SpellSinger - разработчик игр на Unity

SpellSinger - разработчик игр на Unity

@spellsingergames

Привет! Я Денис и я делаю игры на Unity. В этом блоге я делюсь своим опытом в геймдеве и не только.

Навигация в закрепе: https://t.me/spellsingergames/3

Написать в личку: @sinedsem
Subscribers
548
Photos
90
Videos
22
Links
105

Showing posts older than #154 · Back to latest

Older Posts 20 shown
Post #153 874
Хм, а это что такое?
  • 👀 5
  • 🤯 2
Post #152 893
Ничего не изменилось
  • 😢 9
  • 🤣 4
  • 🤪 3
  • 👀 1
Post #151 877
Мы убрали стартовый интерстишел в Яндекс Играх 🤪

Подглядели в одной популярной игре, что они показывают интерстишел не в момент загрузки игры, а после нажатия «Играть». Решили сделать также. Теперь этот интер точно не будет влиять на первый шаг моей воронки загрузки, посмотрим, изменит ли это что-то.

Также, возможно я нашёл другую причину отвалов на загрузке. Когда я добавлял интеграцию с ВКонтакте, я вынес в отельный js-файл константы - названия моих объектов, методы которых я посылаю из JS. Возможно, этот скрипт не всегда успевал подгрузиться вовремя, хоть он и весит пару байт, и в этом случае игра не загружалась.

Ну и последнее изменение - я в очередной раз поменял подход к загрузке сцен и данных мира. Я добавил сцену с игрой в билд (раньше загружал её через Addressables), что конечно замедлило первоначальную загрузку, но к ней игроки должны быть привычны, зато потом останется загрузить только мир.

Только что прошли модерацию, послезавтра посмотрим, поменялась ли конверсия.

Ссылка на игру

#бурить_копать #яндекс_игры
Яндекс Игры Бурить-Копать! - играть онлайн бесплатно на сервисе Яндекс Игры Бурить-Копать! — динамичный 2D-рогалик про поиск сокровищ, улучшение бура, мощные комбинации умений и соревнование за максимальную глубину.
  • 👍 14
  • 🔥 6
  • ❤ 1
Post #150 979
Жесть с отказами на странице Бурить-Копать! в Яндекс Играх

Анализирую данные Яндекс Метрики. Счетчик не РСЯ, а мой, который непосредственно встроен в игру.

Почему-то у игры огромное количество посетителей, которые закрывают игру через 1 секунду после загрузки.

ВКонтакте таких почти нет.

Я теперь думаю, что это даже не техническая проблема с игрой - даже если игра долго грузится - этого же не понять через секунду.

Есть сессии по 12-20 секунд, это как раз похоже на тех, кто не дождался.

Но что за сессии по 0-1 секунду? Мне кто-то ботов насылает?

#яндекс_игры
  • 🤔 14
Post #149 830
  • ❤ 7
Post #148 886
В ходе проверки были обнаружены следующие замечания:

Согласно пункту 4.5.1 награда должна быть понятной и очевидной. Пользователю должно быть понятно, что именно он получит в качестве вознаграждения.. Маркер рекламы визуально не привязан к кнопке награды

#яндекс_игры
  • ❤ 3
  • 🔥 2
  • 🤔 2
Post #147 957
Воронка загрузки игры ВКонтакте и в Яндекс Играх

С помощью Яндекс Метрики настроил цели и составил воронки загрузки игры. Собираю чуть больше шагов, но для наглядности составил воронку из 4. На скриншотах выбран режим отображения "Конверсия от предыдущего шага".

А теперь вопрос. Почему в Яндекс Играх 40% игроков не дожидаются загрузки Unity (напоминаю, нужно скачать 7мб), а в ВК таких торопыг только 7%?

Билды идентичные. Вес идентичный. Со стартовым интерстишелом уже разобрались, это норма для ЯИ и не влияет ни на что.

Если смотреть разбивку по устройствам, то в ЯИ на ПК отваливаются до загрузки Unity 22%, а на смартфонах вообще 46%.

Я ничего не понимаю.

#веб_игры #яндекс_игры #игры_вконтакте
Post #146 804
Надоело вручную пихать билд в zip-архив для Яндекс Игр

Написал скриптик, который архивирует папку. Давно пора было так сделать.
using System.IO;
using System.IO.Compression;
using UnityEngine;

public class ZipFolderUtility
{
public static void ZipFolder(string folderPath)
{
if (!Directory.Exists(folderPath))
{
Debug.LogError("Folder does not exist: " + folderPath);
return;
}

var zipName = $"{Path.GetFileName(folderPath)}.zip";
var zipPath = $"{folderPath}/{zipName}";
var oldZipPath = $"{folderPath}/_{zipName}";

if (File.Exists(oldZipPath))
File.Delete(oldZipPath);

if (File.Exists(zipPath))
File.Move(zipPath, oldZipPath);

using var zipToOpen = new FileStream(zipPath, FileMode.Create);
using var archive = new ZipArchive(zipToOpen, ZipArchiveMode.Create);
var files = Directory.GetFiles(folderPath, "*", SearchOption.AllDirectories);
foreach (var filePath in files)
{
var entryName = filePath.Substring(folderPath.Length + 1).Replace("\\", "/");
if (entryName.EndsWith(zipName))
continue;
archive.CreateEntryFromFile(filePath, entryName);
}

Debug.Log("ZIP created: " + zipPath);
}
}


#яндекс_игры
  • ❤ 7
  • 👍 4
  • 🔥 3
  • 🤔 2
Post #145 1.05K
Про баннер в лицо на Яндекс Играх

На Яндекс Играх все игры показывают Interstitial (Fullscreen) рекламу во время загрузки игры. Это норма и про это говорят сами представители ЯИ.

Да, если вы не сильно знакомы с площадкой, это может показаться дикостью, и мы сами проходили через этот этап, но отказ от этого баннера повлияет на метрики вашей игры с точки зрения алгоритмов Яндекса.

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

#яндекс_игры
  • 💩 7
  • 👍 4
Post #144 1.23K
Я выяснил, почему Бурить-Копать! провалилась в Яндекс Играх

Метрики в Яндекс Играх можно смотреть по разным типам устройств (ПК 🖥 или мобила📱), но почему-то я не замечал эти фильтры в консоли ЯИ, пока не прочитал новость о том, что это сегментирование доступно теперь и в метриках монетизации.

Я выбрал фильтры и 😱🤯. Смотрите сами (среднее за неделю):
Игроки: 🖥 57 📱142 (сумма 199)
Время играния на игрока: 🖥 25.8 📱4.4 (среднее 10,5)
Конверсия в игровую сессию: 🖥 72.6 📱20.7 (среднее 31.5)
Возвращаемость 1-ого дня:🖥 13.8 📱1.9 (среднее 4)

У нас очевидно какая-то проблема с мобильной версией игры. Она или не грузится вообще, или грузится слишком долго с мобильным интернетом, который в среднем медленнее.

Вчера мы выложили обновление, в котором я с помощью Яндекс Метрики сделал воронку основных этапов загрузки. Zip-архив со всем игрой у меня весит 20 МБ, но грузятся они не сразу.
1) Сначала загружаются скрипты Unity и пустая сцена – 7МБ
2) Потом загружается главное меню – 0.5 МБ
3) Потом уже грузится основная сцена с игрой и мир.

Воронка показала, что самого первого шага, загрузки Unity, дожидаются только 61% игроков со смартфонов, до главного меню доходят только 27%, а в игру попадают всего 11%.

В связи с этим, мне нужна ваша помощь.
1) Пожалуйста, откройте игру с вашего смартфона, и, желательно, с мобильным интернетом, и напишите, пожалуйста, как долго она у вас грузилась, а главное, нет ли каких-нибудь ошибок.
2) Если есть какие-то другие советы, как можно ускорить загрузку самого Unity, напишите, пожалуйста. Мой wasm.br весит 5.35 МБ, в Unity уже отключены все лишние пакеты, IL2CPP Code Generation = Faster (smaller) builds и Stripping Level = High.

Скриншот воронки загрузки кину в комменты.

#бурить_копать #яндекс_игры
  • 😱 11
  • 👍 2
  • 👻 2
  • 🌚 1
Post #143 754
Делаем стороны картинок кратными 4

Чтобы использовать Crunch Compression, нужно, чтобы ширина и высота текстуры были кратны 4. Заставить художника помнить об этом и вырезать спрайты по этому требованию - задача не выполнимая, поэтому я решаю её на своей стороне.

Один из вариантов - распихать все спрайты по атласам, но это не всегда удобно. Иногда проще отредактировать конкретный спрайт. Для этого у меня уже давно написан инструмент, который делает это в один клик прямо внутри Unity.

Вчера я его доработал. Теперь в нем два режима.
1) Default - добавляет пустые пиксели по краям изображения (как раньше)
2) Stretch Center - растягивает изображение в центре. Этот новый режим особенно полезен для 9-Sliced UI-элементов.

Обновляйте мой пакет или устанавливайте его по инструкции из этого поста. Ищите в контекстном меню пункт Resize PNGs to multiple of 4.

#selfmade #ui
  • 🔥 7
  • 🫡 5
  • 👍 2
  • ❤ 1
  • 🤝 1
Post #142 816
Сервер для Unity WebGL игры на Python

Делюсь своим сервером, которым я пользуюсь для локального запуска WebGL билдов . Он выставляет правильные заголовки для .wasm и .data файлов, и выставляет правильный заголовок при использовании Brotli сжатия.

Для запуска сервера нужен установленный Python.

https://gist.github.com/sinedsem/4c9727097f76648b697aaa7b1a289054

UPD: по совету из комментариев обновил код. Добавил необязательные параметры для указания порта и папки.

Команда для запуска сервера:
python server.py --dir ./Build —-port 8080

На маках вместо python нужно писать python3

#selfmade
Gist Python web server for Unity WebGL Python web server for Unity WebGL. GitHub Gist: instantly share code, notes, and snippets.
  • 👍 7
  • 🔥 1
Post #141 907
Как добавить на сцену сразу несколько спрайтов?

Если перетащить на сцену один sprite, то Unity создаст новый объект со Sprite Renderer'ом. Но если перетащить на сцену несколько sprite'ов, то Unity предложит создать из них анимацию.

А если вам нужно несколько Sprite Renderer'ов, а не анимация, то тащите спрайты с зажатой клавишей Alt.

По традиции канала, ставь 🤡 если это было очевидно и без меня.
  • 👍 23
  • 🤡 5
  • ❤ 2
  • 🍌 2
  • 👀 1
Post #140 1.12K
Вручную назначаем спрайты частицам

Такую анимацию, как на видео, легко сделать системой частиц. Кроме одного момента: стандартными средствами Unity нельзя задать конкретный Sprite конкретной частице. C помощью ParticleSystem.EmitParams можно указать положение, размер, вектор движения, время жизни и ещё несколько параметров, но не конкретный Sprite.

Но если очень нужно, то можно использовать костыль. Мы будем управлять цветом частицы, а с помощью шейдера преобразовывать цвет в UV конкретного спрайта в текстуре.

Для простоты я взял квадратную текстуру с 16 спрайтами (по 4 в строке и 4 в колонке). В Unity нарезал её на спрайты. И делаю emit частицам следующим образом:
var dimensions = 4;
var sprite = ...
particleSystem.Emit(new ParticleSystem.EmitParams
{
position = ...,
startSize = sprite.texture.width / sprite.pixelsPerUnit / dimensions,
startColor = new Color(sprite.rect.x / (sprite.rect.width * dimensions),
sprite.rect.y / (sprite.rect.height * dimensions), 0, 1),
velocity = ...
}, 1);

Далее шейдером перевожу r и g компоненты цвета в uv координаты текстуры. Шейдер и пример текстуры ищите в комментах к этому посту. Там же можно задать мне вопрос, если что-то осталось не понятно.

За этот трюк безмерно благодарен зрителю моих стримов, который, если я правильно помню, предпочитает оставаться анонимом.

#тайные_знания
  • 👍 20
  • 🔥 2
  • 👏 2
  • 🤯 2
  • ❤ 1
Post #139 997
Делюсь результатом А/Б-теста иконки и обложки на Яндекс Играх. Здесь результаты только за 5 дней, но больше держать нет смысла, эксперимент откровенно хуже.

Это и логично, ведь этот эксперимент я сляпал самостоятельно, без художника.

Однако, выросший CTR обложки дает идеи на следующий тест.

#бурить_копать #яндекс_игры
  • 👍 10
  • 🤔 2
  • ❤ 1
Post #138 4.16K
Зачем использовать Git, даже если работаешь в одиночку?

1️⃣ История
Когда я работаю с системой контроля версий (VCS, Git как частный случай), я фиксирую (коммичу) каждое изменение в своём проекте. И подписываю его (commit message). Я могу вернуться в любой момент истории проекта, посмотреть что я сделал и зачем.

2️⃣ Бэкапы
Я не сломаю и не потеряю свой проект. Я слышал истории, когда у разработчиков переставал открываться проект, а последний бэкап был сделан месяц назад. С git я потеряю только то, что не успел закоммитить (и поэтому коммитить нужно как можно чаще).

3️⃣ Контроль изменений
Перед тем, как закоммитить очередной кусочек работы, я обязательно просматриваю список изменений в файлах. Очень часто на этом этапе я отлавливаю недоделки или, что ещё хуже, случайные/временные изменения, которые я забыл откатить.

4️⃣ Наглядность
Каждый раз, когда я добавляю какую-то зависимость в свой проект, я коммичу все свои изменения, и коммичу изменения сразу после добавления и настройки зависимости. Благодаря этому я знаю, что появилось в моем проекте из-за этой зависимости и что нужно будет удалить, если я решу от нее отказаться. Или если мне не нравится зависимость, я просто откатываю все изменения до коммита.

5️⃣ Работа над разными задачами параллельно
Когда я работаю один, я всё равно иногда использую ветки (branch). Если я начинаю работать над какой-то большой задачей, и в процессе понимаю, что мне, например, нужно сделать фикс в релизной версии, я коммичу большую задачу в отдельную ветку и переключаюсь на мастер, чтобы сделать фикс.

6️⃣ Отсутствие мусора в коде
Я смело удаляю куски кода, ставшие ненужными. Я знаю, что я в любой момент я смогу найти этот код в истории, и мне в работе не мешаются закомментированные куски кода.

7️⃣ Работа на нескольких устройствах
Дома я работаю за десктопным ПК, а в поездки езжу с ноутом, и благодаря git я имею актуальную версию игры (также как и всю историю) на всех устройствах.

P.S. для Unity я использую Git и расширение LFS для ассетов.

#программирование
  • 👍 16
  • 🔥 14
  • 👏 3
  • ❤ 1
  • 💯 1
Post #137 852
Давайте будем помогать друг другу

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

В комменты кину ещё пару примеров моих последних запросов.

#репорчу_баги
  • ❤ 10
  • 🤝 4
  • 🫡 4
Post #136 860
Я выстрелил себе в ногу

Распространенное выражение среди программистов, но мне всегда было только примерно понятно, что оно означает. А вчера я смог это прочувствовать на себе.

TL;DR Я обосрался с циклическими зависимостями

Не так давно у нас в игре появилась система заданий. С помощью этой системы мы реализовали обучение. По мере обучения в игре разблокируются новые механики и элементы интерфейса. Соответственно, в игре есть класс QuestService, который отвечает за все задания, и от него зависят все остальные сервисы, в которых что-то разблокируется по мере прохождения обучения.

Так как обучение появилось в игре не сразу, есть ряд игроков, которые уже прокачались, и им не нужно показывать всё обучение или его часть. Для этого предусмотрена возможность пропустить квест по какому-то условию. Естественно, эти условия вычисляются при инициализации QuestService.

Добавляя последний квест, я сделал условие, которое зависело от сервиса Foo, при этом сам этот сервис Foo зависел от QuestService. В итоге, при вычислении условия, код получал не проинициализированную до конца зависимость, и падал с NRE (NPE, прим. для моих коллег джавистов). Обиднее всего то, что баг был плавающий, и не воспроизвелся, когда я тестировал, поэтому он попал на все площадки, где есть наша игра, и нам срочно пришлось выпускать хотфикс.

Чтобы не допустить такого в будущем, я буду потихоньку рефакторить весь код, и переходить от field injection к constructor injection. Ибо
Constructor injection guarantees no circular dependencies between classes, which is generally a bad thing to do. Zenject does allow circular dependencies when using other injections types however such as method/field/property injection

Изначально я поленился использовать constructor injection, потому что приходилось писать больше кода, и потому что я делал сервисами монобехи (а для монобехов не поддерживается constructor injection), чтобы хранить в них ссылки на объекты сцены или какие-то serialized параметры. Видимо, очень зря. Надо будет поискать какой-нибудь lombok для C# или плагинчик для Rider, который будет генерировать конструкторы, и делать монобехи со ссылками и параметрами отдельными зависимостями для сервисов, чтобы сервисы были обычным объектами.

#программирование
  • 👍 16
Post #135 1.49K
Быстро делаем A/B тесты с помощью динамических выражений

Обычно для проведения A/B теста нужно сделать новый билд, в котором будут использоваться значения с сервера. Я предлагаю ускорить этот процесс и с самого начала заложить в игру возможность удаленно поменять почти любой параметр.

Предлагаю для этого использовать ключи и значения конфигов определенного формата.

Ключи у нас будут типа dynamic_0, dynamic_1, ..., dynamic_N
Мы будем по очереди запрашивать значение каждого ключа, пока не дойдём до отсутствующего:
for (var i = 0; i < 100; i++)
{
var expression = Get($"dynamic_{i}");
if (string.IsNullOrWhiteSpace(expression))
break;

// apply configs
}


Значения будут вот такого формата:
desert>Nitro.Levels[0].Cost=[50]


Сначала идет входная точка (desert>). Это должен быть какой-то объект, на который у вас есть ссылка. У меня в проекте почти до любого параметра можно добраться через корневой объект, описывающий мир, поэтому desert - это идентификатор мира. У вас в проекте это может быть всё, что угодно.

Далее идет само выражение Nitro.Levels[0].Cost=[50]. В данном примере мы берем свойство Nitro корневого объекта, у него берем свойство Levels, обращаемся к 0 элементу массива Levels, и в свойство Cost, которое у меня является массивом int'ов, записываем значение [50].

Код, который парсит такие выражения и записывает значения в объекты я уже написал. Забирайте его тут. Он умеет:

- Парсить выражения
- Переходить по вложенным объектам
- Переходить по элементам массивов и списков
- Изменять значения приватных свойств
- Работать с string, int, float и bool
- Задавать одиночные значения, List'ы и массивы.

На вход методу передавайте корневой объект и выражение, которое нужно применить к этому объекту. Код не умеет работать с полями, только со свойствами, но при желании его можно доработать самостоятельно.

#самый_крутяк #selfmade
Gist PropertyOverrideUtil for Unity PropertyOverrideUtil for Unity. GitHub Gist: instantly share code, notes, and snippets.
  • 🔥 8
Post #134 900
Выключаем Android-зависимости

Если вам по какой-то причине нужно временно убрать Android-зависимость из сборки, например, для тестового билда или для билда под разные платформы, то можно закомментировать зависимость в xml-файле, её описывающем. В том числе, с помощью кода.

Например, вот так я убираю YandexMobileAds из билда для Google Play, потому что там используется AppLovin.

SetDependenciesCommented("Assets/YandexMobileAds/Editor/YandexMobileadsDependencies.xml", true);


public static void SetDependenciesCommented(string xml, bool value)
{
if (File.Exists(xml))
{
var content = File.ReadAllText(xml);
if (value)
{
content = Regex.Replace(content, "^<dependencies>(\r?\n)", "<dependencies><!--$1");
content = Regex.Replace(content, "(\r?\n)</dependencies>$", "$1--></dependencies>");
}
else
{
content = content.Replace("<dependencies><!--", "<dependencies>");
content = content.Replace("--></dependencies>", "</dependencies>");
}

File.WriteAllText(xml, content);
}

PlayServicesResolver.ResolveSync(false);
}

#android
  • 🔥 5
  • 👍 2
Older posts →
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →