Как создать свою первую игру и выжить. Часть вторая: как не забросить

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Всем привет! Я Виктор Антоненко, Lead Unity-разработчик в компании OBRIO. Мы ― часть экосистемы бизнесов Genesis и занимаемся разработкой мобильных приложений и игр. Кто со мной еще не знаком, может узнать подробнее в первой части этого цикла. Там я пошагово рассказываю, как запустить свой первый гейм-проект.

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

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

Основные термины (расширение)

Фича (Feature) ― это дополнительная возможность, функционал, игровая механика или её часть.

Гейм-джем (Games Jam) ― хакатон для разработчиков игр. Мероприятие, где за ограниченное время команда должна сделать и представить игру или её прототип.

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

Навмеш (NavMesh) ― простыми словами, это механизм постройки карты проходимости для поиска пути агентами этого навмеша.

Навмеш агент (NavMesh Agent) ― это объект, имеющий ряд настроек (ширина, высота прыжка), для которого строится навмеш. Например, слон и человек ― разные навмеш агенты, поэтому карты их проходимости отличаются.

Майлстоун (Milestone) ― метафора, обозначающая промежуточный этап разработки проекта.

Поэтапная организация разработки

1. Планирование. На этом этапе нужно составить и описать задачи, а затем распределить их между членами команды. Дальше идет обсуждение и, если нужно, разбивка на более мелкие задачи. Рассмотрим пример. Вы хотите разработать фичу передвижения персонажа на поверхности, куда он может пройти по клику. Для этого стоит разбить функцию на подзадачи: обработка клика, механизм поиска пути, непосредственно передвижение персонажа в указанную точку, его анимация, пока он двигается и стоит.

2. Проектирование. Перед разработкой определитесь, как вы будете решать поставленную задачу, какие инструменты и методы будете использовать. Если логика задачи достаточно сложная, можно подготовить схему работы. Мой способ проектирования сложных задач ― это детальное обсуждение фичи с коллегой-разработчиком. В процессе объяснения я сам лучше понимаю способ решения, также мы рисуем схемы на лету и находим возможные варианты воплощения фичи. Продолжим пример с функцией передвижения персонажа. На этом этапе вы выбираете, какой механизм поиска пути использовать ― что-то готовое или написать свой алгоритм ― и устраняете все пробелы в знаниях для решения задачи.

3. Разработка. Переходим к реализации описанного функционала: пишем код, работаем с движком или файлами. Для передвижения персонажа это будет разработка системы, которая обрабатывает ввод для получения данных клика, передача их в систему, поиск пути персонажем, постройка маршрута, скрипт самого передвижения, изменение анимации.

4. Тестирование и отладка следуют за разработкой. На этом этапе идет проверка соответствия поставленной задачи с фактической работой приложения. Обязательно проводите отладку и тестирование. Без них вы никогда не будете уверены, что закончили задачу в полной мере. Ещё стоит помнить, что в практически любом случае разработчик плохо тестирует свою работу. А вот тестировщик сможет, например, найти, что в определенных участках маршрута персонаж попадает на те возвышенности, на которые заходить не должен. Или вычислить, что если персонажей становится несколько, то в конечной точке маршрута они устраивают «толкучку», которую нужно исправлять.

Техническая часть создания игры

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

Не стоит пытаться разработать абсолютно всё самостоятельно. Большинство проблем, с которыми вы столкнетесь, уже кто-то решал: реализовывал фичи и наступал на грабли, на которые вы еще не наступали. Поэтому в начале воплощения каждой новой игровой функции стоит поискать готовые наработки. Возможно, вы удивитесь, сколько всего есть в свободном доступе.

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

