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 #217 · Back to latest

Older Posts 17 shown
Post #216 803
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();
  • 🔥 5
Post #214 850
Сел в лужу с 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
- минус время на допиливание ассета под себя
- время на переезд на другую локализацию
  • 😢 16
Post #213 920
SpellSinger - разработчик игр на Unity Поймал вчера баг у себя в коде, хочу предостеречь вас. Что выведет в консоль следующий код (при условии, что другие скрипты могут менять значение Foo)? (опрос в следующем посте) public class Demo : MonoBehaviour { public bool Foo { get; set; } private…
Для меня это было не очевидно, но действительно, код после
yield return new WaitUntil(() => Foo); 

выполняется не сразу же после выхода из проверки. Перед этим могут быть выполнены другие скрипты, поэтому мы не можем быть уверены в значении Foo.
Post #211 1K
Поймал вчера баг у себя в коде, хочу предостеречь вас.

Что выведет в консоль следующий код (при условии, что другие скрипты могут менять значение Foo)?

(опрос в следующем посте)
public class Demo : MonoBehaviour
{
public bool Foo { get; set; }

private IEnumerator Start()
{
yield return new WaitUntil(() => Foo);
Debug.Log(Foo);
}
}
  • ❤ 2
Post #210 1.04K
Исправил зависимости в Яндекс 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 доброе дело в копилку ✅

#репорчу_баги
  • 🔥 13
  • 😎 4
  • ❤ 2
  • 👍 1
Post #209 851
Приходится править 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 #репорчу_баги
gamepush.canny.io
  • 👍 5
Post #208 920
Как перестроить UI при включении/выключении дочернего элемента? Внедряемся в Layout.

Сегодня снова про UI. Ничто не отнимает у меня столько времени, как борьба с UI
.

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

В Unity есть коллбек OnTransformChildrenChanged, который вызывается, когда добавляется или удаляется новый дочерний элемент. Но этот коллбек не вызывается, когда дочерний элемент становится активен или неактивен.

Можно постоянно высчитывать количество активных дочерних элементовв Update, но это на самый крайний случай. Лучше не делать никакую логику в Update.

Можно повесить на дочерние элементы скрипт, который будет уведомлять родителя о своем включении/выключении, но это неудобно.

Предлагаю решение на основе методов SetLayoutHorizontal/SetLayoutVertical из интерфейса ILayoutController. Юнити вызывает их, когда перестраивает layout, то есть как раз, когда нам нужно.

После изменения нужных нам параметров, нужно попросить Unity заново построить layout (LayoutRebuilder.ForceRebuildLayoutImmediate), желательно в том же самом кадре, чтобы не было никаких миганий. Но при этом не уйти в рекурсию.

Полный код решения:
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

[Serializable]
public class CellSizeRule
{
[field: SerializeField] public int MinCount { get; private set; }
[field: SerializeField] public Vector2 CellSize { get; private set; }
}

[RequireComponent(typeof(GridLayoutGroup))]
[ExecuteAlways]
public class DynamicCellSizer : UIBehaviour, ILayoutController
{
[SerializeField] private CellSizeRule[] rules;

private GridLayoutGroup grid;
private int count = -1;

private GridLayoutGroup Grid
{
get
{
if (!grid)
grid = GetComponent<GridLayoutGroup>();
return grid;
}
}

protected override void OnEnable()
{
base.OnEnable();
ApplyRule();
}

private void ApplyRuleIfNeeded()
{
var count = transform.Cast<Transform>()
.Where(t => t.gameObject.activeSelf)
.Select(t => t.GetComponent<LayoutElement>())
.Count(le => le == null || !le.ignoreLayout);

if (count == this.count)
return;

this.count = count;

ApplyRule();
}

private void ApplyRule()
{
foreach (var rule in rules.OrderByDescending(r => r.MinCount))
{
if (count >= rule.MinCount)
{
SetSize(rule);
return;
}
}

if (rules.Length > 0)
SetSize(rules[^1]);
}

private void SetSize(CellSizeRule rule)
{
Grid.cellSize = rule.CellSize;
LayoutRebuilder.ForceRebuildLayoutImmediate(Grid.transform.parent as RectTransform);
}

public void SetLayoutHorizontal() => ApplyRuleIfNeeded();
public void SetLayoutVertical() => ApplyRuleIfNeeded();

#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
ApplyRule();
}
#endif
}

#ui
  • 👍 9
  • 🔥 8
  • ❤ 1
  • 🤯 1
Post #207 902
Post #203 983
  • 🔥 25
  • ❤ 7
  • 👍 6
  • 😍 1
  • 💘 1
Post #201 916
Начинаем шоукейс на Игропром

Остальные фото будут в комментариях к этому посту.
  • 🔥 31
  • ❤ 6
  • 👏 3
