Heaps.io

Данный материал написан на основе записей докладов Nicolas Canasse:

  1. Making games with Heaps.io
  2. The tech behind Northgard
  3. Nicolas About Haxe : Heaps.io 2D/3D engine

Полный код приведенных здесь примеров можно найти здесь.

Что такое Heaps?

Heaps - это:

  • 2D + 3D игровой фреймворк с поддержкой аппаратного ускорения графики. Он создан таким образом, чтобы его легко можно было переносить на новые платформы, поэтому на текущий момент Heaps работает в Windows, MacOS, Linux, HTML5, Android, Xbox One, Playstation 4 и Nintendo Switch. 2D API в Heaps похож на Flash API (граф сцены реализован в виде Display List, так же как во Flash), и в то же время в Heaps можно работать с более низкоуровневыми объектами (шейдерами, буферами, вершинами и т.д.);
  • классы Heaps разбиты на 4 категории (пакета):
    • h2d - 2D API
    • h3d - 3D API
    • hxd - общие классы для работы с событиями, звуками, ресурсами и т.д.
    • hxsl - классы, относящиеся к языку шейдеров hxsl
  • при работе с 2D API предоставляются следующие основные объекты:
    • h2d.Tile - область или текстура целиком,
    • h2d.Sprite - спрайт, базовый класс для отображаемых объектов. Сам спрайт непосредственно ничего не может отрисовать, но выступает в качестве контейнера для других объектов.
    • h2d.Bitmap - объект, отображающий на экране текстуру или заданную область из текстуры.
    • h2d.Text - соответственно объект, отображающий на экране текст.
  • при работе с 3D чаще всего придется иметь дело с экземплярами следующих классов:
    • h3d.scene.Object - аналогично спрайту он может выступать как контейнер для других объектов в 3d-сцене, но сам не хранит информацию о геометрии объектов на сцене;
    • h3d.Camera - виртуальная камера, через которую можно наблюдать за сценой;
    • h3d.scene.Mesh - это объект, у которого есть геометрия (например, куб или сфера), а также материал, описывающий как эта геометрия должна отрисовываться, какие шейдеры и текстуры должны для этого использоваться.
  • работа с 2D и 3D объектами в Heaps осуществляется схожим образом. Например, чтобы добавить вложенный объект на сцену, и для 2D и для 3D используется метод addChild(), также у 2D и 3D объектов есть ряд одноименных свойств (visible, scaleX, scaleY, name и т.д.).
  • hxd.Res - подсистема управления ресурсами, основанная на макросах. Представляет собой виртуальную файловую систему с возможностью загрузки данных из разных источников (с диска, из сети, из данных, встроенных в само приложение).
  • встроенная система событий (события ввода с помощью мыши, клавиатуры, геймпада, сетевые события).
  • система шейдеров, которые пишутся на языке HxSL (Haxe Shader Language). HxSL-шейдеры являются по сути Haxe-кодом и напрямую встраиваются в код приложения (без всяких кавычек). С помощью макросов шейдеры конвертируются в язык, поддерживаемый на конкретной целевой платформе (glsl, hlsl, pssl).
  • настраиваемый рендер с поддержкой современных графических эффектов (например, PBR). Одна из идей, которой придерживались при создании Heaps, состоит в том, что любой его элемент можно было бы легко изменять. Поэтому рендер в Heaps можно полностью переделать под свои цели, не внося при этом изменений в код самого движка.
  • как уже упоминалось, Heaps может компилироваться под разные платформы:
    • Flash (используется Stage3D API),
    • HTML5 (в версии 1.3.0 появилась поддержка WebGL 2),
    • HashLink. Собирая проект под Windows вы можете выбрать между SDL и DirectX, с помощью которых осуществляется создание окна приложения, отрисовка графики и обработка событий. На других настольных ОС доступен только SDL. HashLink (а вместе с ним и проекты на Heaps) также может собираться под современные игровые консоли (доступ к коду, обеспечивающему поддержку этих платформ, бесплатен, для доступа необходимо обратиться к Nicolas Cannasse).
  • HIDE (Heaps IDE) - редактор для Heaps (также с открытым исходным кодом). В настоящее время в нем доступны такие функции как: просмотр ресурсов (моделей и их анимаций, текстур, эффектов), создание эффектов (систем частиц). Также началась работа над возможностью редактирования игровых уровней. Для отображения объектов в HIDE используется встроенный в него Heaps, так что объекты в редакторе выглядят также как и в игре.

