Save the Penguin — рассказ о первом опыте разработки под Android

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

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

Разработка велась на Unity в свободное время и заняла около месяца. Над выбором движка долго не думали, ибо мы знаем C# и давно хотели познакомится с Unity поближе. В процессе разработки возникали некоторые вопросы, о решении которых я расскажу ниже, надеюсь, кому-то из начинающих разработчиков они будут полезны.

Первый вопрос. Адаптация под разные разрешения

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

Второй вопрос. Оптимизация

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

  • Выбор размера ассета в зависимости от разрешения экрана.
    public class QualityManager : MonoBehaviour
    {
        public string FolderInResources;
        private string _qSuffix;


        void Start()
        {
            _qSuffix = GetQuality();

            ManageQuality();
        }

        private string GetQuality()
        {
            var screenH = Screen.height;

            if (screenH > 2000)
                return "4x";

            if (screenH < 960)
                return "1x";

            return "2x";
        }

        private void ManageQuality()
        {
            if (_qSuffix != "2x")
            {
                var spriteName = GetComponent<SpriteRenderer>().sprite.name.Split('@')[0];

                var sprite = Resources.Load<Sprite>(String.Format("{0}/{1}@{2}", FolderInResources, spriteName, _qSuffix));

                GetComponent<SpriteRenderer>().sprite = sprite;
            }

            Resources.UnloadUnusedAssets();
        }
    }
  • В игре предусмотрена покупка вещей для Пингвина, т.е. смена скина, в первой версии использовался алгоритм, при котором все скины хранились в оперативной памяти вне зависимости от их использования в данный момент. Логика смены скинов была полностью переделана на более правильную, по нашему мнению.
public class ReSkinner : MonoBehaviour
    { 
        public string SkinName;


        void LateUpdate()
        {
            var subSprites = Resources.LoadAll<Sprite>(SkinName);

            foreach (var render in GetComponentsInChildren<SpriteRenderer>())
            {
                var newSprite = Array.Find(subSprites, item => item.name == render.sprite.name);

                if (newSprite)
                {
                    render.sprite = newSprite;
                }
            }
        }
    }

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

Третий вопрос. Сохранение данных

В Unity существует несколько способов сохранения данных. И мы в очередной раз пошли по самому простому пути — хранение данных с помощью встроенного класса PlayerPrefs. В данном случае хранимые данные не должны превышать 1 МБ, в принципе, вполне подходит под наши задачи, но нам нужно хранить информацию о скинах (доступен тот или иной скин пользователю). По большому счету, это булевая переменная, но хранить несколько десятков таких переменных достаточно затратно. Решение очевидно – использование битовых масок.

static class FlagCollection
    {
        public static int SetFlag(this int collection, int numberFlag, bool valueFlag)
        {
            if (numberFlag < 0 || numberFlag >= 32)
            {
                throw new ArgumentException("Number of flag isn't correct");
            }

            if (valueFlag)
            {
                return collection | (1 << numberFlag);
            }
            else
            {
                return collection & ~(1 << numberFlag);
            }
        }

        public static bool GetFlag(this int collection, int numberFlag)
        {
            if (numberFlag < 0 || numberFlag >= 32)
            {
                throw new ArgumentException("Number of flag isn't correct");
            }

            return (collection & (1 << numberFlag)) != 0;
        }
    }

Вывод: eсли вы сомневаетесь, разрабатывать своё приложение или нет — определённо да! Это бесценный опыт, который вам точно не окажется лишним. Главное — помнить об одном: “Успех складывается из мелочей”; если вы взялись за что-то, сделайте это качественно, в наше время плохо сделанный продукт заведомо обречён на провал.

4

Ссылка на игру: https://play.google.com/store/apps/details?id=com.pacetap.savethepenguin

P.S. Будем очень признательны за вашу критику и фидбэк.