Иконка ресурса

GExt - The layout engine for Minecraft

Нет прав для скачивания
Версия(и) Minecraft
1.7.10,1.12-1.19
АКТУАЛЬНО ДЛЯ GExt v.1.5.1.0-SNAPSHOT

Полезные ссылки:
GitHub
GitHub Wiki(English)

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

Библиотека доступна в Maven. Пока что не было первого релиза, так что живём на снапшотах. Чтобы заполучить GExt в свой Gradle-проект, добавьте репозиторий снапшотов:
Gradle (Groovy):
repositories {
    maven {
        url = "https://oss.sonatype.org/content/repositories/snapshots/"
    }
}
и зависимость:
Gradle (Groovy):
implementation group: 'com.github.stannismod.gext', name: <имя_адаптера>, version: <версия>
Например,
Gradle (Groovy):
implementation group: 'com.github.stannismod.gext', name: 'forge112', version: '1.5.1.0-SNAPSHOT'

Сразу отмечу, что, прежде чем читать этот тутор, стоит заглянуть в тему ресурса. Вероятно, это избавит вас от части вопросов.

Шаг первый - Сборка

Если вы скачали из maven, то смело переходите к следующему.

Клоним репозиторий и дожидаемся сборки Gradle проекта. Если у вас слабый компьютер и/или вам нужна библиотека "вот прям щас" и для одной-единственной версии - можно в файле settings.gradle отключить ненужные версии. Для версий до 1.13 необходимо выполнить задачу setupDecompWorkspace в Gradle. После этого выполните build для каждой интересующей версии и заберите deobf-версию библиотеки из директории compiled.

Шаг второй - Основной класс

В туториале будем рассматривать Minecraft GUI версии 1.12, на версиях ниже API почти не отличается. На версиях выше возможны большие отличия в API основных GUI классов Minecraft, которые влекут изменения API адаптеров библиотеки - но только их. Вы найдёте знакомые вашей версии методы в классах Extended*.

Несколько стандартных примеров размещено в классе com.github.stannismod.forge*version*.GuiTest

Для начала нам потребуется корневой контейнер - в библиотеке предусмотрены расширения для GuiScreen, GuiContainer и Gui. Унаследуйте интерфейс от одного из них, их работа с точки зрения библиотеки абсолютно одинакова. Я возьму GuiScreen, точнее, его Extended версию:
Основной класс:
public class GuiTest extends ExtendedGuiScreen {

    @Override
    public void init() {
        // здесь мы будем создавать нашу структуру компонентов
    }
}

Библиотека создаётся с целью дать максимальное удобство создания компонентов. За это отвечает класс Graphics, предоставляющий доступ к фабрикам компонентов. Создание компонентов всегда проходит следующим образом: вызов соответствующего метода у Graphics, установка нужных параметров, доступных в фабрике, вызов build() для завершения сборки и получения собранного компонента. После этого его с помощью методов add можно добавить в любой контейнер

Теперь, вероятно, мы хотим туда что-то добавить. Самое простое, что мы можем, когда у нас ничего нет - это добавить надпись. Так и сделаем. Добавим в init():
Пишем hello world:
GLabel label = Graphics.label()   // создаём надпись
.text("Hello, world!")            // устанавливаем текст
.placeAt(50, 50)                  // размещаем на координатах (50, 50)
.build();                        // собираем компонент

this.add(label);                   // добавляем собранный компонент в корневой контейнер
В итоге при открытии Gui получаем следующее:
2021-10-16_23.22.54.png

Просто, не правда ли? Вот и я так думаю. Все компоненты собираются похожим образом. Сделаем что-то посложнее, например, ссылку.
Оставляем ссылки:
GLink link = Graphics.link()                       // создаём ссылку
.text("GExt original source")                    // устанавливаем текст
.url("https://github.com/StannisMod/gext")       // ссылку
.color(0xffffff, 0x121212)                         // активный и пассивный цвет
.scale(2.0F)                                       // увеличение
.placeAt(250, 200)                                 // размещаем на координатах (250, 200)
.build();                                          // собираем компонент

