Рендеринг Angular-приложения в Minecraft

Идея эксперимента состоит в том, чтобы отрендерить Angular-приложение в Minecraft. Создание компьютера из редстоуна, способного запустить Node.js, могло бы занять полжизни, поэтому приложение работает в Node.js снаружи, а Minecraft служит ему экраном.
В ходе эксперимента создаётся приложение погоды с управлением из игрового мира. За интерфейсом стоят обычные Angular-сущности.
Для этого нужно связать три вещи: операции Angular над элементами, расчёт их расположения и команды Minecraft. Выберем способ отрисовки.
Отрисовка в Minecraft через RCON и block_display
Для программной отрисовки удобнее всего использовать RCON, однако для этого надо запустить мир именно на сервере с доступом к внутренним настройкам. Делаем это локально, скачав готовый сервер на официальном сайте. RCON - интерфейс для удалённого выполнения консольных команд. Через npm-библиотеку rcon-client приложение отправляет команды из Node.js в игровой мир.
Первый вариант очевиден: считать каждый блок пикселем. Команда setblock устанавливает один блок, а fill заполняет прямоугольную область. Так можно собрать экран, и этот режим в проекте тоже есть. Возьмем экран 200 на 100 пикселей, но уже для такого изображения требуется 20 000 позиций блоков. При обновлении интерфейса требуется менять участки мира, а размер пикселя ограничивается размером одного блока. Это достаточно тяжелая операция и даже так, экран таких размеров не подходит, он слишком мал. Поэтому будет использоваться block_display.
Это сущность, которая отображает модель блока и позволяет менять её размер и положение. Такие сущности доступны начиная с Minecraft Java Edition 1.19.4. Это виртуальный блок, который виден в мире, но не участвует в физических взаимодействиях в мире. Из модели блока можно сделать тонкий прямоугольник и разместить его в любой точке экрана. В настройках задаётся pixelsPerBlock: 4 четыре пикселя по горизонтали и четыре по вертикали на площади одной грани блока. Получается 16 пикселей вместо одного. Экран 880 на 550 пикселей занимает 220 на 137,5 блока. Такого размера вполне достаточно для отображения небольшого приложения и комфортной работы. Теперь необходимо решить вопрос самого рендринга.
Рендеринг Angular и создание собственной платформы
В обычном браузерном приложении Angular-компонент содержит данные и шаблон, который описывает интерфейс. При компиляции шаблон превращается в инструкции Angular. При создании представления эти инструкции создают элементы и текстовые узлы, связывают их между собой, устанавливают атрибуты и регистрируют обработчики событий. Когда состояние компонента меняется, Angular во время проверки изменений вычисляет значения привязок и обновляет те, чьи значения отличаются от предыдущих. Например, новое значение температуры приводит к обновлению текста в соответствующем узле. Пересоздавать весь интерфейс для этого не требуется. Операции над узлами выполняются через рендерер. В браузерной реализации они изменяют DOM, после чего браузер рассчитывает расположение элементов и рисует результат на экране. Angular управляет структурой представления и привязками, а расчёт геометрии и отрисовку пикселей выполняет браузер. Для вывода в Minecraft этот последний участок нужно заменить: вместо DOM хранить собственные узлы, рассчитывать их расположение через Yoga и отображать результат сущностями в игровом мире.
Angular позволяет создавать собственные платформы и реализации рендерера. Платформа собирает зависимости для нужной среды, а рендерер определяет, что происходит при создании элемента, изменении текста или установке стиля.
Назовем платформу platformMinecraft. Она создаётся через createPlatformFactory на основе platformCore. При запуске ей передаются подключение RCON, параметры экрана и настройки шрифтов:
platformMinecraft([
{ provide: MC_RCON, useValue: rcon },
{ provide: MC_SCREEN, useValue: screen },
{ provide: MC_FONT, useValue: font },
]).bootstrapModule(AppModule);Внутри регистрируется MinecraftRendererFactory, которая возвращает рендерер для Minecraft. Он реализует Renderer2: createElement, createText, appendChild, setStyle, setValue и другие методы, через которые Angular работает с узлами интерфейса.
Пример простого шаблона:
<div style="background: black_concrete; color: white_wool">
{{ temperature() }}
</div>При создании представления рендерер получает операции создания элемента и текста, их связывания и установки атрибутов. Когда значение temperature() меняется, Angular обновляет текст через setValue. Рендерер для Minecraft преобразует эти операции в изображение. Для этого хранится отдельное дерево объектов McNode. У каждого узла есть родитель, дети, атрибуты и стили. Текстовые узлы содержат строку, узлы картинок содержат декодированное изображение. Angular вызывает методы рендерера, а они изменяют это дерево. Например, добавление дочернего узла в проекте выглядит так:
appendChild(parent: McNode, child: McNode): void {
if (!parent || !child) return;
this.detach(child);
child.parent = parent;
parent.children.push(child);
parent.yogaNode.insertChild(
child.yogaNode,
parent.yogaNode.getChildCount(),
);
this.markDirty();
}Здесь ребёнок добавляется сразу в два дерева: дерево элементов McNode и дерево Yoga, которое отвечает за их размеры и положение. В конце сцена помечается как требующая обновления.
Интеграция Yoga и расчёт layout
Создать дерево недостаточно. Нужно узнать, где начинается карточка, сколько места занимает заголовок и куда поставить кнопку после текста. В браузере этим занимается движок layout. В платформе Minecraft эту работу будет выполнять Yoga.
Yoga рассчитывает размеры и координаты прямоугольников по правилам, основанным прежде всего на Flexbox. Сам движок ничего не рисует. На вход ему передаются структура элементов и поддерживаемые стили, а результатом расчёта становится геометрия. Для каждого узла можно получить положение относительно родителя и размер, а затем преобразовать их в экранные координаты { x, y, w, h } для отрисовки.
Например, контейнер шириной 400 и высотой 120 пикселей располагает два элемента в строку. Внутренние отступы составляют 16 пикселей со всех сторон, промежуток между элементами равен 12 пикселям. У первого элемента заданы ширина 120 и высота 80 пикселей. Второй имеет flex: 1 и растягивается по высоте через align-items: stretch у родителя.
После calculateLayout() вызов getComputedLayout() для каждого дочернего узла даёт следующие значения. Здесь показаны четыре поля результата, которые нужны для отрисовки:
// Первый элемент
{ left: 16, top: 16, width: 120, height: 80 }
// Второй элемент
{ left: 148, top: 16, width: 236, height: 88 } Второй элемент начинается после левого отступа, первого элемента и промежутка: 16 + 120 + 12 = 148. Его ширина занимает остаток контейнера: 400 - 16 - 16 - 120 - 12 = 236. Высота равна доступной высоте между отступами: 120 - 16 - 16 = 88. Если сам контейнер расположен на экране в точке { x: 32, y: 24 }, рендерер прибавляет это смещение и получает:
// Прямоугольники в координатах экрана
// [{ x: 48, y: 40, w: 120, h: 80 }, { x: 180, y: 40, w: 236, h: 88 }] Это ещё не координаты блоков Minecraft. Yoga рассчитывает layout в единицах интерфейса, которые здесь принимаются за пиксели. Перевод в мировые координаты с учётом масштаба экрана выполняет рендерер.
Связь элементов рендерера с узлами Yoga
Yoga подключается непосредственно к собственному рендереру. При создании каждого McNode создаётся и соответствующий узел Yoga:
import Yoga from 'yoga-layout';
function createNode(type: McNode['type'], tag?: string): McNode {
return {
type,
tag,
children: [],
attrs: {},
parent: null,
yogaNode: Yoga.Node.create(),
style: {},
};
}Методы createElement и createText рендерера используют эту функцию. При добавлении ребёнка appendChild, показанный выше, вызывает parent.yogaNode.insertChild(...). При удалении используется removeChild у узла Yoga. Так структура layout остаётся синхронизированной с деревом, которым управляет Angular.
Преобразование стилей в параметры Yoga
Например, шаблон задаёт такую раскладку:
<div style="flex-direction: row; padding: 16px; gap: 12px">
<div style="width: 120px; height: 80px; background: blue_concrete"></div>
<div style="flex: 1; background: gray_concrete"></div>
</div>Первый элемент получает ширину 120 пикселей. Второй занимает оставшееся место. Yoga учитывает внутренние отступы и промежуток между ними. Свойства из style рендерер переводит в вызовы вроде setWidth, setPadding и setFlex. Для динамических стилей Angular вызывает setStyle. В рендерере этот метод передаёт значение в applyStyle и помечает сцену как требующую обновления:
setStyle(
el: McNode,
style: string,
value: any,
flags?: RendererStyleFlags2,
): void {
applyStyle(el, style, value);
this.markDirty();
}Внутри applyStyle имена свойств приводятся к camelCase, а значения размеров разбираются функцией toLen. Ниже приведён фрагмент обработки свойств:
const y = el.yogaNode;
switch (name) {
case 'width': return y.setWidth(toLen(str));
case 'height': return y.setHeight(toLen(str));
case 'flex': return y.setFlex(toLen(str) as number | undefined);
case 'gap': return void y.setGap(Gutter.All, toLen(str));
}Статический атрибут style="..." приходит через setAttribute. Его содержимое разбивается на отдельные объявления, каждое из которых также передаётся в applyStyle. Поэтому статическая ширина и привязка [style.width] в итоге меняют один и тот же узел Yoga. Цвета обрабатываются отдельно: background и color сохраняются в McNode для отрисовки и не участвуют в расчёте layout.
Пересчёт layout при изменении представления
В фабрике рендерера корневому узлу Yoga задаются ширина и высота экрана. Callback markDirty, переданный в MinecraftRenderer, устанавливает флаг dirty у фабрики. Этот флаг означает, что сцену нужно обновить.
После прохода обновления представлений Angular вызывает RendererFactory2.end(). В реализации Minecraft здесь планируется пересчёт:
end(): void {
if (!this.dirty || this.scheduled) return;
this.scheduled = true;
setTimeout(() => {
this.scheduled = false;
this.dirty = false;
this.flush();
}, 0);
}Так несколько изменений элементов объединяются в один запланированный вызов flush(). В его начале запускается расчёт Yoga:
this.root.yogaNode.calculateLayout(
this.screen.width,
this.screen.height,
Direction.LTR,
);Затем collectPaint обходит дерево и читает результат расчёта каждого видимого узла. Координаты Yoga заданы относительно родителя, поэтому к ним прибавляются накопленные смещения:
const l = node.yogaNode.getComputedLayout();
const x = offX + l.left;
const y = offY + l.top;
const w = Math.round(l.width);
const h = Math.round(l.height);По этим координатам собираются фоновые прямоугольники, пиксели текста и изображений. Полученный набор передаётся в MinecraftPainter, который формирует команды для Minecraft.

