Не Unity єдиним, або Як ми в Playrix розробляли свій движок

За часів великої кількості і доступності якісних ігрових движків начебто Unity і Unreal необхідність у розробці власних виникає рідко. Про один із винятків хочу розповісти в цій статті. Мова піде про ігровому движку компанії Playrix, в якій я працюю майже 5 років. Розповім про його минуле та сьогодення, поточної функціональності, про те, яким технічним рішенням ми прийшли і чому не стали використовувати Unity.

В першу чергу ця стаття може бути цікава молодим студіям, які ще не вирішили, яким шляхом іти: створювати свій движок або вибрати існуюче рішення. А може, стаття підкине нові ідеї, якщо ви вже використовуєте рушій власного виробництва.

Для початку давайте познайомимося. Мене звати Віталій, я програміст візуальних ефектів в Playrix. В компанію прийшов майже п'ять років тому, в кінці 2015 року. Починав з портування проекту Gardenscapes на Mac-платформу, а потім перейшов на проект Fishdom, де цілком поринув у роботу з графікою, анімацією і програмними ефектами.

Прийшовши в компанію, виявив, що на всіх проектах використовується єдиний ігровий движок власного виробництва. Він має довгу історію і як окремий проект розпочав своє існування в 2009 році з загальної частини гри Brickshooter Egypt . На момент написання статті движку вже 11 років! Можна сказати, він найстаріший з працюючих проектів Playrix.

Отже, трохи про функціонал: спочатку движок підтримував тільки платформу Windows, а через кілька років з'явилася підтримка мобільних платформ. Він покривав весь базовий набір потреб і був добре заточений під наші проекти. Що ми мали тоді:

Для комфортної роботи не вистачало тільки зручних редакторів і систем для організації ігрових об'єктів. У старі часи у нас різні проекти працювали з візуалом по-різному. Наприклад, Gardenscapes застосовував систему вікон, зроблену на Flash, що дозволяло дизайнеру налаштовувати верстку та анімації. На Fishdom все було трохи складніше: побудова багатьох графічних об'єктів велася безпосередньо з коду, що давало свої плюси при створенні візуальних ефектів. Але було ясно одне: нам потрібна єдина зручна система по роботі з контентом ігор.

Кілька разів піднімалося питання використання Unity, але для цього знадобилося б перевести вже існуючі проекти з C++ на Unity. Цей процес вимагає багато часу та ресурсів. Уникнути його не представлялося можливим, так як ми часто використовуємо напрацювання одних проектів на інших. Різні технології сильно ускладнили процес.

В цій ситуації логічним рішенням стало розвиток власного інструменту — Visual Scene Object (VSO). У першій версії VSO ще не було ні кодогенерации, ні різноманітності редакторів. Він був написаний за два місяці в зв'язці з ігровим проектом і став основою подальшого розвитку. Спираючись на чітке розуміння потенціалу і затребуваності продукту, команда з 4 осіб активно працювала над проектом, нехай це і не завжди збігалося з основним вектором розробки движка.

Далі йшла розробка еволюційно і поступово, з урахуванням інтересів і побажань проектів. Зараз в команді VSO більше 10 осіб, а часу на вихід нових версій йде набагато більше, ніж раніше. Крім основної команди, у розвитку VSO беруть участь програмісти інших проектів. Коли Fishdom почав свій перехід на VSO, я швидко включився в процес написання нових behaviour-сценаріїв для спрощення процесу розробки. Частина цих напрацювань перекочувала в загальну VSO-бібліотеку.

Що ми маємо зараз

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

Ми використовуємо набори об'єктів, зібраних в сцени. Вони складаються з вузлів, які, в свою чергу, складаються з низки сутностей:

Програмісти можуть робити свої власні component, behaviour, sorting. Більш універсальні створює команда VSO, а вузькоспрямовані — програмісти всередині ігрових проектів. Деколи функціонал з проекту переходить всередину загального функціоналу VSO і стає частиною бібліотеки. Для написання своєї версії класу потрібно створити спадкоємця від потрібної базової реалізації і перевизначити події (наприклад, OnInit, OnClone). Замість використання макросів, як це зроблено в UnrealEngine, ми використовуємо теги в коментарях.

Приклад коду компонентидоступний тут .

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

Генерацію і серіалізацію редакторів використовують не тільки для візуальних об'єктів, але і для будь-яких інших класів, успадкованих від Serializable. Потрібно лише помітити властивості тегами. Ну а якщо наслідувати свій клас від класу Asset, отримаємо повноцінний ассет, схожий по функціоналу на ScriptableObject у Unity. Така модель бібліотеки прискорює розробку нової функціональності і делегує частину завдань профільним фахівцям.