В настоящее время профилирование приложений на HashLink осуществляется путем профилирования C кода, который получается на выходе (например, с помощью MS Visual Studio).

Для работы с сетью Nicolas рекомендует использовать его же библиотеку hxbit, пример работы с которой доступен в примерах, распространяемых вместей с Heaps - Network.hx.

Установка и настройка среды для Heaps

В настоящий момент в документации говорится, что для работы с Heaps необходима версия Haxe не ниже 3.4.2. Однако, если вы хотите собирать проект под HashLink, то могут возникнуть несовместимости между версиями Haxe и HashLink. Поэтому сразу оговорюсь, что при написании этого материала я использовал следующие версии Haxe, HashLink и библиотек:

  • Haxe 4.0.0-preview.4
  • HashLink 1.7
  • heaps 1.3.0
  • hlsdl 1.7.0
  • hldx 1.7.0
  • hlopenal 1.5.0 (библиотека для работы со звуком, от которой зависят библиотеки hlsdl и hldx)

Скачав и распаковав HashLink, нужно добавить путь к его папке в системную переменную Path (иначе не получится запускать hl-файлы в VS Code).

Heaps можно установить либо с haxelib:

haxelib install heaps

либо использовать git-версию библиотеки:

haxelib git heaps https://github.com/HeapsIO/heaps.git

Вместе с Heaps распространяется набор примеров (см. папку samples). Крайне рекомендуется ознакомиться с ними для лучшего понимания библиотеки.

В данном материале попробуем использовать VS Code в качестве редактора.

Для работы с Haxe в VS Code следует использовать набор расширений “Haxe extensions pack”:

Запускаем VS Code, открываем в нем папку для проекта (Файл->Открыть папку).

Добавляем в проект 2 файла:

1. Game.hx - основной (и единственный в нашем случае) класс проекта, наследуемый от hxd.App (базового класса для приложений на Heaps):

class Game extends hxd.App
{
        static function main()
        {
                new Game();
        }
}

2. game.hxml - файл с настройками для компиляции проекта.

В файле game.hxml задаем следующие параметры:

# Подключение библиотеки heaps
-lib heaps
# Подключение библиотеки hldx (работает только под Windows)
# Альтернативно можно использовать hlsdl
-lib hldx
# Задаем основной класс приложения с точкой входа
-main Game
# Выходной файл
-hl game.hl

Ctrl+Shift+B - запуск задачи - выбираем “haxe: game.hxml” - компиляция проекта.

После этого в списке файлов должен появиться файл game.hl.

Если этого не происходит, то это скорее всего означает что какой-то процесс завис и нужно перезапустить VS Code, проще всего это сделать вызвав команду “Reload window” - для этого вызываем панель команд (Ctrl+Shift+P) и вызываем эту команду.

Также если у вас перестает работать автокомплит, или переходы по коду, то скорее всего потребуется просто перезапустить Language Server - вспомогательный процесс, обращающийся к компилятору Haxe для выполнения данных задач. Перезапуск Language Server также осуществляется через панель команд (Ctrl+Shift+P) командой “Haxe: Restart Language Server”.

Попробуем теперь автоматизировать запуск нашего hl-файла, для этого создадим соответствующую задачу в проекте. Жмем F5 и в качестве среды выбираем HashLink:

В сгенерированном коде необходимо исправить имя hxml-файла (по-умолчанию оно задано как “build.hxml”) на наш “game.hxml”

Теперь нужно настроить задачу сборки по умолчанию (Задачи->Настроить задачу сборки по умолчанию…), выбрав game.hxml в качестве задачи:

В сгенерированной задаче необходимо дать ей имя “Build”, добавив поле “label” с соответствующим значением (так как задача с таким именем ищется и запускается перед запуском приложения - задана в launch.json полем “preLaunchTask”):