Первый образец касается механизма поиска пути на основе навмеша. Вот полезный туториал, который поможет воплотить такой функционал. А это моя реализация обертки навмеша:

    public class NavMeshAgentWrapper : MonoBehaviour
    {
        private NavMeshAgent unitAgent;
        private bool isMoving;
        private bool isAttacking;
        private Action callback;
        private Transform target;

        private Vector3 prevPoint;
        private Vector3 unitDirection;
        private float currentSpeed;
        private float prevSpeed;
        private float speedDelta;

        private void Awake()
        {
            unitAgent = gameObject.GetComponent<NavMeshAgent>();
            isMoving = false;
            isAttacking = false;
        }

        public void MoveToPosition(Vector3 position, Action callback)
        {
            unitAgent.isStopped = false;
            unitAgent.SetDestination(position);

            this.callback = callback;
            isMoving = true;
        }

        public void AttackMove(Transform target, Action callback = null)
        {
            unitAgent.isStopped = false;
            this.target = target;
            this.callback = callback;

            isAttacking = true;
        }

        public void Stop()
        {
            unitAgent.destination = transform.position; // TODO maybe delete this
            unitAgent.isStopped = true;
            isMoving = false;
            isAttacking = false;

        }

        private void Update()
        {
            if(isAttacking)
            {
                unitAgent.destination = target.position;
            }

            if (isMoving || isAttacking)
            {
                if(unitAgent.remainingDistance <= unitAgent.stoppingDistance && unitAgent.pathPending == false)
                {
                    DispatchUtil.DispatchIfNotNull(callback);
                    Stop();
                }
            }
        }

        private void LateUpdate()
        {
            unitDirection = prevPoint - transform.position;
            currentSpeed = Vector3.Distance(prevPoint, transform.position) / Time.deltaTime;
            prevPoint = transform.position;

            speedDelta = Mathf.Abs(prevSpeed - currentSpeed);
            prevSpeed = currentSpeed;
        }

        public Vector3 GetPredictedPosition(float time)
        {
            Vector3 resultPoint = new Vector3();

            if (speedDelta < 0.05f)
            {
                if(currentSpeed > 0.05f)
                {
                    resultPoint = transform.position + unitDirection * (currentSpeed * time);
                }
                else
                {
                    resultPoint = transform.position;
                }
            }
            else
            {
                resultPoint = transform.position + (unitDirection * ((speedDelta * time) + (currentSpeed * time)));
            }

            return resultPoint;
        }
    }

Следующий пример связан с интеграцией сервисов аналитики.

    [RequireComponent(typeof(SegmentsSubsystem))]
    public class AnalyticsModule : MonoBehaviour, IInitable
    {
#pragma warning disable 0649
        [SerializeField]
        private DevToDevWrapper devToDevService;
        [SerializeField]
        private List<Trackers.BaseTracker> trackers;
#pragma warning restore 0649

        private SegmentsSubsystem segmentsSubsystem;
        private bool isInited;

        public bool IsInited => isInited;
        public SegmentsSubsystem SegmentsSubsystem => segmentsSubsystem;

        public event Action<int> DaysPlayedChangedEvent = (days) => { };

        public void Init()
        {
            if(segmentsSubsystem == null)
            {
                segmentsSubsystem = gameObject.GetComponent<SegmentsSubsystem>();
                App.InitializationModule.AddOnDemandInitable(segmentsSubsystem);
            }
            foreach(var tracker in trackers)
            {
                tracker.Init();
            }
            isInited = true;
        }

        public void InitDevToDevService(string userId, int userLevel, string appVersion)
        {
            devToDevService.InitService(userId, userLevel, appVersion);
        }

        public void Reinit()
        {
            isInited = false;
            Init();
        }

        public void PlayerProgressChangedHandler(int locationNumber)
        {
            CreateConditionAndCheckSegments(ConditionEnum.LocationNumber, locationNumber.ToString());
        }

        public void MoneyPaidHandler(int moneyPaid)
        {
            CreateConditionAndCheckSegments(ConditionEnum.MoneyPaid, moneyPaid.ToString());
        }

        public int PlayedDaysHandler(int daysPlayed, DateTime nowTime, DateTime lastPlayTime)
        {
            int result = daysPlayed;
            if (nowTime.Day > lastPlayTime.Day)
            {
                int newDaysPlayed = daysPlayed + 1;
                result = newDaysPlayed;
                CreateConditionAndCheckSegments( ConditionEnum.DaysPlayed, newDaysPlayed.ToString());
            }

            return result;
        }

        public void AddInitialSegments()
        {
            CreateConditionAndCheckSegments(ConditionEnum.DaysPlayed, "0");
            CreateConditionAndCheckSegments(ConditionEnum.LocationNumber, "0");
            CreateConditionAndCheckSegments(ConditionEnum.MoneyPaid, "0");
        }

        private void CreateConditionAndCheckSegments(ConditionEnum type, string conditionValue)
        {
            List<ConditionsData> conditionValues = new List<ConditionsData>();
            ConditionsData condition = new ConditionsData();
            condition.Condition = type;
            condition.ConditionStringValue = conditionValue;
            conditionValues.Add(condition);
            segmentsSubsystem.CheckSegmentRequirements(type, conditionValues);
        }

        public void LogPurchase(string transactionID, string productID, float price, StoreType storeType)
        {
#if UNITY_IOS || UNITY_ANDROID
            devToDevService.LogPurchase(transactionID, productID, price);

            var dictionaryValues = new Dictionary<string, string>
            {
                {AFInAppEvents.CONTENT_ID, productID},
                {AFInAppEvents.PRICE, price.ToString()},
                {AFInAppEvents.QUANTITY, "1"},
                {AFInAppEvents.CURRENCY, "USD"},
                {AFInAppEvents.REVENUE, price.ToString("F")}
            };
            AppsFlyer.sendEvent(AFInAppEvents.PURCHASE, dictionaryValues);
#endif
        }

        public void LogEvent(Enumerations.AnalyticsEventName eventName, List<KeyValuePair<string, string>> eventValues = null)
        {
            //DevToDev
            devToDevService.LogEvent(eventName.ToString(), eventValues);
            
            //AppsFlyer
            var dict = new Dictionary<string, string>();
            if (eventValues != null)
            {
                foreach (var kvPair in eventValues)
                {
                    dict.Add(kvPair.Key, kvPair.Value);
                }
            }

            AppsFlyer.sendEvent(eventName.ToString(), dict);

        }

        public void LogEvent(Enumerations.AnalyticsEventName eventName, string key, string value)
        {
            devToDevService.LogEvent(eventName.ToString(), key, value);
        }
    }