Измерение размеров текста и изображений
С текстом и картинками есть дополнительная задача. Yoga не знает, сколько места занимает строка выбранным шрифтом или какого размера исходный PNG. Для таких элементов задаётся [функция измерения](https://www.yogalayout.dev/docs/advanced/external-layout-systems). Движок передаёт ей ограничения, а она возвращает размер содержимого.
В случае картинки, если задана только ширина, высоту можно получить из пропорций исходного изображения. Для текста нужно измерить символы и посчитать переносы. Если надпись становится длиннее, её высота может измениться и сдвинуть элементы ниже. Поэтому при изменении текста его узел Yoga помечается через markDirty(), для пересчёта размеров.
Здесь есть два разных признака изменений: флаг dirty фабрики планирует обновление экрана, а yogaNode.markDirty() сообщает Yoga, что содержимое требует повторного измерения. При изменении текста рендерер устанавливает оба:
setValue(node: McNode, value: string): void {
node.value = value;
if (node.type === 'text') node.yogaNode.markDirty();
this.markDirty();
}После layout известны размеры и координаты прямоугольников элементов. Залить фон уже просто. Теперь нужно заполнить их содержимым.
Растеризация текста
Можно вывести текст на canvas, прочитать пиксели и перенести их в Minecraft. В текущей реализации используется opentype.js, без canvas.
Библиотека читает TTF- и OTF-файлы и даёт доступ к контурам глифов, то есть графических форм символов. Растеризатор приближённо разбивает кривые контура на отрезки и проверяет, какие пиксели попадают внутрь буквы.
Одной проверки в центре пикселя недостаточно для тонких штрихов: результат слишком зависит от того, как контур ложится на сетку. Поэтому внутри каждого пикселя проверяются девять точек, расположенных сеткой 3 на 3. Если доля точек внутри контура достигает порога, пиксель закрашивается.
Это суперсэмплинг. Здесь он помогает выбрать форму буквы на грубой сетке. Сам пиксель в результате либо закрашен, либо пропущен; полупрозрачного сглаживания текста здесь нет.
Размеры символов используются и при расчёте layout. Строки переносятся по словам, а слишком длинное слово разбивается по символам. Контуры и растеризованные глифы кэшируются, чтобы одну и ту же букву одного размера не считать заново.
На выходе получается список координат пикселей. Их цвет берётся из свойства color, а файл шрифта выбирается через font-family из зарегистрированных в приложении семейств.

Обработка изображений и подбор цветов
С картинками проще: пиксели уже есть в файле. PNG и JPEG декодируются через pngjs и jpeg-js, затем изображение приводится к размеру, который рассчитывает Yoga.
При уменьшении усредняются цвета исходной области с учётом прозрачности, при увеличении выбирается исходный пиксель. Пиксели с альфой ниже порога пропускаются. Готовый результат кэшируется.
Теперь нужно подобрать блок для каждого цвета. В палитре есть бетон, шерсть и терракота. Для каждого материала записан приблизительный средний цвет текстуры. Рендерер сравнивает его с нужным RGB по взвешенному расстоянию redmean и выбирает ближайший вариант.
Так обрабатываются и пиксели картинки, и цвета вроде #a144ff в стилях. Если нужен определённый материал, можно написать прямо background: black_concrete или color: white_wool.
Точное совпадение с браузерными цветами здесь не гарантируется: набор материалов ограничен, а у блоков остаются текстуры. Зато даже логотип Angular получается собранным из материалов Minecraft.

Оптимизация отрисовки и обновления сущностей
К этому моменту рендерер знает положение фонов и пикселей текста и картинок. Если отправить каждый пиксель отдельной командой, такой экран требует слишком много ресурсов.
Поэтому фон контейнера сразу выводится одним прямоугольником. Пиксели текста и изображений сначала объединяются в горизонтальные полосы одного материала. Если полосы на соседних строках совпадают по положению, ширине и глубине, они склеиваются по вертикали.
Например, сплошной вертикальный штрих буквы может стать одной вытянутой сущностью. Для такого прямоугольника достаточно команды summon minecraft:block_display с нужным масштабом, материалом и смещением.
Чтобы текст был виден поверх фона, слои немного разнесены по глубине. Чем глубже элемент вложен в дерево, тем ближе к зрителю располагается его изображение.
Следующая оптимизация касается обновлений. Рендерер хранит предыдущий набор прямоугольников и сравнивает его с новым. Совпавшие фрагменты остаются на месте, новые создаются через summon, исчезнувшие удаляются через kill. При сравнении учитываются координаты, размеры, материал и глубина.
Если меняется температура, не нужно заново создавать фон и остальные неизменившиеся части интерфейса. Если изображение целиком остаётся прежним, команды отрисовки не отправляются.
Пересчёт запускается после изменений от Angular через RendererFactory2.end(): сначала Yoga обновляет геометрию, затем рендерер собирает прямоугольники и отправляет разницу в Minecraft.
Обработка действий игрока и событий интерфейса
Для событий у Renderer2 есть метод listen(). Через него Angular регистрирует обработчики, которые вызываются при соответствующем действии игрока. Событие срабатывает при приближении к элементу. Раз в 500 мс приложение запрашивает позиции игроков через RCON
Позиция с поправкой на высоту глаз проецируется на плоскость экрана. Затем по координатам, рассчитанным Yoga, определяется элемент под этой точкой. Если игрок достаточно близко и входит в область нового элемента, вызываются mouseenter и click. Когда выходит, вызывается mouseleave. Для повторного нажатия той же кнопки нужно выйти из её области и вернуться.
Направление взгляда и нажатие кнопки мыши здесь не учитываются. В настройках экрана допускается расстояние до плоскости в 40 блоков. Получается управление перемещением игрока, поэтому для кнопок задаются крупные размеры.
Для click есть всплытие по дереву и stopPropagation(). Обработчик выполняется внутри Angular zone, чтобы изменение состояния вызывало обновление приложения.
С точки зрения компонента всё привычно: (mouseenter) включает подсветку, (mouseleave) убирает её, (click) вызывает обработчик. Кнопка сообщает родителю о нажатии, тот меняет выбранный день, Angular обновляет текст и стили. В Minecraft появляется следующий прогноз.
Исправление смещения фрагментов текста
При проверке рендерера в игровом клиенте обнаружилась проблема: в локальном PNG-превью текст выглядел ровным, а в Minecraft отдельные фрагменты мелких букв были сдвинуты. Штрихи не стыковались. Изначально каждая сущность создавалась в собственном центре с дробными мировыми координатами. Сама модель смещалась обратно на половину своего размера. По расчётам границы соседних прямоугольников совпадали, но в игровом клиенте получались расхождения. Проблему удалось устранить изменением системы координат. Теперь все сущности экрана создаются в одной целочисленной точке возле его центра. Положение каждого прямоугольника задаётся локальным сдвигом относительно этой общей точки. Экран находится далеко от начала мира, примерно у X = -24535. При разборе проблемы учитывалась точность координат. После перехода на общую точку отсчёта смещения исчезли, что было проверено в игре.
Возможности и ограничения рендерера
В результате рендерер поддерживает Flexbox через Yoga, размеры и отступы, прямоугольные заливки, текст, PNG/JPEG и события приближения игрока. Этого хватает для погодного приложения. CSS-классы не обрабатываются, поэтому стили задаются inline и через Angular-привязки. Нет теней, скруглений и полного набора браузерных правил layout. Компоненты, которые напрямую обращаются к DOM, требуют адаптации. Скорость обновления зависит от количества изменившихся прямоугольников, обмена по RCON и отрисовки сущностей клиентом. Без сравнительных замеров производительности нельзя утверждать, что такой способ всегда быстрее отрисовки настоящими блоками. Получившийся рендерер позволяет Angular-компоненту загружать погоду, реагировать на действия игрока и обновлять интерфейс в игровом мире. Чтобы переключить прогноз на завтра, нужно подойти к кнопке "Далее".
Автоматическая пересборка и перезапуск приложения
Для разработки добавляется режим автоматического обновления:
npm run devСкрипт dev.mjs следит за файлами в src. После сохранения шаблона или TypeScript-файла он запускает сборку, а при её успехе перезапускает приложение. При старте старые сущности экрана удаляются и интерфейс рисуется заново. Вручную запускать сборку или вводить команды в Minecraft не требуется.
Например, можно поменять font-size: 28px на font-size: 36px в компоненте показателя погоды и сохранить файл. После сборки и перезапуска Yoga рассчитывает новые размеры, а текст на игровом экране становится крупнее. Так же можно менять отступы, цвета и расположение элементов и сразу после обновления оценивать результат в Minecraft. По рабочему процессу это похоже на hot reload, но технически используются пересборка и полный перезапуск процесса Node.js. Состояние приложения не сохраняется: выбранный день сбрасывается, а прогноз загружается снова. Обновление занимает время сборки, запуска и отрисовки. Если сборка завершается с ошибкой, предыдущая версия приложения продолжает работать.
Общая схема архитектуры

Приложение работает в Node.js на платформе platformMinecraft. Angular создаёт представление и обновляет привязки через собственный Renderer2. Рендерер хранит дерево McNode, передаёт структуру и стили в Yoga и после изменений запускает расчёт размеров и координат.
По этой геометрии собирается изображение: фоны становятся прямоугольниками, текст растеризуется из контуров шрифта, PNG и JPEG масштабируются до нужных размеров. Цвета заменяются материалами Minecraft. Одноцветные пиксели объединяются, новый набор прямоугольников сравнивается с предыдущим, и через RCON отправляются команды только для изменившихся фрагментов. В мире результат отображается сущностями block_display с дробным масштабом и общей точкой отсчёта.
Обратный путь начинается с опроса координат игроков. MinecraftInput проецирует их на экран, проверяет попадание в границы элементов и вызывает обработчики, зарегистрированные через listen(). Обработчики меняют состояние Angular, после чего повторяется цикл отрисовки. Поэтому интерфейс реагирует и на новые данные погоды, и на действия игрока.
Поддержка интерфейса ограничивается выбранным набором стилей и событий. Yoga отвечает за layout, а рендерер обеспечивает рисование и ввод, которые в обычном веб-приложении предоставляет браузер.
Приложение прогноза погоды
На этой платформе реализовано приложение прогноза погоды для Дубны на семь дней. На экране размером 880 на 550 пикселей есть дата, погодная иконка, максимальная и минимальная температура выбранного дня, максимальный ветер и вероятность осадков. Внизу расположены две кнопки для переключения дней.
Например, сначала открыт сегодняшний прогноз. Игрок подходит к области кнопки перехода вперёд, она подсвечивается, и приложение переключается на завтра. На экране меняются дата, показатели и погодная иконка. Можно пройти дальше по дням или вернуться назад. На первом и последнем днях соответствующая кнопка отключается и меняет цвет.
Так работает обратная связь: положение игрока в Minecraft становится событием для Angular. Обработчик меняет состояние приложения, а рендерер возвращает результат в игровой мир. Клик здесь срабатывает при входе в область кнопки; нажимать кнопку мыши не нужно. Механизм обработки этих событий описан в разделе об обработке действий игрока.
Данные приходят из Open-Meteo при запуске и обновляются каждые 15 минут. Пока идёт первый запрос, отображается состояние загрузки. Если связь пропадает, приложение сохраняет последний прогноз на экране, показывает пометку об ошибке и повторяет запрос через минуту.
В коде интерфейс разделяется на компоненты: заголовок, сводка погоды, отдельные показатели и навигация. Сервис отвечает за данные, а кнопки отправляют события родителю. На этом приложении можно проверить и отрисовку текста с картинками, и обновление привязок, и взаимодействие с игроком.
Примеры Angular-компонентов
Например, отдельный показатель погоды оформляется обычным компонентом с двумя входными параметрами. Ни команд Minecraft, ни вызовов Yoga в нём нет:
import { Component, Input } from '@angular/core';
@Component({
selector: 'weather-metric',
host: { style: 'flex: 1; gap: 6px; justify-content: center' },
template: `
<div style="font-size: 16px; color: white_concrete">{{ label }}</div>
<div style="font-size: 28px; color: white_wool">{{ value }}</div>
`,
})
export class WeatherMetricComponent {
@Input() label = '';
@Input() value = '';
}
Родитель передаёт подпись и значение, а компонент определяет их оформление. Особенность этой платформы видна в стилях: вместо CSS-классов используются поддерживаемые inline-свойства, а цвет можно указать именем блока.
В корневом шаблоне сводка и навигация подключаются так. Это фрагмент страницы без заголовка и состояний загрузки и ошибки:
@if (day(); as forecast) {
<weather-summary [day]="forecast"></weather-summary>
}
<weather-day-navigation
[index]="index()"
[count]="weather.days().length"
(step)="move($event)">
</weather-day-navigation>
Навигация получает номер выбранного дня и длину прогноза. Кнопки внутри неё отправляют step.emit(-1) или step.emit(1). В родительском компоненте событие обрабатывает метод:
move(delta: number): void {
const next = this.weather.days()[this.index() + delta];
if (next) this.selectedDate.set(next.date);
}
selectedDate хранится в signal, а index и day вычисляются через computed. Изменение выбранной даты обновляет данные, переданные в сводку, и значения привязок в её шаблоне.
Сама кнопка тоже является Angular-компонентом. Ниже сокращённая версия McButtonComponent для цветов используются фиксированные значения, а обработка событий остаётся без изменений.
import { Component, EventEmitter, Input, Output, signal } from '@angular/core';
import { MinecraftUiEvent } from '../minecraft/minecraft-platform';
@Component({
selector: 'mc-button',
template: '<ng-content></ng-content>',
host: {
'style': 'padding: 10px 18px; font-size: 18px',
'[style.background]':
'disabled ? "gray_concrete" : hovered() ? "cyan_concrete" : "black_concrete"',
'[style.color]': 'disabled ? "light_gray_wool" : "white_wool"',
'(click)': 'onClick($any($event))',
'(mouseenter)': 'hovered.set(true)',
'(mouseleave)': 'hovered.set(false)',
},
})
export class McButtonComponent {
@Input() disabled = false;
@Output() pressed = new EventEmitter<MinecraftUiEvent>();
hovered = signal(false);
onClick(event: MinecraftUiEvent): void {
event.stopPropagation();
if (this.disabled) return;
this.pressed.emit(event);
}
}
При входе игрока в область элемента меняется hovered, что обновляет цвет фона через привязку. Событие click вызывает onClick, а pressed передаёт нажатие родителю. Отключённая кнопка событие pressed не отправляет. Вся логика определения позиции игрока остаётся в MinecraftInput, поэтому компонент работает с готовыми событиями.
Заключение
Собственная платформа и реализация Renderer2 позволяют отображать Angular-приложение в Minecraft, сохраняя компоненты, привязки и обработчики событий. Приложение выполняется в Node.js, Yoga рассчитывает расположение элементов, а рендерер пребразует интерфейс в команды для block_display.
На примере прогноза погоды реализован полный цикл взаимодействия: данные сервиса появляются на игровом экране, перемещение игрока вызывает события кнопок, а изменение состояния Angular обновляет изображение. Автоматическая пересборка позволяет проверять изменения шаблонов и компонентов непосредственно в Minecraft.
Для такого вывода необходимо самостоятельно обеспечить расчёт геометрии, растеризацию и ввод. Поэтому совместимость ограничивается набором поддерживаемых стилей и возможностей рендерера. При этом логика погодного приложения остаётся на уровне Angular-компонентов, которым не требуется отправлять команды Minecraft или рассчитывать мировые координаты.

Очень рад что всё получилось! Приложение ничего не знает о Minecraft и тем не менее успешно работает!

Посмотреть результат и переключение прогноза в игровом мире можно в видео на YouTube.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.