Настроив все задачи, можем наконец запустить приложение, нажав F5 - откроется пустое окно с черным фоном.

Кроме того, что в VS Code есть поддержка синтаксиса Haxe, работа с Heaps и HashLink в VS Code удобна еще и тем, что в пакет расширений входит отладчик HashLink.

Простой пример работы с отладчиком:

Давайте переопределим в приложении метод init(), который вызывается при запуске приложения. Добавим в него какую-нибудь строчку кода и поставим напротив нее точку останова (breakpoint):

Пересоберем и запустим приложение (F5) и увидим, что выполнение программы остановится, а в панели отладки отображаются стек вызовов функций и текущие значения переменных:

Обзор основных возможностей Heaps

Давайте теперь добавим визуальные объекты на сцену. Добавим 2d изображение - объект Bitmap, конструктор которого принимает 2 параметра: tile и parent.

tile описывает некоторую область текстуры (из одной текстуры можно создать множество тайлов). В этом примере создадим тайл программно - из текстуры с заливкой красным цветом.

parent - родительский объект на сцене. Добавим наш объект на 2d-сцену, которая автоматически создается движком - s2d (является свойством основного класса - h2d.App, кроме нее в приложении создается и 3d-сцена - s3d):

var b:h2d.Bitmap;

override function init()
{
        trace("Hello world!!!");

        b = new h2d.Bitmap(h2d.Tile.fromColor(0xff0000, 60, 60), s2d);
        b.x = 50;
        b.y = 100;
        // центрируем тайл (задаем ему якорную точку),
        // таким образом все трансформации объекта на экране
        // будут рассчитываться относительно центра тайла.
        // Если данный метод не вызывать, то тайл будет трансформироваться
        // относительно его левой верхней точки
        b.tile = b.tile.center();
        b.rotation = Math.PI / 4; // повороты задаются в радианах
}

В этом примере мы использовали метод tile.center(), чтобы поместить якорную точку тайла в его центр. Такого же результата можно добиться, используя свойства dx и dy тайла.

У объектов на 2d-сцене есть множество свойств, аналогичных свойствам DisplayObject во Flash, например, цветовые трансформации могут настраиваться следующим образом:

// Множитель цвета
b.color.set(1.0, 0.0, 0.0);
// Добавить цвет
b.colorAdd = new h3d.Vector(0.0, 0.0, 1.0, 0.0);
// Более сложная настройка цветовой трансформации
b.adjustColor({hue: Math.random()});
// "Выключение" заданного цвета
// (что-то вроде хромакея)
b.colorKey = 0xffffffff;

Добавим динамики - пусть наш объект изменяет свое положение со временем. Для этого в h2d.App есть метод update(dt:Float), который вызывается каждый кадр. Давайте переопределим его:

override function update(dt:Float)
{
        b.rotation += 0.01;
}

Теперь, если мы скомпилируем пример, то увидим красный вращающийся квадрат.

Рассмотрим теперь как в Heaps обрабатываются события пользовательского ввода.

В Heaps, в отличие от Flash, объекты на сцене сами не могут обрабатывать события мыши. Для этого предназначены объекты типа h2d.Interactive, которые добавляются как дочерние объекты на сцену:

// Интерактивный объект для обработки событий мыши
// По умолчанию он задает прямоугольную область.
// Добавляем этот объект как дочерний,
// чтобы он обрабатывал события для родительского объекта b
var i = new h2d.Interactive(b.tile.width, b.tile.height, b);
// необходимо сместить интерактивный объект для учета его якорной точки,
// которая находится посередине тайла
i.x = -0.5 * b.tile.width;
i.y = -0.5 * b.tile.height;

// Обработчики событий
i.onOver = function(_) b.alpha = 0.5;
i.onOut = function(_) b.alpha = 1.0;

Теперь при наведении мыши на красный квадрат он будет становиться полупрозрачным, а при отведении - непрозрачным.