Основні блоки

Кодогенерация

Так як в мові C++ немає рефлексії (reflection — отримання даних про тип коду), значну частину коду, що забезпечує роботу системи, доводиться писати руками. Але ми змогли домогтися генерації більшої частини цього рутинного коду.

Наш генератор — це питоновский скрипт, який парсити відмінності файли і на їх основі створює потрібний код. За рахунок тегів настроювання генерації вийшла гнучкою. Для деяких з наших систем код генерується цілком, наприклад:

Приклад згенерованого коду класу доступний тут .

Парсинг C++

Одним з рішень для розбору заголовних файлів міг бути парсинг з Clang, але його швидкості не вистачало. Тому зупинилися на CppHeaderParser — це проста Python-бібліотека, що складається всього з одного файлу. Вона працює швидко, але має ряд обмежень, наприклад, не ходить #include та не обробляє макроси або символи. Тому ми її серйозно допрацювали, серед іншого додали нововведення з C++17, і використовуємо досі.

Щоб уникнути невизначеностей статусу при генерації коду, ми вирішили робити бібліотеку повністю автоматичною. Для цього обрали CMake. Проводимо генерацію при кожній компіляції. Для економії часу зберігаємо кеш з результатами парсинга, і холостий прохід кодогенерации займає всього кілька секунд.

Генератор коду

Вибір бібліотеки для генерації за шаблонами виявився досить простим. Ми зупинилися на Templite+ . Вона не велика, але володіє всім необхідним функціоналом.

До поточної версії генерації прийшли не відразу. Спочатку основну логіку писали на Python, використовували шаблони по мінімуму, а код містив безліч умов і перевірок. У такому підході були свої плюси: краща читабельність, ніж у шаблонах, можливість реалізувати хитру логіку. Але при цьому Python-код сусідив з великою кількість коду на С++, і така мішанина швидко стомлювала. Генератори на Python частково вирішували проблему, але не цілком.

За підсумком все-таки перейшли на генерацію з допомогою шаблонів, і Python тепер тільки готує дані.

Серіалізація

Ми розглядали цілий ряд бібліотек для серіалізації, серед яких були FlatBuffers, cereal, Protobuf та інші.

Бібліотеки з кодогенерацией, як FlatBuffers і Protobuf, мають на увазі рукописні структури, а згенеровані структури неможливо інтегрувати в користувальницький код. Результатом використання цих бібліотек було б збільшення в два рази класів виключно для серіалізації.

У цій ситуації cereal виглядала кращим кандидатом. У цієї бібліотеки приємний синтаксис, зрозуміла реалізація, в ній досить комфортна генерація коду для серіалізації. Єдиний великий мінус — її бінарний формат. Основною вимогою з нашого боку була незалежність формату від «заліза» (порядок байтів або розрядність не повинні впливати на читання даних). А також зручність запису бінарного формату з Python. За цими критеріями cereal нам також не підходив.

Взявши за основу ідею cereal, ми створили бібліотеку, в якій знаходяться базові архіви читання і запису даних. Спадкоємці від цих архівів реалізують запис в потрібному форматі json, xml. Конвертація з xml в бінарний формат виконується простим Python-скриптом. Код серіалізації надалі генерується за класами і користується цими архівами при запису даних.

Редактори

Для написання вікон редакторів ми вибрали бібліотеку ImGui. На ній створені вікно вмісту сцени, інспектор ассетов, редактори анімацій та ефектів та інше. Велика частина коду для редакторів пишеться вручну, але для роботи з властивостями класів, їх перегляду або редагування використовується бібліотека RTTR, а також биндинг, згенерований під неї, і узагальнений код інспекторів.

Бібліотека рефлексії — RTTR

Для організації рефлексії в С++ ми вибрали бібліотеку RTTR. Вона має простий API, і їй не потрібно втручатися в класи. RTTR підтримує різні обгортки над типами (наприклад, розумні покажчики) і колекції, а також дозволяє реєструвати свої власні обгортки і володіє необхідною функціональністю.

Головна проблема цієї бібліотеки — її громіздкість та повільність. Тому застосовуємо її тільки для редакторів. Для ігрових об'єктів зробили свою простеньку бібліотеку.

В бібліотеці RTTR вимагається написання биндинга з оголошенням всіх методів і властивостей класу. Цей биндинг генерується з коду на Python для тих класів, яким потрібна можливість редагування. А за рахунок того, що RTTR підтримує метадані для всіх сутностей, генератор створює різні налаштування членам класу: специнспектор для поля, межі числових полів, зринаючі підказки.. В інспекторі ці метадані застосовуються для відображення інтерфейсу редагування.

