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

Для непосвящённых: Haxe — это язык программирования и кросс-компилятор. Это значит, что можно написать игру на Haxe, и она автоматически "переводится" на другой язык программирования, в зависимости от выбранной платформы (C++ для Windows, JavaScript для Web, и т.д.), и компилируется в нативную программу для той платформы.

У языка есть несколько полезных функций метапрограммирования, которые используются для написания кода, который, грубо говоря, сам себя меняет. Эта статья — не туториал и не руководство, а просто несколько примеров того, как такие приёмы могут быть использованы в разработке компьютерных игр.

Кстати, некоторые из этих функций есть и в других языках, но могут называться по-другому. Так что эти идеи могут пригодиться не только тем, кто пишет на Haxe.

Условная компиляция

Это, вероятно, самый простой способ, как можно повлиять на процесс сборки. Есть возможность выборочно компилировать куски кода, используя флаги компилятора.

Например, при разработке игр я всегда пользуюсь собственным редактором уровней, который встроен в саму игру. За исключением игры Speebot, этот редактор доступен только мне, и не включён в конечную сборку, которую запускает игрок. Это достигается "заворачиванием" всего кода, что связан с редактором, в условие, которое проверяет наличие флага "dev" при компиляции. Если флага нет — редактор "стирается" из исходного кода перед нативной компиляцией игры.

Встроенный редактор уровней в игре Пайли, который доступен только в режиме разработки.
Встроенный редактор уровней в игре Пайли, который доступен только в режиме разработки.

Эта функция также позволяет мне отделять ресурсы для "demo" и "prod" версий. Демо версии моих игр включают в себя несколько уровней игры, и я использую флаги компилятора, чтобы определить, какие уровни, файлы музыки и т.д. нужно включить в сборку. Так неиспользуемые ресурсы просто не попадают в демо версии.

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

Мета данные

В Haxe есть функции metadata, которые могут быть использованы для аннотации, чтения и манипуляции частей исходного кода, который обычно не доступен.

В моём случае, есть класс Settings, в котором есть набор переменных для опций, доступных игроку в меню Опции. Настройки пользователя хранятся в отдельном файле. Этот файл генерируется автоматически на основе класса Settings. Движок бежит по всем переменным класса, и сохраняет или загружает значения из файла. Для этого используется reflection API.

Не все переменные в Settings нужно сохранять в файле, так как там есть и константы, которые менять не нужно. Такие поля помечаются мета тэгом "@ignore(true)". Движок, видя эту аннотацию, не включает такое поле в файл.

Макро

Это самая сложная и самая интересная функция метапрограммирования в Haxe. Macro позволяют запускать реальный Haxe код во время компиляции, который может напрямую модифицировать исходный код игры.

Самое простое применения этому: добавления времени компиляции и номера сборки. Эта информация у меня используется вместо номеров версий. Она всегда обновляется автоматически, поэтому мне не нужно вручную увеличивать какие-то версии.

Но самый большой плюс для меня — это возможность переместить код из run-time в compile-time.

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

Эта логика работает нормально до тех пор, пока не становится необходимо показать эту информацию для 200 уровней одновременно. Игре необходимо загрузить и обработать 200 файлов уровней, что использует много памяти и реально тормозит игру на несколько секунд.

Всего 4 мира, в каждом по 50 уровней. Процент прохождения высчитывается на основе количества пройденных уровней и собранных кристаллов в каждом уровне данного мира.
Всего 4 мира, в каждом по 50 уровней. Процент прохождения высчитывается на основе количества пройденных уровней и собранных кристаллов в каждом уровне данного мира.

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

Решение: написать макро, которое загружает все 200 уровней (у макро есть доступ к файловой системе), обрабатывает все необходимые данные, и сохраняет нужные числа в массивы. Игре больше не нужно ничего вычислять в run-time, потому что вся информация на этот момент уже жёстко прописана в исходном коде игры с помощью макро.

Такой же подход используется в игре Путь Фантома. Игрок может найти артефакт, который показывает количество пропущенных секретов, сокровищ, записок и т.д. в каждой области игры на карте. Вместо того, чтобы загружать и обрабатывать информацию всех областей игры в run-time, это происходит в момент компиляции с помощью макро.

Так, полностью пропадает зависание игры при просмотре карты. Кроме того, игра использует меньше памяти, так как не нужно подгружать все уровни сразу.

Оригинал статьи на моём сайте.

Комментарии (7)


  1. igrishaev
    04.05.2022 16:37
    +2

    Эта стятья — не туториал и не руководство, а просто несколько примеров того,

    И где эти несколько примеров? Скорее эта статья -- реклама блога и своих игр.


    1. kircode Автор
      04.05.2022 17:23

      Примеры из статьи:

      • Условное включение участков кода, в зависимости от режима

      • Условное разделение ресурсов и кода для демо версий

      • Различные режимы оптимизации движка

      • Автоматическая сериализация файлов данных на основе структуры кода

      • Фиксирование номера билда и времени сборки

      • Перевод обработки данных из run-time в compile-time для оптимизации


      1. igrishaev
        04.05.2022 17:30
        +3

        Это не примеры, а упоминания фич.


      1. pfffffffffffff
        05.05.2022 00:53

        На счет последнего пункта, его можно реализовать через скрипт который будет пробегать по этим 200 картам и собирать из них данные в один класс


  1. AndreyMust19
    04.05.2022 16:37
    +1

    Конечно, я мог бы вручную посчитать все кристаллы

    Можно было добавить код сохранения кол-ва кристаллов в самом редакторе, во время сохранения уровня


    1. kircode Автор
      04.05.2022 17:30

      Если добавлять информацию в тот же файл, то смысла в этом нет, так как чтобы прочитать это количество, придется всё равно читать весь файл.

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


      1. AndreyMust19
        05.05.2022 10:08

        А я и не говорю сохранять в тот же файл.
        И что мешает прочитать только заголовок, а не весь файл, и кол-во кристаллов хранить там?

        Хотя да, наиболее универсальный способ - если еще не посчитано кол-во кристаллов, то загрузить файл, посчитать и сохранить в другой файл.