this.add(link);                                    // добавляем собранный компонент в корневой контейнер
2021-10-16_23.24.24.png
По клику ссылка окрашивается белым и происходит переход по url.
Под конец построим сложный компонент с множеством настраиваемых параметров. Думаю, уже можно не комментировать каждую строчку:
Собираем кнопку:
this.add(100, Graphics.button()  // поставим глубину 100, чтобы кнопка была поверх всего остального
        .label(Graphics.label().text("Primary text", 0xffffff).scale(2.0F).setCentered().build())
        .action(button -> {
            if (button.getLabel().getText().startsWith("P")) {
                button.getLabel().setText("SecondaryText");
            } else {
                button.getLabel().setText("PrimaryText");
            }
        })
        .size(150, 60)
        .placeAt(800, 400)
        .build());
2021-10-16_23.25.27.png

2021-10-16_23.25.32.png
Дальнейшие шаги необязательны для реализации, но тем не менее раскрывают потенциал системы и могут быть интересны пользователям для создания сложных функциональных графических интерфейсов.

Шаг третий - Инструменты композиции

Ради чего и создавалась библиотека - вашему вниманию представлены мощные(но, возможно, пока немногочисленные) инструменты композиции. Другими словами, это контейнеры - компоненты, занимающиеся организацией других. В частности, они особым для себя образом располагают добавляемые компоненты. Итак, на момент написания существуют:
  • GPanel - обыкновенный контейнер, никак не преобразующий координаты компонентов. Можно добавить ScrollHandler - компонент, отвечающий за взаимодействие посредством прокрутки. Существуют варианты вертикальных и горизонтальных скроллбаров, а также прокрутка по колёсику мыши.
  • GList - расширение GPanel, располагающее компоненты вертикально или горизонтально в ряд, а также дающее доступ к ним по индексу. По сути, является GUI-представлением списка.
  • GTabList - расширение GList, имеющее своей целью управление составом целевого контейнера. То есть это стандартный таблист - нажатие на вкладку переключает контент целевого контейнера.
Добавление в контейнер происходит с помощью метода addComponent независимо от типа контейнера.

Например, при добавлении к Gui кода
Первый в жизни контейнер:
final GPanel<IGraphicsComponent> panel = Graphics.panel().size(500, 500).placeAt(100, 100).build();
this.add(panel);

this.add(Graphics.label().text("The first perfect text should be here").placeAt(300, 100).setCentered().build());
this.add(Graphics.label().text("The second perfect text should be here").placeAt(800, 100).setCentered().build());
panel.addComponent(Graphics.background().size(400, 200).build());
panel.addComponent(10, Graphics.label().text("This text should be rendered", 0xffffff).placeAt(200, 200).setCentered().build());
panel.addComponent(Graphics.label().text("But this shouldn't, because it's out of bounds", 0xffffff).placeAt(800, 200).setCentered().build());
Весь контент, добавленный в контейнер panel, будет сдвинут на (100, 100) и ограничен в размерах рамкой (500, 500):
2021-10-16_23.36.23.png


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

Относительная вёрстка

Component Binding

Кроме контейнеров и группировки по принципу иерархии, есть ещё один способ задать относительное позиционирование. Называется он binding(привязка). Вызовом метода setBinding на компоненте либо bind на фабрике вы осуществляете "привязку" - координаты, присвоенные вами компоненту, теперь считаются не от родителя, а от точки привязки. К примеру, таким кодом можно получить почти ту же картину, что и использованием контейнера GList:
Создаём равномерную цепочку надписей:
final GPanel<GLabel> panel = Graphics.<GLabel>panel().size(500, 500).placeAt(500, 300).build();

GLabel prev = null;
for (int i = 0; i < 10; i++) {
    panel.addComponent(prev = Graphics
                       .label()
                       .text("Label " + i, Color.WHITE.getRGB())
                       .placeAt(0, 10)
                       .bind(prev)
                       .build());
}

add(panel);
1639769252533.png


Alignments - привет из веба

