Как это сделано: мобильный кроссплатформенный движок +22


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


Содержание


Часть 1. Мобильный кроссплатформенный движок
Часть 2. Рендеринг UTF-8 текста с помощью SDF шрифта
Часть 3. Рендеринг капли с прозрачностью и отражениями


А нужен ли свой движок вообще?


Каждый раз, когда очередной популярный движок становится бесплатным или открытым, я задаю себе этот вопрос. Давайте рассмотрим плюсы и минусы:

Плюсы

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

Минусы

  • Все придётся писать самому, а так же вникать во все нюансы каждой платформы. Этот процесс займёт довольно много времени, поэтому просто так «ради интереса» этим заниматься не стоит. Другое дело, если вы занимаетесь геймдевом профессионально и планируете выпускать довольно много игр. Практика показывает, что большинство игровых студий рано или поздно создают свои SDK.
  • Если вы делаете проекты на заказ, то не все заказчики рады вашим самописным решениям. Ведь возможно поддерживать проект придется совершенно другим людям.
  • Во многих популярных движках есть встроенные визуальные 2D/3D редакторы. Об их удобстве можно долго спорить, но у вас изначально не будет и этого.

Конечно каждый для себя увидит свои плюсы и минусы. Мое дело предупредить. Поехали!

Из чего это сделано?


Мы говорим в первую очередь о разработке мобильных игр, поэтому основа будет однозначно на C++/OpenGL. Без вариантов! Однако без второстепенных языков тоже не обойтись. Давайте посмотрим что используется на каждой платформе:
Платформа Основа Обертка Графика
iOS C++ ObjectiveC или Swift OpenGL
Android C++ (NDK) Java OpenGL
WindowsPhone C++ C# OpenGL через врапер или DirectX
tvOS (AppleTV) C++ ObjectiveC или Swift OpenGL
OSX C++ ObjectiveC или Swift OpenGL
Linux C++ C++ OpenGL
Как видите C++ и OpenGL встречаются везде. На ObjectiveC/Java/C# придется написать только обертку для работы с системой девайса. Сам же код ваших проектов будет единый — на С++. На этой ноте скажем: «До свидания, мучительное портирование!».

OpenGL


Настоятельно рекомендую использовать OpenGL 2.0 и выше. Время OpenGL 1.1 давно прошло, а переход с 1.х на 2.х вы будете вспоминать в кошмарных снах. Однако не спешите использовать последнюю версию OpenGL не убедившись, что все целевые платформы его поддерживают. В большинстве случаев OpenGL 2.0 вполне хватает и поддерживают его все платформы.

С++


Та же ситуация и с С++11/14. Если уверены, что все компиляторы с ним дружат – супер. Мне же хватает C++98, так что при добавлении новой платформы — а в планах есть поддержка консолей — я буду спокоен.

IDE


Xcode – для iOS, OSX, tvOS. Плагины через CocoaPods.
Android Studio – для Android. Плагины через Gradle.
Visual Studio – все что под Windows.

Структура движка


Прежде всего движок и проекты должны аккуратно и логично храниться на диске. В итоге я пришел к такой структуре:

  • Engine (все что касается движка)
    • Classes (.h, .cpp файлы движка)
    • Modules (модули и сторонние SDK, которые нужны не во всех проектах)
      • Рекламные SDK
      • Аналитика
      • Game Center
      • Изображения
      • Социальные сетки
      • … и т.д.
    • Platforms (специфические классы по платформам)
      • Android
      • iOS
      • OSX
      • tvOS
      • … прочие платформы

  • Тестовый проект
    • iOS
      • Проект.xcworkspace
      • Icons (иконки приложения, *auto — сборщик проекта сам заполняет эти папки)
      • Launch (картинки при старте приложения, *auto)
      • Res (готовые ресурсы приложения, *auto)
      • Pods
      • … прочие файлы ios проекта, plist, build и т.д.
    • Android, OSX, tvOS… такие же по смыслу папки под разные платформы и IDE. Для Android Studio своя структура проекта.
    • Assets
      • Icons (иконки приложения всех размеров)
      • Launch (ланч-скрины всех размеров)
    • Resources (оригиналы ресурсов проекта)
      • General (основные ресурсы для всех платформ)
      • Lang (шрифты и локализация)
        • Fonts (папка с SDF шрифтами)
        • Lang.xls (файл с переводами)
      • Platform (ресурсы специфические для платформы)
        • iOS
        • Android
        • … прочие платформы
      • Shaders (шейдеры)
      • Sounds (звуки)
        • MP3 (для треков)
        • OGG (для звуков)
      • Textures (ресурсы по форматам текстур)
        • ATI
        • ETC
        • PVRTC
        • S3TC
    • Source (.h, .cpp файлы самого проекта)
    • Config (файл настроек проекта для сборщика)

Сборщик проекта


