Это первая часть истории (вперемешку с рассказом о моих ошибках и их решениях) о том, как я (где-то два года в свободное время) разрабатывал мобильное приложение под iOS и Android, которое бы мотивировало мою дочь решать примеры по математике, чтобы она достигла автоматизма в основах арифметики (склад числа 10 или табличка умножения). В итоге, получилось приложение, позволяющее ребёнку зарабатывать деньги своим умом.
Использовал я движок Unity и язык C#, а также дополнительный набор софта вроде обязательного Photohsop или Audacity (для создания звуков).
План рассказа (часть первая)
- Предыстория
- О монетизации
- Почему Unity
- О Scriptable Objects
- О плагине Anima2D
- О Lean Localization
- Об iTween
- О Unity Analitics
- О Visual Studio
- Ссылки
Предыстория и колорадские жуки
Мой предыдущий опыт — это несколько лет в 3D и, с недавних пор, разработка нескольких инди-игр на пару с программистом, где я выступал в основном только в роли дизайнера и художника (очень редко пописывая какие-то элементарные скрипты на C#). Хотя, с программированием знаком не понаслышке (колупал Basic в школе и баловался с C/C++ в универе).
Вся эта эпопея началось с проблемы. Я увидел, что при решении школьных задач, дочь «буксует» и ошибается не на чём-то сложном, а на основах. Я решил, что будет прикольно, если я напишу ей забавное приложение, где она в игровой форме будет решать примеры (набираясь таким образом опыта и достигая автоматизма). И, чтобы мотивировать её ещё больше, я сделал так, что за правильное решение примеров она получала деньги (количество денег подсчитывало приложение на основе количества правильных ответов, а я потом выводил нужную сумму, выплачивая дочери наличные).
Хотя… эта история началась ещё раньше. Сначала я сделал дочери приложение, которое выплачивало деньги за то, что она учила английские слова. Но это приложение оказалось намного сложнее реализовать так, чтобы оно стало удобным не только для меня (разработчика) и моей дочери, но также и для других родителей. Поэтому, английское приложение пока так и остаётся внутренней разработкой.
Я выбрал деньги в качестве мотиватора, поскольку это было проще всего реализовать. А ещё из-за личных воспоминаний из детства: я обожал делать всё за деньги. Даже если это была очень нудная работа, типа собирать колорадских жуков с картошки. Помню, насобирал их полулитровую банку (родители мне платили 1 копейку за каждого жука). Вот я и подумал, что раз дочь моя (я это не проверял, но крепко уверен в этом), то и ей это должно понравиться. Ну и в итоге не прогадал.
Моя жена против того, чтобы стимулировать детей делать что-то за деньги. Но мой аргумент её разубедил немножко: в случае этого приложения, ребёнок получает деньги не за то, что он и так должен делать (типа домашнее задание), а за то, что он дополнительно практикует математику в своё свободное время.
Вторая причина, почему я взялся за разработку — я хотел попрактиковаться в программировании. «Стать программистом» было моей заветной мечтой ещё с первых классов школы (сразу после мечты «стать учёным», но перед мечтой «делать мультики»).
Монетизация и удовольствие
Изначально я делал это приложение (рабочее название было Math4Ami) исключительно под iPod touch 5 дочери. Я даже не думал делать приложение доступным на всех iOS устройствах или публиковать его для всех и, тем более, не думал выпускать его для платформы Android (наслушался страшных историй от iOS разработчиков плюс, мне не на чём тестировать Андроид версию).
Какое-то время назад я загорелся желанием опубликовать его в AppStore (меня очень манила мысль, что у меня будет своё приложение в Apple магазине и все это смогут увидеть).
Подумал, что за месяц управлюсь. Ведь весь функционал был готов, оставалось дело за малым — сделать его рабочим для всех соотношений сторон экрана и понятным не только для меня, но и для других родителей. И вот, спустя полгода, я опубликовал его в AppStore и Google Play.
Я изначально решил сделать Math4Ami полностью бесплатным без намёка на монетизацию. Причин для этого решения несколько.
Первая. Как вы уже поняли, я изначально делал его бесплатным для дочери и не хотел ничего прикручивать в «конце» разработки.
Вторая. Я решил, что это будет разработка для собственного удовольствия. У меня уже есть подобный опыт — я делаю блог для удовольствия (который изначально только кушал средства: деньги на
Третья — ради более широкой аудитории, которая бы значительно урезалась, сделав я приложение платным (как делают многие разработчики детских приложений). Другие типы монетизации я отмёл по нижеописанным причинам.
Внутриигровую рекламу я на дух не переношу — не люблю когда дизайн приложения уродуют рекламные сообщения (разве что кроме рекламы в виде просмотров видео по желанию, а не когда видео выскакивает из-за угла). К тому же, для участия в программах «Made for kids» у Apple и «Designed for Families» у Google, нужно строго фильтровать показываемую детям рекламу.
Внутриигровые покупки я сам, как родитель, блокирую на всех устройствах и дети, когда качают приложения самостоятельно, просто физически не могут ничего купить внутри приложения. Другое дело, когда родитель сам изначально покупает приложение для ребёнка (но об этом я уже сказал выше).
Почему Unity и как
Выбрал я Unity поскольку работал в нём раньше и мне понравилось. Также у меня был хороший друг программист на C# и я надеялся, что он мне поможет с программированием, если что. А ещё у Unity шикарное сообщество и очень легко найти ответы в Гугле почти на все вопросы реализации чего-то там на C# + Юнити.
С Unreal я тоже работал (как 3D художник), но так и не разобрался ни с C++, ни с 2D функционалом.
Изначально Math4Ami было «облачным», хотя это и громко сказано. Все данные хранились на моём eVPS (Elastic Virtual Private Server) и я использовал FTP, чтобы передавать TXT-файлы с данными и настройками приложения (до использования базы данных руки так и не дошли, хотя первые шаги в написании своего сервера на node.js я предпринял). Для работы с ftp я прикрутил к Unity лёгкий в использовании Simple C# FTP Class.
Потом, когда я решил сделать app публичным, я отказался от серверной части.
С одной стороны, это было бы слишком заморочено: делать аутентификацию (этого пользователи ох как не любят) или сохранять в iCloud идентификатор сессии с помощью NSUbiquitousKeyValueStore (это позволило бы автоматически идентифицировать пользователя между удалением апликухи и повторной установкой), но я так и не разобрался с этим (возможно, мне бы помогла статья Пишем плагин для Unity правильно. Часть 1: iOS, но тогда её ещё не было).
С другой стороны, данные в этом приложении не такие уж и важные, чтобы их нужно было хранить на сервере.
С третьей стороны, не было необходимости в серверной синхронизации. Вот для моего приложения по обучению английскому — там да, синхронизация была нужна. Поскольку родитель добавляет новые слова в родительском приложении, а ребёнок учит их в детском приложении (хотя, может я и любитель всё усложнять).
В итоге я сделал, чтобы всё хранилось локально (на устройстве), но уже не в txt, а JSON формате.
ScriptableObject и правильные ответы
JSON формат в связке с ScriptableObject оказался шикарной находкой. Я использовал родные методы UnityEngine для сериализации объектов в json — JsonUtility (а потом сохранял текстовые файлы json локально на устройстве в папку Application.persistentDataPath).
ScriptableObject (SO) — это отдельная тема разговора, но я всё же её затрону. Даже не представляю, как я раньше жил без SO.
Всё что я использую в работе, я почерпнул из этих двух мегаполезных видео о принципах работы с SO (и сопутствующего кода на GitHub и Bitbucket):
- Game Architecture with Scriptable Objects (код);
- Overthrowing the MonoBehaviour Tyranny in a Glorious Scriptable Object Revolution (код).
Лично я использовал SO для таких целей:
- Для хранения данных (чтобы не нужно было каждый раз лезть в код для добавления нового функционала или данных):
- разновидность примеров,
- тип валюты,
- стиль кнопок (у меня во многих местах одинаковые кнопки и я просто создаю skin-ы для них на основе SO),
- величина награды и т.д.
- В качестве глобальных переменных (которые видны во всех сценах):
- количество верных ответов,
- количество заработанных денег,
- активные в данный момент настройки,
- текущий тип примера,
- таймер, рекорды и т.п.
- Для хранения логики (к примеру, подписка на событие получения правильного ответа).
Единственный минус SO для работы с данными — в нём нельзя хранить данные между сессиями приложения: ассет SO (после холодного запуска приложения) будет всегда содержать данные, которые вы записали туда в редакторе. Поэтому логика работы у меня такова:
- После старта приложения я считываю json-файлы с диска и подгружаю данные из них в ассеты SO (метод FromJsonOverwrite).
- Пока приложение работает и мне нужна максимальная производительность — я работаю только с ассетами Scriptable Object-ов. Эти asset-ы хранят данные всё время, пока приложение запущено или находится в фоне.
- Когда нужно сохранить данные (к примеру, при завершении приложения или по ходу работы), то я сериализую SO в json (метод ToJson) и сохраняю на диск.
Есть (очевидный) недостаток такого подхода — нельзя сохранить на диск только один изменившийся параметр (если их несколько в SO), всё время приходится сохранять текстовый json-файл полностью.
Но многие данные не нужно сохранять на диск (к примеру, текущее количество правильных ответов) и тогда SO является мощным инструментом, позволяющий значительно упростить мне работу.
На видео ниже я показываю пример моей реализации учёта верных и неправильных ответов с помощью UnityEvent (событие — изменилось ли количество правильных ответов) + Listener (слушатели делают какую-то работу, если услышали, что получен правильный ответ, а логика подписки слушателей на событие также реализована на SO) + SO (ведёт учёт количества правильных ответов):
Таким образом, я могу не только руками вводить правильные и ошибочные ответы, но просто двигая ползунок, генерировать новые примеры и тестировать логику работы приложения.
Anima2D, персонажи и дёрганая улыбка
На видео выше видно, что когда падает новая копейка, то другие копейки начинают широко улыбаться, а когда падает какашка, то копейки в ужасе.
Я долго не мог победить глюк, когда при переключении с одного типа улыбки на другую, смена происходила не мгновенно, а моргала (из одного состояния в другое) какое-то время. Дальше я расскажу, как я это реализовал и как победил этот глюк.
Смена выражений лица реализована с помощью скрипта Sprite Mesh Animation, входящего в состав мощного плагина Anima2D (который Unity недавно выкупила и сделала бесплатным). Этот скрипт по сути просто переключает спрайты для рта (улыбка, открытая улыбка, испуганный рот) с помощью ползунка Frame:
Вся засада в том, что значение ползунка Frame нельзя изменять напрямую из скриптов, а только через систему анимации. Поэтому я создал новый анимационный слой OpenSmile (стрелка 1 на рис. ниже) в режиме смешивания Additive с весом Weight=1 и добавил туда анимацию ужаса (Coin_scared) и широкой улыбки (SmillingWide).
Кстати, вы заметили, какой плохой пример я подаю, с именами анимации? Я всё ещё в процессе приведения имён к единому стилю. Правильно было бы изменить Coin_scared на A_CoinScared (почему именно так читайте в разделе «О чём я жалею»).
Я создал новый слой, а не использовал старый, поскольку не хотел перезаписывать анимацию рта. Мне нужно было только менять спрайт рта (с улыбки на широкую улыбку или с улыбки на ужас) и чтобы при этом анимация рта осталась с базового слоя. Именно поэтому я выбрал режим смешивания Additive — добавление новой анимации к уже существующей (не перезаписывая её).
По своей сути, анимации SmillingWide и Coin_scared — это просто анимация ползунка Frame в позицию 1 и 2, соответственно.
Вся проблема была в том, что переход из любого состояния в состояние ужаса (при клике на переход (стрелка 2 на рис. выше), в инспекторе открываются свойства этого перехода (стрелка 3 на рис. выше)) происходил не моментально, а плавно на протяжении отрезка времени Transition Duration (стрелка 4 на рис. выше), которое было не нулевым по умолчанию. Таким образом, значение ползунка Frame не могло изменяться правильно, ведь там были только целые числа, а значит между 0 и 1 нету промежуточного значения. Поэтому, чтобы избавиться от моргающего глюка, нужно было всего лишь обнулить величину Transition Duration.
Ну а условием перехода в состояние ужаса служит trigger isScared (стрелка 5 на рис. выше). Я активирую этот триггер в коде с помощью следующего обращения к объекту, на котором висит компонент Animator (с контролером, слои которого я показал выше):
...GetComponent<Animator>().SetTrigger("isScared");
Как я переводил приложение на разные языки
Где-то здесь же, на Хабре, я читал, что о локализации нужно задумываться ещё в начале создания приложения и я последовал этому совету… сразу же… спустя полтора года разработки (как только решил, что Math4Ami будет публичным).
Почему я выбрал именно Lean Localization (кроме той причины, что плагин бесплатный) я уже не помню, но помню, что выбирал долго и усердно.
Пользоваться им оказалось очень просто. Можно как вручную задавать язык, так и использовать автоматическое определение языка. Я остановился на автоматическом определении языка (по примеру других детских приложений).
Плагин переводит всё (от текста до звуков и картинок).
Но я всё равно допустил одну ошибку с локализацией (хотя я сделал её намеренно, поскольку хотел попробовать разные подходы). Ошибка в том, что я поместил не все фразы в текстовый файл (слева на рис. ниже). Некоторые фразы остались внутри компоненты Lean Localization (справа на рис. ниже). Поэтому теперь, когда я отдаю этот файл переводчику на японский, мне придётся поработать вручную (чтобы перенести ВСЁ в текстовый файл).
Хотя, некоторые вещи нельзя перевести текстовым файлом (типа пробела " ", который я использовал в качестве разделителя между тысячами) — придётся всё равно использовать компоненту.
Сочный iTween
Когда-то давно я посмотрел шикарнющее видео Juice it or lose it о том, как всякие маленькие микродвижения и нюансы анимации помогают из скучной игры сделать захватывающее дух действие. А ещё до этого мне в душу запало другое видео — The art of screenshake, которое на самом деле не только и не столько о дрожании экрана.
Всё время, пока создавал Math4Ami, я держал в уме концепции из вышеприведённых видео, а также ту мысль, что вся эта дополнительная анимация должна быть максимально короткой и действовать больше на подсознание, чем на сознание. Порой, я тратил больше времени на добавление «сочности», чем на добавление полезного функционала.
Только одно место меня смущает очень сильно — финальный подсчёт заработанных денег (вы можете видеть этот момент в конце моей видеодемонстрации выше). Я его укоротил как только мог, но он всё равно занимает чуть больше 4 секунд (исчезает клавиатура, появляется надпись победа, идёт подсчёт копеек, выезд таблицы рекордов, получение шильдика «Новый рекодр», отображение кнопки «Еще»).
Лучшим «источником сока» для меня является бесплатное дополнение iTween. Даже не представляю, как без него вообще можно что-то делать в Unity. Я его использую везде, где нужна хоть какая-то анимация (будь то анимация кнопки или появление пункта меню или анимация подсчёта копеек).
Я пробовал реализовывать что-то подобное самостоятельно на основе корутинов и Mathf.Lerp или Mathf.MoveTowards, но это было не гибко и не универсально (а порой и работало по разному в редакторе и на устройстве). Поэтому сейчас я не стараюсь изобретать велосипед, а просто наслаждаюсь iTween.
Есть и подводные камни у этой системы анимации, с которыми я неправильно боролся поначалу:
- Если во время работы iTween спрятать объект (через SetActive(false), к примеру), а потом показать его снова, то iTween продолжит исполняться с прерванного места.
- Если во время работы одного iTween запустить другой (который влияет на те же значения), то по окончанию исполнения обоих, объект может не вернуться в исходное положение.
- Нужно следить за тем, какой именно GameObject запускает iTween, а на каком эта анимация работает.
К примеру (по последнему пункту), объект А запускает iTween, чтобы он работал на объекте Б. Чтобы остановить iTween анимацию, нельзя просто запустить iTween.Stop() на объекте А. Нужно запустить iTween.Stop(объект Б).
Сильной стороной iTween является возможность использования разных типов изинга (type of easing). Изинг — это параметр, который смягчает движение (чтобы оно не начиналось рывком и не заканчивалось тупо).
Обалденной находкой для меня стали типы изинга:
- spring
- easeOutBounce
- easeInBack
- easeOutElastic
Чтобы подобрать нужный изинг, я использую наглядную Easing Demo (нужен флеш). А здесь я беру документацию ко всем типам анимации iTween.
Статистика Apple и Google хорошо, но Unity Analytics лучше
Ещё по опыту прошлых игр я знал, что иметь свою собственную статистику — это очень классно. Сначала я хотел создать какую-то свою систему логирования, но потом вспомнил о Unity Analitics. И какое же было моё удивление, когда оказалось, что у бесплатной версии функционал для моего случая ничем не ограничен. Хуже мне пришлось бы, будь у меня какая-то монетизация, тогда инструменты аналитики доступны только для Pro подписчиков.
Просто встраивая Analytics.CustomEvent в нужное место кода, я могу отслеживать какие примеры пользуются большей популярностью, сколько дети решают примеров в первые дни или спустя время и т.д.
Я могу в одном месте сравнивать данные с разных платформ (iOS и Android).
А сколько там всего интересно, что хотелось бы попробовать, да всё руки не доходят. Типа Remote Settings (изменение контента приложения без заливки обновления) или A/B Testing или Tutorial Manager.
Visual Studio наподобие Sublime
В прошлом, когда мне нужно было править какой-то код (будь-то python, html или node.js) я использовал Notepad++ (полностью бесплатный, но только под Windows) и Sublime Text (платный под все ОС, но можно полноценно пробовать бесплатно).
В Unity я сидел на MonoDevelop, но он меня так задолбал своими глюками (типа невозможность переключаться между раскладками или вставить что-то, скопированное за пределами Mono), что я решил — пора бросать тонущий корабль и перелез на Visual Studio Community 2017 (благо, она бесплатная для одиноких, как я, разработчиков).
Для разработчиков на Unity 2018 сейчас это не актуально, так как в составе 2018-ой версии идёт мультиплатформенный Visual Studio Code. Но я хотел, чтобы моё приложение работало под iOS 7 (поскольку iPhone у дочери именно с этой iOS), поэтому нужно было использовать любую версию Unity старее 2018-ой.
Помогло мне с переходом на VS видео How to setup Visual Studio with Unity.
С коробки VS не имеет всех тех классных штук, к которым я привык в других редакторах, поэтому я упростил себе жизнь:
- включил миникарту вместо простой вертикальной прокрутки:
- добавил расширение SemanticColorizer, которое позволяет более гибко настраивать цвета кода. Конкретно мне оно понадобилось, чтобы отличать глобальные переменные от локальных по цвету.
- установил расширение Match Margin, которое выделяет слово под кареткой и все его копии по тексту кода, а также делает это на миникарте. Это очень удобно для быстрой навигации по коду, чтобы найти все места, где используется какой-то метод или переменная:
- использую Strip'em для автоматического исправления line endings.
Мои скрипты для этого приложения на GitHub. Там только мои скрипты, а не весь Unity проект — извините, если из-за этого их невозможно будет понять. Я до последнего момента не планировал давать ссылку на исходники, поскольку не считаю мой код таким, на который стоит ориентироваться. Но потом передумал из-за шанса, что более опытные разработчики могут указать на мои ошибки.
Это конец первой части. Во второй части я расскажу:
- О написании кода
- О контроле версий
- Об озвучке
- Об иконке
- О сборке под Android
- О сборке под iOS
- О названии и продвижении
- Статистика
- О чём жалею
- Что понял
Ссылки
Список ссылок из тела статьи в очерёдности их упоминания:
+ Simple C# FTP Class.
+ Идентификатор сессии для iOS.
+ Пишем плагин для Unity правильно. Часть 1: iOS.
+ Методы для сериализации объектов в JSON (official help).
+ ScriptableObject (official help).
+ Видеоурок Game Architecture with Scriptable Objects (код).
+ Мастер-класс Overthrowing the MonoBehaviour Tyranny in a Glorious Scriptable Object Revolution (код).
+ Видеодемонстрация работы моего приложения в редакторе.
+ Бесплатный плагин Anima2D для скелетной анимации 2D персонажей.
+ Бесплатная библиотека для локализации приложений — Lean Localization.
+ Видео об уловках, улучшающих восприятие игры Juice it or lose it.
+ Видео о действующих на подсознание приёмах анимации The art of screenshake.
+ Бесплатная, но мощная система анимации iTween.
+ Визуальная демонстрация типов изинга (нужен флеш).
+ iTween (official help).
+ Unity Analitics.
+ Текстовые редакторы Notepad++ и Sublime Text.
+ Visual Studio Community 2017 и Visual Studio Code.
+ Видеоурок How to setup Visual Studio with Unity.
+ Плагин SemanticColorizer (для настроек цвета кода).
+ Плагин Match Margin (выделяет слово под кареткой и все его копии).
+ Плагин Strip'em (автоисправление line endings).
Автор: Юрий Лебедев