Приклад оголошення класу в RTTR дивіться тут .

Інспектор

Зазвичай код всередині редактора не взаємодіє безпосередньо з RTTR. Для цього використовують рукописні класи-прошарки, які вміють малювати ImGui-інспектор для об'єкта.

Метадані, зазначені в RTTR при реєстрації, потрібні, щоб адаптувати для виведення редаговані дані об'єкта. Ми можемо створювати об'єкти, які зберігаються за значенням або за вказівником, а також маємо підтримку примітивних типів і колекцій.

В інспекторі ми заклали можливість скасувати операції. Для цього використовуємо команди, які створюються при будь-якій зміні даних і в яких закладений функціонал повернення початкових даних. Це дало Ctrl+Z.

Вікна і редактори

На основі кодогенерации, функціоналу редакторів і систем створення ассетов було створено велику кількість підсистем і корисних ігрових редакторів:

Редактор сцени — це гнучка система інтерфейсів, яка дозволяє наповнити сцену об'єктами, властивостями, налаштувати верстку та зовнішній вигляд.

Редактор подій і квестів — призначений для створення різних туториалов, квестів та інших подій. Може використовуватися практично без участі програмістів.

Редактор станів

Редактор анімаційних станів — це набір станів об'єкта, керованих за допомогою анімації і переходів між ними. Редактор дозволяє також настроювати зв'язку між цими станами, їх тривалість і тонкощі переходу. Отримавши команду на зміну стану, контролер автоматично знайде до нього шлях і програє всі анімації в проміжних блоках.

Редактор анімацій

Редактор анімацій — основний інструмент для створення анімацій в VSO. Крім управління трансформацією і кольором об'єктів, завдяки редактору можна прив'язати анімацію практично до будь-якого з властивостей або керувати їхньою поведінкою. Наприклад, регулювати швидкість програвання flash-кліпу або запустити ефект в потрібний момент.

Редактор ефектів

Редактор ефектів дозволяє створювати і редагувати ефекти частинок і є основним інструментом роботи VFX-художників в компанії.

У нашій реалізації ефект — це набір кількох однотипних систем частинок, які змінюються у часі за однаковими законами. У системи частинок є як постійні характеристики — текстура частинок, параметри емітера, граничне число частинок, так і змінні — положення (або швидкість), розмір, кут повороту, колір. З-за того, що деякі параметри системи частинок при старті ефекту вибираються випадково, характеристики кожної окремої частинки можуть відрізнятися від характеристик іншої частинки. Це створює ілюзію, що частинки живуть самі по собі.

З допомогою ефектів легко оживити статичні сцену, додати краси і розставити акценти.

Редактор ICS

Interactive Cutscenes (ICS) призначений для створення сценаріїв, за якими на сцені будуть відбуватися різні події в залежності від заданих умов. Основна ідея полягає не в тому, щоб перейти з одного стану в інший, як це відбувається в редакторі станів, а щоб програти сцену (набір дій). ICS дозволяє вручну налаштувати маршрут, додати розвилки з залежністю від умов, очікувати обробки кліків і багато іншого. Програмісти всередині проектів можуть створювати особливі блоки дій, розширюючи функціонал редактора.

Підсумки

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

Таким чином, головне надбання — це свобода дій. Оскільки двигун створювався і розвивався, орієнтуючись на потреби проектів, в ньому не висвітлена функціональність, яка не використовується в іграх Playrix. Наприклад, у нашому движку немає підтримки великих 3D-сцен. Ми не використовуємо статичні і динамічні тіні, пряме й відстрочене освітлення — це великий пласт робіт, на який ми зараз не готові. Однак якщо будемо розробляти гру, яка вимагає використання, наприклад, ландшафту або освітлення, то обов'язково повернемося до Unity, Unreal Engine або іншого популярного движка.

Свій движок — це гнучкість і свобода, але і великі витрати на розробку та підтримку. Вибір готового движка — швидке впровадження, можливість знайти фахівця з досвідом взаємодії, але і необхідність підлаштовуватися і йти на компроміси. Рішення залишається за вами.

Опубліковано: 06/08/20 @ 10:00
Розділ Різне

Рекомендуємо:

Вигідне географічне розташування та розвинена ІТ-сфера: Дніпро очима упередженого місцевого програміста
Чому SOLID — важлива складова мислення програміста. Розбираємося на прикладах з кодом
Кар'єра в IT: NLP Engineer і NLP Researcher
Як за допомогою тестів пришвидшити реліз
Що потрібно знати тестировщику про рецензування та як його використовувати в роботі