Post #200 717
К шоукейсу на GamePush Conf готовы

Остальные фотки будут в комментариях к этому посту.
  • 🔥 15
  • ❤ 5
  • 👍 4
  • 👏 1
Post #199 742
Увидимся?

Завтра (18 сентября) мы вместе с художником будем на конференции GamePush, будем показывать Бурить-Копать! в зоне шоукейсов.

С пятницы по воскресенье (19-21 сентября) будем на фестивале Игропром со стендом и косплеем нашего маскота Полли.

Мы заготовили мерч: значки и стикерпаки.

Подходите, давайте знакомиться лично 😉
  • 🔥 11
  • 👍 2
Post #198 716
Добавили промокоды в игру

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

Фичи:
1) Ограничение по количеству активаций.
2) Ограничение по дате начала и окончания.
3) Один и тот же игрок не может использовать промокод дважды.
4) Можно объединять промокоды в группы. Один игрок не может использовать разные промокоды из одной группы.
5) Клиент (игра) может получить информацию о награде без активации, чтобы проверить, можно ли её активировать (например, достиг ли игрок необходимого прогресса).
6) Ограничение по площадкам.

Для подписчиков этого канала сделал промокод. Активируется в настройках (Пауза - Настройки).
ТЕЛЕГА1

Он действует до конца сегодняшнего дня и даёт 100 счастливых монеток. Активировать можно на Пикабу, ВКонтакте и Яндекс Играх, на андроиде пока старый билд.

#бурить_копать #бэкенд
  • 🔥 12
  • ⚡ 2
  • 👍 2
Post #197 759
Постоянные покупки: консумить их или нет?

Покупки в играх могут быть одноразовые/используемые (ресурсы, монетки, бустеры) и постоянные (отключение рекламы, вип статус). И есть два способа, как реализовать постоянную покупку. Расскажу на примере ЯИ.

После того, как игрок совершил покупку, метод payments.getPurchases() будет возвращать её. Если консумить покупку - вызывать метод payments.consumePurchase() - покупка будет списана, и больше не будет возвращаться из метода payments.getPurchases().

Соответственно, есть два варианта, как работать с постоянными покупками.
1) Консумить постоянную покупку, записывать инфу о ней в сохранение.
2) Не консумить покупку, и проверять её наличие в списке покупок.

Документация и поддержка ЯИ советуют использовать 2 вариант:
Для обработки постоянных покупок применяйте метод payments.getPurchases().


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

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

Для себя я решил всё таки консумить покупки, и запоминать инфу в сохранение. Я вижу несколько плюсов в этом подходе.
1) Все покупки обрабатываются единым способом, не надо писать отдельную логику для используемых и постоянных покупок.
2) Я могу сделать продукт (инап), в котором будет, например, и вип статус, и ещё кучка расходников.
3) Я могу выдывать вип-статус с помощью промокода (игрок вводит промокод, игра записывает ему в сохранение вип статус). В список покупок игра ничего не сможет записать.

Минусов я не вижу, кроме теоретической ситуации, когда игрок теряет свою сохранение вместе со своей постоянной покупкой. Но это, кажется, почти невозможный сценарий.

#инапы
  • 👍 3
Post #196 796
Как дела у «Бурить-Копать!»?

До недавнего времени в игре была только рекламная монетизация. Но в Бурить-Копать! она плохо работает. У нас получились довольно длинные игровые сессии, которые мы пока что никак не можем сократить. Поэтому у нас был выбор: или вставлять рекламу посреди забега (после повышения уровня), или оставить только интерстишелы после рестарта.

Первый вариант не подходит - он сильно бьёт по удержанию. А второй вариант очень мало зарабатывает, и даже не смотря на средний плейтам в полчаса, игра медленно умирала на ЯИ, и потихоньку жила в ВК.

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

Задача заняла, конечно, кучу времени, больше всего потратили на UI магазина, и на рефакторинг существующего кода, чтобы было легко добавлять новые механики. На своей шкуре прочувствовал важность буквы O из SOLID (Open/Closed Principle).

Мы уже вышли с инапами в ЯИ, ВК, на Пикабу и других партнерских площадках GP. Игроков у нас очень мало, чтобы делать какие-то выводы, но покупки уже есть, и это прям очень радует. Огромное спасибо всем, совершившим покупку в игре, это должно очень сильно её бустануть. Это при условии, что мы не делаем никаких офферов, скидок и т. д. Мы просто добавили магазин и всё.

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

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

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

Сейчас мы делаем возвратные механики (daily reward, trophy road), новые инапы (no ads, starter pack) и готовимся к новому тестированию на Android с издателем.

Ну и конечно ждём, как алгоритмы веб-площадок отреагируют на появление инапов (и покупок!).

#бурить_копать
  • 🔥 13
  • ❤ 4
  • 👍 3
  • 😎 1
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 →