Для того, чтобы переопределить поведение Interactive объекта (области, в которой срабатывают события мыши), вы можете переопределить у него метод onCheck().
Также в Heaps вместо прямоугольной области проверки можно задать ему область в форме эллипса, для этого у Interactive объекта есть булево свойство isEllipse.

Попробуем использовать изображение в качестве текстуры.

Добавим в проект папку “res” (по умолчанию папка с таким именем используется в качестве хранилища ресурсов, но при необходимости имя папки можно переопределить в проекте, задав значение флага компиляции resourcesPath), и поместим в нее наше изображение:

Теперь просто поменяем строчку

b = new h2d.Bitmap(h2d.Tile.fromColor(0xff0000, 60, 60), s2d);

на

b = new h2d.Bitmap(hxd.Res.haxeLogo.toTile(), s2d);

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

Кроме того, на основании расширений файлов система ресурсов самостоятельно определяет тип ресурса (например, jpg и png обрабатываются как изображения).

Таблица соответствия расширений файлов типам ресурсов определена в классе hxd.res.Config и может быть изменена, т.к. является публичной переменной:

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

hxd.Res.load("haxeLogo.png").toImage().toTile();

Также можно создать ресурс из массива байтов и привести его к требуемому типу:

hxd.res.Any.fromBytes(...);

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

class Game extends hxd.App
{
        static function main()
        {
                hxd.Res.initLocal();
               
                new Game();
        }

        var b:h2d.Bitmap;