Сборщик проекта отвечает за подготовку ресурсов, форматы и упаковку. А именно:
  • Берет иконки (или даже одну иконку максимального размера 1024х1024) из Assets/Icons, делает остальные размеры (от 16х16 до 1024х1024) и копирует в папки по платформам [Platform]/Icons
  • Так же поступает с экранами старта из Assets/Launch
  • Берет ресурсы приложения из следующих папок:
    • Resources/General
    • Resources/Shaders
    • Resources/Sounds/MP3, OGG
    • Resources/Textures/[нужный формат текстур]
    • Resources/Platform/[платформа]
    • Resources/Lang/Fonts
Далее сборщик конвертирует ресурсы, шифрует, упаковывает и помещает в [Platform]/Res.

Самое важное тут – это конвертация файлов по расширению. Я использую такие конвертации:
  • PNG и JPEG превращаются в WEBP. Общие настройки конвертации (например минимальное качество) можно вынести в настроки проекта Project/Config, а можно и указать прямо в названии файла.
    Например image~q100.png будет сжата с параметром quality 100, а image~less.png будет сжата без потери качества.
    Так же хорошо себя зарекомендовали пресеты.
    Например к image~p1.png будет применен 1й пресет, который перевернет картинку зеркально и сохранит с качеством 90%.
  • Текстовые файлы и шейдеры (.txt, .vs, .ps) шифруются нехитрым способом. Простая защита от любопытных.
  • Файл локализации Resources/Lang/Lang.xls парсится по языкам, шифруется и упаковывается в бинарный формат.
  • На лету создаются текстурные атласы. К примеру из папки с именем folder~atlas будут взяты все картинки и упакованы в единую картинку + сохранится файлик с координатами.
  • Звуки конвертируются в OGG формат.
  • 3D модели конвертируются во внутренний формат движка.
  • Файлы с именем file.pack преобразуются из текстовых в бинарные. Это хорошо подходит для всевозможных конфигов игры, уровней и т.д.

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

Форматы


Я бы рекомендовал использовать такие форматы:

WEBP для картинок. Вряд ли для кого-нибудь этот формат окажется новым. А для тех, кто слышит о нем впервые – webp может хранить картинку без потери качества как PNG, а так же с потерей — как JPEG, однако с заметно лучшим качеством, меньшем весом и с прозрачностью. Еще из плюсов – возможность скейла картинки на лету при чтении файла. Компилируется libwebp под все платформы без проблем.

OGG для звуков. Андроид нативно понимает OGG формат, а на iOS/OSX/tvOS я использую библиотеку Tremor (fixed-point version of the Ogg Vorbis) для раскодировки звуков в WAV и скармливанию их OpenAL. Попытки использовать OpenAL и на андроиде успехом не увенчались (звуки были с задержками).

Классы и модули


Разберем подробнее какие классы содержит движок и для чего нужны модули?
Правило «что выносить в модуль, а что в движок?» очень простое:


Следуя этому правилу, я распределил классы следующим образом:

Движок


  • Работа с платформой. Тут происходит инициализация приложения, а так же передача внешних событий (пауза, тачскрин, кнопки) в основной класс движка.
  • Основной класс. Тут крутится mainloop, обрабатываются входящие события (уже в универсальном виде независимом от платформы), происходит управление потокам и фоновыми задачами.
  • Работа с 2D. Вывод картинок, атласов, постэффектов.
  • Работа с 3D моделями. Загрузка моделей, рендеринг, управление шейдерами.
  • Стандартные UI элементы. Окна, кнопки, скролинг, уведомления.
  • Текстуры. Загрузка и выгрузка текстур. Сами декодеры находятся в модулях.
  • Вывод текста, рендеринг SDF шрифтов.
  • Математика. Всевозможные формулы, матрицы, кватернионы и т.д.
  • Социалка. Отправка писем, стандартный шаринг, rate me. Сами же соц. cети вынесены в модули.
  • Чтение/запись файлов.
  • Работа с UTF8 строками.
  • Работа с сетью.
  • Музыка/звуки.

Модули


  • Социалка
    • Facebook
    • Google Plus
    • Twitter
    • VK
  • Replay Kit (запись экрана для iOS)
  • JSON/XML
  • Декодеры картинок
    • WEBP
    • JPEG
    • PNG
  • In-apps (внутренние платежи)
  • Game Center, Google Play Services
  • Crashlytics (отслеживать краши)
  • Branch (глубокие ссылки)
  • Аналитика
    • Google Analytics
    • Game Analytics
    • Flurry
  • Реклама
    • Appodeal
    • Chartboost
    • Fyber
    • AdColony
    • UnityAds
    • Tapjoy
    • Google Ads
    • Heyzap
    • AdToApp

В следующих статьях я подробнее остановлюсь на конкретных классах и модулях, с примерами и полезностями. Отдельное внимание хочу уделить рендерингу SDF шрифтов (Signed Distance Field) и шейдерам в игре из шапки.

Если какие-то отдельные вопросы вас заинтересовали – плз пишите в каментах, добавлю их в план статей.




К сожалению, не доступен сервер mySQL