И напоследок третий инструмент относительной вёрстки, пришедший к нам напрямую из веб-программирования - Alignments.
Грубо говоря, это способ позиционирования относительно родителя в терминах "сверху/снизу/сбоку/посередине". Обратите внимание - данная опция не совместима с Component Binding. Из двух инструментов для каждого компонента может быть применён лишь один(но при этом никто не мешает для одного компонента применить Component Binding, а для другого Alignment). Переходя к примерам, следующий код:
Java:
final GPanel<IGraphicsComponent> labels = Graphics.panel().size(400, 400).placeAt(100, 100).build();

        // добавим фон шириной в весь контейнер для красоты
        labels.addComponent(Graphics.background().size(400, 400).build());

        // на самом деле этот код может быть короче, но он так отформатирован ради читаемости
        labels.addComponent(Graphics
                .label()
                .text("Top", Color.WHITE.getRGB())
                .placeAt(0, 0)
                .alignment(Alignment.XCENTER, Alignment.TOP)
                .padding(50, 50)
                .build());

        labels.addComponent(Graphics
                .label()
                .text("Bottom", Color.WHITE.getRGB())
                .placeAt(0, 0)
                .alignment(Alignment.XCENTER, Alignment.BOTTOM)
                .padding(50, 50)
                .build());

        labels.addComponent(Graphics
                .label()
                .text("Left", Color.WHITE.getRGB())
                .placeAt(0, 0)
                .alignment(Alignment.LEFT, Alignment.YCENTER)
                .padding(50, 50)
                .build());

        labels.addComponent(Graphics
                .label()
                .text("Right", Color.WHITE.getRGB())
                .placeAt(0, 0)
                .alignment(Alignment.RIGHT, Alignment.YCENTER)
                .padding(50, 50)
                .build());

        labels.addComponent(Graphics
                .label()
                .text("Center", Color.WHITE.getRGB())
                .placeAt(0, 0)
                .alignment(Alignment.XCENTER, Alignment.YCENTER)
                .padding(50, 50)
                .build());

        add(labels);
на выходе даёт сие:

1639863974552.png


Добавление прокрутки

Это сделано максимально доступным образом, по моему мнению. Все компоненты, реализующие интерфейс IScrollable, поддерживают прокрутку контента. Без лишних слов, такой код:
Java:
@Override
public void initLayout() {
    final GList<GLabel> panel = Graphics.<GLabel>list().size(50, 200).placeAt(500, 300).build(); // создали список
    panel.setScrollHandler(Controls.verticalScroll().barWidth(8).scrollFactor(0.25F).build());  // установили ему вертикальный скролл
    for (int i = 0; i < 1000; i++) {   // добавили надписей
        panel.addComponent(Graphics.label().text("Label " + i, Color.WHITE.getRGB()).build());
    }

    this.add(panel);   // присоединили к интерфейсу
}
В игре отображается вот так:
1639218257451.png


Шаг четвёртый - Создание своего компонента

Притаскивая библиотеку на свой проект, вам наверняка рано или поздно захочется больше возможностей, чем можно реализовать стандартными средствами GExt. В таком случае выход - написать свой компонент. Плюс библиотеки в том, что, реализовав в нём интерфейс IGraphicsComponent, вы сделаете свой компонент управляемым с точки зрения библиотеки. Все компоненты, в том числе контейнеры, смогут работать с вашим компонентом, как и с любым другим. Вы можете сосредоточиться на решении своей задачи в компоненте, не отвлекаясь на организацию GUI. Стандартная реализация базовых функций GUI-компонента представлена в классе GBasic, крайне рекомендуется наследование от него или его потомков. Реализация впрямую интерфейса IGraphicsComponent, конечно, возможна, но сложно представить ситуацию, в которой это необходимо.

Также можно создать интерактивное контекстное меню для компонента. Для этого переопределите метод constructMenu в компоненте. За создание компонентов меню отвечает класс MenuBuilder. Пример задания меню можно наблюдать ниже:
Java:
@Override
public IContextMenuList<? extends IContextMenuElement> constructMenu() {
    return MenuBuilder.<GTextBox>create(80)
        .point("Point", (c, p) -> System.out.println("Point"))
        .point(Icon.APPROVE, "Point with item", (c, p) -> System.out.println("Point 1"))
        .newList(Icon.DECLINE, "Just list 1", 70)
            .point(Icon.CHECKBOX, "List 1 label 1", (c, p) -> System.out.println("Point 1/1"))
            .point(Icon.CHECKBOX, "List 1 label 2", (c, p) -> System.out.println("Point 1/2"))
        .endList()
        .newList(Icon.DECLINE, "Just list 2", 70)
            .point(Icon.CHECKBOX, "List 2 label 1", (c, p) -> System.out.println("Point 2/1"))
            .point(Icon.CHECKBOX, "List 2 label 2", (c, p) -> System.out.println("Point 2/2"))
            .newList(Icon.CHECKBOX, "List 2 sublist 1", 100)
                .point(Icon.CHECKBOX, "List 2 sublist 1 label 1", (c, p) -> System.out.println("Point 2/2/1"))
                .point(Icon.CHECKBOX, "List 2 sublist 1 label 2", (c, p) -> System.out.println("Point 2/2/2"))
            .endList()
        .endList()
        .point(Icon.APPROVE, "Point with item", (c, p) -> System.out.println("Point 2"))
        .build();
}
Существует всего два вида компонентов меню - пункты и списки. У пункта можно задать иконку, надпись и действие, исполняемое при нажатии. У списка - иконку, надпись и ширину.