        override function init()
        {
                // b = new h2d.Bitmap(hxd.Res.haxeLogo.toTile(), s2d);
                b = new h2d.Bitmap(hxd.Res.load("haxeLogo.png").toImage().toTile(), s2d);
                ...

В данном примере мы вызываем метод Res.initLocal(), что означает, что ресурсы будут читаться из локальной дисковой системы как отдельные файлы. Кроме этого метода доступны методы:

initEmbed() - все ресурсы будут встроены в файл приложения;

initPak() - ресурсы будут упакованы в pak-файлы (архивы ресурсов, которые можно сгруппировать по какому-либо признаку, например, ресурсы определенного игрового уровня).

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

В Heaps реализован механизм live reload для ресурсов, который активируется строчкой (перед инициализацией системы ресурсов):

hxd.res.Resource.LIVE_UPDATE = true;

А для обработки изменения ресурса нужно назначить callback, который будет вызываться при этом:

hxd.Res.haxeLogo.entry.watch(changeCallback);

Здесь свойство entry у hxd.Res.haxeLogo - это объект виртуальной файловой системы, абстракции с которой работает система ресурсов. У entry есть свой интерфейс, с помощью которго можно определить, является ли entry папкой или файлом, прочитать его байты и т.д.

Также следует сказать о том, что в Heaps неиспользуемые ресурсы автоматически удаляются из памяти, но есть и ручные методы их уничтожения, например, у тайлов есть метод dispose(), который удаляет из памяти используемую им текстуру.

Для кнопок и других “тянущихся” элементов (с использованием 9 slice) в Heaps присутствует класс h2d.ScaleGrid.

Для разметки элементов (layout) может использоваться компонент h2d.Flow.

Отрисовка текста может осуществляться либо с помощью класса h2d.Text, либо с помощью h2d.HtmlText, поддерживающего разметку с помощью html тэгов.

Текстовое поле с возможностью ввода реализовано в класса h2d.TextInput.

В данных классах в качестве шрифтов используются bitmap-шрифты, которые можно подготовить например в программе bmfont (в сети можно найти множество ее аналогов с большим набором возможностей). Также если в качестве целевой платформы выступают flash, js или lime (официально не поддерживается), то bitmap-шрифты можно генерировать программно из векторных с помощью класса hxd.res.FontBuilder:

var fnt = hxd.res.FontBuilder.getFont("Verdana", 24);

Простейший пример использования класса h2d.Text:

// В данном примере используется стандартный шрифт Heaps
var t = new h2d.Text(hxd.res.DefaultFont.get(), s2d);
t.scale(10);
// Для данного шрифта лучше не включать билинейную фильтрацию
// t.smooth = true;
t.text = "Haxe Rocks!!!";

h2d.HtmlText наследуется от h2d.Text и работа с ним ничем не отличается:

var t = new h2d.HtmlText(hxd.res.DefaultFont.get(), s2d);
t.scale(10);
t.text = "Haxe <font color='#ff0000'>Rocks!!!</font>";

h2d.HtmlText поддерживает тэги font, img (то есть в текст можно встраивать изображения), br, у тэга font поддерживаются атрибуты color, opacity, face.

h2d.InputText добавляет возможность обработки ввода текста с клавиатуры, а также возможность получить информацию о выделенном фрагменте текста (с помощью свойств cursorIndex и selectionRange):

var t = new h2d.TextInput(hxd.res.DefaultFont.get(), s2d);
t.scale(10);
t.text = "Input text";

Для батчинга тайлов в Heaps есть 2 класса:

h2d.TileGroup - для статических групп, элементы которых не изменяют своего относительного положения;

h2d.SpriteBatch - для динамических групп.

Пример использования h2d.TileGroup:

var g:h2d.TileGroup;

override function init()
{
        // Разбиваем текстуру на кадры по сетке
        var tiles = hxd.Res.haxeLogo.toTile().gridFlatten(10, 0, 0);
        // создаем группу
        g = new h2d.TileGroup(tiles[0], s2d);
        // отключим blend mode для группы, это ускорит отрисовку тайлов
        g.blendMode = None;
        // и добавляем в нее тайлы, произвольно расположенные на экране
        for (i in 0...1000)
        {
                g.add(Std.random(s2d.width), Std.random(s2d.height), tiles[i % tiles.length]);
        }
}

override function update(dt:Float)
{
        g.rotation += 0.01;
}

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

Рассмотрим создание простых 2d-анимаций с помощью класса h2d.Anim:

var animationSpeed = 15;
b = new h2d.Anim(hxd.Res.haxeLogo.toTile().split(10, false), animationSpeed, s2d);

Здесь мы разбиваем текстуру на 10 горизонтальных кадров, задаем скорость воспроизведения анимации и добавляем анимированный спрайт на сцену.

Конечно же, у анимированного спрайта можно вручную изменять скорость анимации, задавать текущий кадр, поставить анимацию на паузу и т.д. (в общем делать все, что ожидается от анимации):

b.currentFrame = 1;
b.pause = true;
b.speed = 10;

Для отрисовки геометрических примитивов в Heaps используются объекты типа h2d.Graphics (также есть h3d.scene.Graphics, работающий с 3D-сценой), которые похожи на flash.display.Graphics во Flash, но являются при этом элементом сцены, а не свойством спрайтов:

var g:h2d.Graphics;

override function init()
{
        g = new h2d.Graphics(s2d);
        g.beginFill(0xff00ff, 0.5);
        g.drawCircle(150, 150, 100);
}

При этом в отличие от OpenFL все примитивы отрисовываются полностью средствами GPU, не потребляя дополнительной памяти, а также сохраняются в буфере (то есть не пересчитываются каждый кадр).

Обработка нажатий кнопок клавиатуры отличается от Flash:

override function update(dt:Float)
{
        if (hxd.Key.isDown(hxd.Key.RIGHT))
        {
                g.x++;
        }
        else if (hxd.Key.isDown(hxd.Key.LEFT))
        {
                g.x--;
        }
}

Для организации работы с разными задачами в Heaps добавлен класс hxd.WaitEvent, который выполняет передаваемую в него функцию до тех пор, пока она не вернет true. Также в WaitEvent есть несколько предопределенных функций, например, для отложенного вызова метода:

var w:hxd.WaitEvent;

override function init()
{
        w = new hxd.WaitEvent();
        // Отложенное выполнение функции
        w.wait(2, function() {trace("Kept you waiting, huh?");});
        // функция everyFrameRoutine будет выполняться каждый кадр
        w.add(everyFrameRoutine);
}

override function update(dt:Float)
{
        w.update(dt);
}

function everyFrameRoutine(dt:Float):Bool
{
        trace("everyFrameRoutine");
        return false;
}

Перейдем теперь к знакомству с 3d в Heaps, рассмотрим для этого пример Base3D, который распространяется вместе с библиотекой.

Чтобы сгенерировать проект для VS Code запустим файл gen.hxml (он создаст проекты для всех примеров) и откроем его в VS Code (samples/build/base3D/).

В созданном проекте уже будут настроены задачи сборки под flash, html5 (выбирается по умолчанию) и HashLink.

Для отладки html5-версии вам понадобится установить расширение “Debugger for Chrome”. Но здесь давайте продолжим собирать приложение под HashLink, для этого перейдите на панель отладки и выберите в качестве конфигурации “HashLink”.

В данном примере показано как осуществляется работа с 3D-примитивами, простыми материалами, освещением и камерой:

В примере создается один примитив - куб с размерами 1х1х1, который затем используется двумя разными 3D-объектами (Mesh):

var prim = new h3d.prim.Cube();

Можно провести аналогию между парами “примитив-меш” в Heaps и “BitmapData-Bitmap” во Flash: также как несколько объектов типа Bitmap может отображать одну и ту же BitmapData, имея при этом разные свойства (положение, масштаб, поворот, цветовую трансформацию и т.д.), несколько объектов типа Mesh могут отображать одну и ту же геометрию, заданную примитивом.

Затем осуществляем подготовку куба для того, чтобы его можно было текстурировать и корректно работать с освещением:

// unindex the faces to create hard edges normals
prim.unindex();

// add face normals
prim.addNormals();

// add texture coordinates
prim.addUVs();

Создаем материал, который будет использовать текстуру:

var mat = h3d.mat.Material.create(tex);

Создаем 3D-объекты, отображаемые на сцене s3d. При этом второму объекту мы задаем пустой материал (без текстуры, что означает, что объект будет закрашен заданным цветом):

// our first cube mesh on the 3D scene with our created material
obj1 = new Mesh(prim, mat, s3d);
// creates another cube, this time with no texture
obj2 = new Mesh(prim, s3d);
// set the second cube color
obj2.material.color.setColor(0xFFB280);

Добавляем на 3D-сцену источник направленного света:

var light = new h3d.scene.DirLight(new h3d.Vector(0.5, 0.5, -0.5), s3d);
light.enableSpecular = true;

Также задаем интенсивность окружающего освещения на сцене, чтобы грани объектов, не освещенные добавленным источником света, все же были видны на экране:

s3d.lightSystem.ambientLight.set(0.3, 0.3, 0.3);

Отключаем у материалов объектов расчеты теней:

obj1.material.shadows = false;
obj2.material.shadows = false;

А в методе update(), вызываемом каждый кадр, мы обновляем положение камеры, которая облетает добавленные на сцену объекты, а также вращаем один из кубов вокруг произвольной оси:

s3d.camera.pos.set(Math.cos(time) * dist, Math.sin(time) * dist, dist * 0.7 * Math.sin(time));
obj2.setRotationAxis(-0.5, 2, Math.cos(time), time + Math.PI / 2);

Работа с событиями мыши в 3d аналогична - для этого создаются объекты h3d.scene.Interactive, которым задаются фигуры для определения пересечений с координатами мыши (с помощью raycasting’а). Добавим в качестве примера цветному кубу реакцию на события мыши:

// создаем объект Collider такого же размера как сам куб
// для более сложных моделей у объектов Mesh есть метод m.getCollider()
var b = h3d.col.Bounds.fromValues(-0.5, -0.5, -0.5, 1.0, 1.0, 1.0);
// добавляем в цветной куб объект Interactive
var i = new h3d.scene.Interactive(b, obj2);
// задаем обработчики событий мыши
i.onOver = function(_) obj2.material.color.setColor(0xFF0000);
i.onOut = function(_) obj2.material.color.setColor(0xFFB280);

В примерах Heaps есть также еще один пример, иллюстрирующий обработку событий мыши в 3D-сценах - Interactive.
В этом же примере есть код, показывающий загрузку анимированных моделей. Как видно из примера ниже, модели загружаются с помощью специального объекта типа ModelCache. Делается это для того, чтобы парсинг данных модели происходил только один раз (то же самое касается и загрузки анимаций из файлов модели):

// Загружаем модель и добавляем объект на сцену
var cache = new h3d.prim.ModelCache();
obj = cache.loadModel(hxd.Res.Model);
obj.scale(0.05);
obj.rotate(0, 0, Math.PI);
s3d.addChild(obj);

// Загружаем анимацию, зашитую в Model.fbx
var anim = cache.loadAnimation(hxd.Res.Model);
// Воспроизводим анимацию
// Обратите внимание, что одну и ту же анимацию
// можно применять к разным моделям
obj.playAnimation(anim);

// Настраиваем освещение на сцене
var light = new h3d.scene.DirLight(new h3d.Vector( 0.3, -0.4, -0.9), s3d);
light.enableSpecular = true;
light.color.set(0.28, 0.28, 0.28);
s3d.lightSystem.ambientLight.set(0.74, 0.74, 0.74);

// Создаем простой контроллер для камеры,
// чтобы ей можно было управлять мышью
new h3d.scene.CameraController(s3d).loadFromCamera();

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

Как уже говорилось, hxsl-шейдеры - это Haxe-код, шейдеры наследуются от базового класса hxsl.Shader. Код шейдера проверяется на этапе компиляции приложения, а не на этапе компиляции самого шейдера во время исполнения программы. Кроме того, на этапе компиляции проверяются и обращения к параметрам шейдера из кода.

Пример простейшего шейдера:

class MyShader extends hxsl.Shader
{
        static var SRC = {
               
                // Шейдер, от которого мы наследуемся.
                // Из него мы имеем доступ ко всем его
                // аттрибутам и константам
                @:import h3d.shader.Base2d;

                // Определяем параметр шейдера,
                // к которому можно будет обратиться из кода
                @param var color:Vec4;
               
                // Простейший фрагментный шейдер,
                // в котором мы переопределяем цвет на выходе
                function fragment()
                {
                        output.color = color;
                }
        };
}

А так этот шейдер можно назначить объекту на 2d-сцене:

b = new h2d.Bitmap(hxd.Res.haxeLogo.toTile(), s2d);
var s = new MyShader();
// задаем значение параметра шейдера,
// который мы добавили ранее
s.color.set(1.0, 0.0, 0.0, 1.0);
b.addShader(s);

Но самое интересное в этой системе шейдеров это то, что одному объекту можно добавить сразу несколько шейдеров, а в результате из них будет собран один шейдер, комбинирующий в себе их действие.

Например, определим еще один простой шейдер:

class MyShader2 extends hxsl.Shader
{
        static var SRC = {
               
                @:import h3d.shader.Base2d;

                @param var color:Vec4;
               
                function fragment()
                {
                        output.color += color;
                }
        };
}

И теперь назначим оба шейдера объекту на сцене:

b = new h2d.Bitmap(hxd.Res.haxeLogo.toTile(), s2d);
var s = new MyShader();
// задаем значение параметра шейдера,
// который мы добавили ранее
s.color.set(1.0, 0.0, 0.0, 1.0);
b.addShader(s);

// Добавляем объекту второй шейдер
var s2 = new MyShader2();
s2.color.set(0.0, 0.0, 1.0, 0.0);
b.addShader(s2);

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

Еще один пример шейдера, но уже для 3D-объекта:

class EffectShader extends hxsl.Shader
{
        static var SRC = {
               
                // Импортируем все определения из
                // базового шейдера для 3D
                @:import h3d.shader.BaseMesh;

                // Вершинный шейдер
                function vertex()
                {
                        transformedPosition.z += sin(transformedPosition.y * 3 + global.time * 4) * 0.5;
                }
               
                // Фрагментный шейдер
                function fragment()
                {
                        output.color.r *= transformedPosition.z;
                }
        };
}

А так данный шейдер задается объекту на сцене:

// Задаем шейдер для нашего объекта типа Mesh
obj.material.mainPass.addShader(new EffectShader());

Как видно, материалы объектов в Heaps могут отрисовываться в несколько проходов (в примере мы используем только основной), что дает возможность создавать эффекты любой сложности.

Инструменты для Heaps

HIDE - IDE для Heaps, написана на Haxe, собирается как приложение на Electron. По словам Nicolas Cannasse выбор Electron для создания редактора крайне удачен, так как позволяет относительно быстро создавать любые интерфейсы, пользуясь при этом html, css и существующими библиотеками на js.

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

В HIDE встроен просмотрщик всех ресурсов, с которыми может работать Heaps: текстовые файлы, звук, 3D-модели и сцены, системы частиц, изображения, скрипты.

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

Nicolas начал работу по интеграции CastleDB в HIDE:

CastleDB - это модель данных для игр, которые хранятся в json и могут использовать в проектах на Haxe с помощью библиотеки castle, доступной на haxelib. При этом библиотека castle не просто парсит json-файлы, с помощью макросов она генерирует enum’ы и typedef’ы для структур данных, которые хранятся в базе и могут ссылаться друг на друга. Таким образом, сокращается число возможных ошибок, так как вы работаете уже с типизированными данными и не сможете обратиться к несуществующему полю у какой-либо структуры.

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

Также в клиенте CasteDB имеется встроенный редактор тайловых карт, при этом на объекты на карте можно навешивать сущности, описываемые в базе данных.

Чтобы загрузить json-файл с данными из CastleDB достаточно добавить в проект класс Data.hx со следующим кодом:

private typedef Init = haxe.macro.MacroType<[cdb.Module.build("data.cdb")]>;

И загрузить данные (после инициализации системы ресурсов):

static function main()
{
        hxd.Res.initEmbed();
        Data.load(hxd.Res.data.entry.getText());

        new Game();
}

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

for (i in Data.item.all)
{
        switch (i.id)
        {
                case Sword:
                        trace(i.cost.name);
                default:

        }
}

Отдельно клиент CastleDB можно скачать с его официального сайта (там же можно найти довольно подробную документацию по нему), однако Nicolas планирует прекратить его поддержку и полностью сосредоточиться на HIDE.

Сейчас Nicolas занимается тем, что работает над 3D-редактором сцен в HIDE:

Редактор систем частиц в HIDE:

Анимация свойств частиц осуществляется на GPU. Системы частиц для 2D и 3D в Heaps реализованы отдельно.

Загрузка эффекта, созданного в редакторе осуществляется в 2 строчки:

var p = new h3d.parts.GpuParticles(s3d);
p.load(haxe.Json.parse(hxd.Res.particlesJson.entry.getText()));

Тот же пример, но с реализацией Live reload:

var p = new h3d.parts.GpuParticles(s3d);

var particlesJson = hxd.Res.particlesJson;
// обработчик изменений в json-файле с описанием системы
function onParticleJsonCHange()
{
        p.load(haxe.Json.parse(hxd.Res.particlesJson.entry.getText()));
}

// назначаем callback
particlesJson.watch(onParticleJsonCHange);
// загружаем json c настройками эффекта
onParticleJsonCHange();

Просмотрщики каждого типа ресурса реализованы в виде отдельных классов (Image, Model, Sound, Script, Particle2D, Particle3D), исходный код которых можно посмотреть в пакете hide.view.

В этом материале мы рассмотрели только базовые элементы, но в Heaps заложено гораздо больше (например, для отрисовки больших 3d-сцен есть специальный класс h3d.scene.World, развивающий сцену на отдельные элементы). Поэтому крайне рекомендую покопаться в примерах, распространяемых вместе с движком.

К сожалению Heaps страдает тем, что слабо документирован, и ваша помощь в исправлении этой проблемы совсем не помешает ;)

Кроме примеров могу порекомендовать следующие источники:

  1. Текущая версия сайта Heaps: https://heapsio.github.io/heaps.io/. Работа над ним еще не завершена, но там можно найти основную информацию по движку и примеры работы с ним.
  2. Официальный чат: https://gitter.im/heapsio/Lobby
  3. Wiki на github: https://github.com/HeapsIO/heaps/wiki

Надеюсь, что данный материал оказался хоть немного полезным :)