Как видно из кода, для аналитики мы используем сервисы DevToDev и Appsflyer. Написанный нами модуль аналитики универсален для внешних вызовов и реализует вызовы оберток сервисов «под капотом». Вся не универсальная часть реализуется в обертке DevToDev. Модуль аналитики содержит подсистему, отвечающую за присвоение и актуализацию сегментов пользователя. Модуль наследует интерфейс IInitable, в нашей архитектуре это подразумевает, что он должен быть подписан на инициализацию управляющим скриптом.

Главные принципы организации командной работы

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

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

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

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

Контролируйте процесс. Принцип «все ответственные и сами знают, что делать» не работает. Когда команду не контролируют, начинается разнобой, как в басне «Лебедь, щука и рак». В моей практике это приводило к тому, что некоторые разработчики не знали, что именно им делать. Даже несмотря на то, что проект имел свой таск-менеджер и в задачах были описания, запланированную работу не всегда выполняли вовремя.

Планируйте и фиксируйте результаты — это вас ускорит и позволит организовать процесс. Настоятельно рекомендую использовать таск-менеджеры — так каждый сможет увидеть прогресс коллег и всего проекта.

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

Работайте в одном пространстве. Идея встретиться и поработать вместе всегда приносит отличные результаты. Это не просто решение ваших задач, но ещё и нетворкинг, который позволит коммуницировать быстрее, находить неожиданные решения и перенимать опыт коллег. Если нет возможности встретиться, можно устроить созвон-коворкинг.

Не удерживайте слабых игроков. Зачастую команда первого проекта состоит из друзей и энтузиастов с разным опытом и возможностями. В таком случае самый медленный член группы всегда будет тормозить остальных. Но не нужно спешить исключать человека из команды, чтобы не деморализовать остальных. Сперва стоит предложить доучиться, забрав у такого игрока критические задачи. Если же человек не растет и не обучается, это говорит о его низкой мотивации. Может, на самом деле эта деятельность ему не нравится, и он участвует, чтобы не выпасть из социальной группы. Стоит обсудить, как выйти из ситуации, один на один. Может, будет достаточно просто сменить его род деятельности в проекте.

На одном из первых проектов я работал со своим другом — опытным .Net разработчиком. Мы использовали Unity как игровой движок, но у друга были предрассудки по поводу работы с ним. Поэтому я передал ему задачи общего характера, минимально связанные с инструментами движка — например, систему локализации. Но из-за незаинтересованности развиваться как Unity-разработчик, он не занимался самообучением и не искал решения сам. Зато я тратил массу времени на объяснения и попытки научить его, как решать подобные задачи.

Будьте гибкими. Гибкость — ваш самый сильный навык! Это касается каждого элемента и процесса внутри проекта. Использование новых для вас программ может помочь организовать коммуникацию и процесс работы, повысив эффективность. Когда мы делали свою первую игру, то долго не хотели использовать таск-менеджеры и переходить из чата в соцсети в Slack. Это привело к огромным проблемам в организации работы.