Ресурсы

С версией 1..5 в GExt приходит своя абстрактная ресурсная модель. Она представлена двумя интерфейсами - IResource и ITexture.

Преобразование ResourceLocation в IResource/ITexture

Java:
ResourceLocation loc = new ResourceLocation("textures/image.png");
IResource resource = GExt.resource(loc.toString());
ITexture tex = resource.toTexture();

Note: Здесь указан способ перевода ResourceLocation, ссылающегося на ресурсы мода. В других случаях универсального перевода нет, так как по факту ResourceLocation даже не ресурс, а просто имя ресурса, и для использования ресурса его надо загрузить. Так что для использования в GExt придётся опираться на его ресурсную модель, либо же написать универсальный адаптер к Minecraft-ресурсам. Можно либо напрямую реализовать IResource, либо же расширить ResourceImpl и переопределить метод getInputStream, отвечающий за доступ к ресурсу. Я же вижу такую ситуацию весьма экзотической, поэтому адаптеры под Minecraft ресурсы в поставке отсутствуют.

Ресурс-провайдеры

Кроме GExt.resource() есть ещё масса способов получать ресурсы. Эти способы зовут ресурс-провайдерами.

Сейчас доступны:
  • AssetsResourceProvider - провайдер ассетов внутри JAR. Грубо говоря, то, что в resources мода лежит. Пример использования в пункте выше.
  • FilesystemResourceProvider - провайдер файловой системы. Можно загружать ресурсы из внешней файловой системы(не JAR). Пример:
Java:
IResourceProvider provider = new FilesystemResourceProvider("Test", Paths.get("resources"));
ITexture tex = provider.getResource("test", "image.png").toTexture();
this.add(Graphics.image()   // отображается картинка, лежащая по пути /resources/test/image.png относительно папки запуска Minecraft
               .texture(tex)
               .uv(0, 0, 200, 50)
               .size(200, 200)
               .placeAt(200, 200)
               .build());
  • NetworkResourceProvider - TODO

В документации к пакету com.github.stannismod.gext.api можно найти исчерпывающую информацию о функциях API, средствах, гарантиях и прочем.
Есть некоторые фичи, касающиеся некоторым образом модификации библиотеки.
1. Портирование библиотеки на любую платформу, использующую OpenGL
Модульная архитектура была создана для того, чтобы максимально облегчить встраивание библиотеки в любую графическую подсистему, использующую OpenGL. Таким образом, для портирования в любую игру(в том числе и на любую версию Minecraft) на самом деле нужно лишь портировать пакет любого адаптера в вашу платформу. Если платформа - не Minecraft(внезапно), то алгоритм создания адаптера с нуля следующий:
  • Реализуем все классы из com.github.stannismod.gext.api.adapter
  • Реализуем все адаптеры к нужным классам GUI с помощью IRootLayout
  • При инициализации платформы вызываем конструктор класса com.github.stannismod.gext.GExt.
Этого достаточно для инициализации библиотеки и подключения всех модулей. Билдскрипт также можно скопировать из пакетов адаптеров, вырезав оттуда всё, что относится к Minecraft и добавив своё.
2. Создание корневых контейнеров
В библиотеке у каждого компонента есть родитель. Но по факту такого быть не может - у иерархии обязан быть какой-то корень. Так вот, интерфейс IRootLayout предоставляет возможность реализовать корневой контейнер с минимальными временными затратами. Отдельный интерфейс и реализация для корневого контейнера сделана специально, потому что фактически он является адаптером контейнера верхнего уровня к системе конечной реализации, которая, скорее всего, имеет свой класс для представления объектов GUI.
Автор
Quarter
Скачивания
14
Просмотры
1,807
Первый выпуск
Обновление
Оценка
5.00 звёзд 1 оценок

Последние рецензии

Спасибо за инструмент!
Сверху