Будьте открытыми. Каждый член команды должен иметь доступ к информации о текущем состоянии проекта, его курсе, задачах, которые будут разрабатываться следующими. Когда я к этому пришел, то стал записывать технические демонстрации механик в короткие ролики и выкладывать в мессенджеры для просмотра. Это позволяло сразу получать фидбек и мотивировать остальных.

Мотивация: что нужно, чтобы продолжить

Чтобы проект был успешным (а успех в этом случае значит достижение ваших целей), не обязательно стать игрой № 1. Иногда это просто опыт. Всегда помните, что именно мотивация и запал держат проект на плаву и позволяют ему развиваться. Каждая строчка кода, изображение, 3D-модель и схема продвигают вас все ближе и ближе к цели.

Вот основные принципы поддержания мотивации.

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

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

Не останавливайтесь на одном месте слишком долго. Если вы встретили сложность и не можете ее преодолеть — ищите обходной путь, напишите вопрос на форуме, попробуйте найти ментора, который мог бы помочь решить проблему. А пока ищете — отложите на время проблемный участок работы и продолжите работу над другим. Спустя некоторое время вы вернетесь к вопросу и сможете посмотреть на него с нового ракурса.

Не распыляйтесь. Часто возникают возможности, которые кажутся заманчивыми. Но если вы переключитесь на них, это не только замедлит ваш проект, но ещё и повлияет на вашу мотивацию продолжать его — гораздо тяжелее начинать заново, чем просто поддерживать процесс. Когда мы делали игру, нам подвернулась возможность поучаствовать в гейм-джеме: сплотиться, научится работать в критических условиях. Участие в мероприятии действительно дало такой опыт. Но мы приняли неверное решение — закончить игру для конкурса, потому что она была почти готова, вместо того, чтобы доработать и выпустить собственный проект. Это стало одной из основных причин, почему наша игра так и не увидела свет. Команда разработчиков переключила усилия, и мы потеряли запал.

Регулярно получайте фидбек — продемонстрируйте ваши текущие успехи членам команды, родственникам, друзьям. Не бойтесь критики — она сделает лучше и вас, и то, что вы делаете. Но и не слишком расслабляйтесь, когда вас хвалят. Качественная обратная связь поможет увидеть точки роста и увеличит ценность собственной работы в ваших же глазах.

Используйте майлстоуны — банальное перетаскивание карточек по доске может очень сильно повысить мотивацию. Организуйте задачи таким образом, чтобы весь проект был разбит на этапы и в конце каждого из них вы могли что-то «потрогать», продемонстрировать и оценить.

***

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

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

Очень интересно почитать в комментариях, с какими проблемами сталкивались вы и ваша команда, какие вещи позволяли вам поддерживать мотивацию и не терять запал :)

Полезные ресурсы

  1. Видео на YouTube. Есть разные туториалы в зависимости от ваших текущих необходимостей.
  2. Stack overflow — куда же без него.
  3. Trello как пример таск-менеджера.
  4. Телеграм-канал «Геймдев, который мы заслужили». Там выкладывают полезную информацию о состоянии индустрии и технологиях.
  5. Принимайте активное участие в коммьюнити, тематических чатах, форумах. Это поможет найти ментора, интересные контакты или даже участников команды своей мечты.

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

👍НравитсяПонравилось4
В избранноеВ избранном3
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Как создать свою первую игру и выжить. Часть вторая: как не забросить

Трон 1 и Трон 2

Stack overflow — куда же без него.

Дає лінк на розділ з рфіянцями
facepalm

Спасибо за фидбек, поправил

Давным давно во времена флеш игор, удалось доработать и зарелизить флеш игру, которую изначально делали для конкурса. Все как полагается, 2 программиста, художник, звуковик. Даже на flash gamm ездили, получили награду «будущий хит»)) При этом работая фултайм на основной работе.
А вот вторую часть не смогли доделать. Конфликты в команде(каждый хотел быть геймдизайнером), слишком амбициозные планы итд.
В целом — рад, что не стал инди разработчиком. Могу позволить себе чуть больше, чем пара дошиков :)

Сокращу до плана:
1) ###к!
2) ###к!
3) И в продакшен!

Не распыляйтесь.

Подписаться на комментарии