Заметки о разработке книги-внутригировой-вики

Заметки о разработке книги-внутригировой-вики

Нет прав для скачивания
Версия(и) Minecraft
1.12.2
Те, кто играл на различных сборках с модами знает, что во многих модах есть книжки с инфой о моде. Например, лексика из ботании, таумономиком из таумкрафта или "материалы и вы" из тинкерса.

Данный ресурс будет повествовать о процессе разработки подобной книги в особом формате периодически выходящих заметок. Что-то похожее блог.
По завершению разработки первого релиза будет некоторое summary.
Назову это тутор в раннем доступе.

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

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

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

Каждая заметка метится порядковым номером.

1554666217002.png

1554666240309.png

Credits:
Спасибо @Dahaka за обсуждение некоторых вопросов

==== 1 ====
Для начал расскажу подробнее о задаче. Требуется создать апи и гуи для мода Ender'sMagic.
Книга делится на главы, открыть любую главу можно со страницы содержания. Каждая глава представляет из себя последовательность страниц.
Апи должно предоставлять удобный способ наполнения книги контентом. Например, если нужно добавить в главу текст, то разрезание на страницы и строки должно происходить автоматически.

Поэтому я решил при первом приближении сделать такую архитектуру:
Java:
IChapterComponent
        |
        V
      IPage
        |
        V
IStructuralGuiElement
  • С точки зрения добавления контента глава представляет из себя список IChapterComponent - кусков главы, которые не обязательно соответствуют одной странице
  • Каждая страница(IPage) предоставляет список структурных элементов
  • IStructuralGuiElement - структурный элемент, примитив для построения страниц. Например, строка текст, или картинка
Эти интерфейсы в коде:
Java:
public interface IChapterComponent {
    List<IPage> pages();
}
public interface IPage {
    List<IStructuralGuiElement> elements();
}
public interface IStructuralGuiElement {
    default Minecraft mc() {// вспомогательный метод, чтобы каждый раз не писать Minecraft.getMinecraft()
        return Minecraft.getMinecraft();
    }

    void render(int mouseX, int mouseY);
}
Определившись с этим, я начал писать реализации для этих интерфейсов.

Первым пошел TextComponent.
Он создается из строки текста и предоставляет список страниц с текстом.
Функция, режущая цельный текст на линии и страницы была выполнена сначала при помощи stream#reduce(посмотреть можно в этом коммите) Это в принципе работало, но было пару неочевидных багов, из-за которых отказался от этого решения в пользу итератора.

С итератором концепция получилась довольно простой:
Имея текст в виде списка слов итератор может отдавать очередную линию текста пока еще впереди есть слова и каждая линия текста наполняется пока она по ширине влезает в страницу
Java:
public class GroupIterator<A> implements ListIterator<List<A>> {
    private final ListIterator<A> list;
    private final Integer max;
    private final Function<A, Integer> size;

    public GroupIterator(ListIterator<A> list, Integer max, Function<A, Integer> size) {
        this.list = list;
        this.max = max;
        this.size = size;
    }

    @Override
    public boolean hasNext() {
        return list.hasNext();
    }

    @Override
    public List<A> next() {
        List<A> acc = new ArrayList<>(10);
        Integer accSize = 0;

        while (accSize < max && list.hasNext()) {
            A next = list.next();
            acc.add(next);
            accSize += size.apply(next);
        }

        if (list.hasPrevious()) {
            acc.remove(acc.size() - 1);
            list.previous();
        }

        return acc;
    }
С этим итератором первый раз за 4 года вылетело OutOfMemory, xD. Дело было в том, что я не учел кейс когда последняя линия текста уже влезает в строку и для нее не нужно убирать последнее слово назад(иначе это слово навсегда застревает в итераторе).
Решение:
Java:
if (accSize > max && list.hasPrevious()) {
    acc.remove(acc.size() - 1);
    list.previous();
}
Запустим тесты в консольке:
Использую для этого scala repl, потому что удобно для маленьких тестов
1554665041097.png

Результат: превышающих длину линий нет
1554665143202.png


Теперь юзаем итератор в TextComponent
Java:
private ImmutableList<IPage> buildPagesForFont(FontRenderer font) {
    font.setUnicodeFlag(true);
    String[] words = text.split("\\s+");

    int spaceWidth = font.getStringWidth(" ");

    GroupIterator<String> lines = new GroupIterator<>(Arrays.asList(words).listIterator(), lineSize, w -> font.getStringWidth(w) + spaceWidth);

    GroupIterator<List<String>> pages = new GroupIterator<>(lines, pageSize, __ -> 1);

    return StreamSupport.stream(Spliterators.spliteratorUnknownSize(pages, Spliterator.ORDERED), false)
        .map(page ->
             new TextPage(page
                          .stream()
                          .map(line -> String.join(" ", line))
                          .collect(ImmutableList.toImmutableList())))
        .collect(ImmutableList.toImmutableList());
}
Одним итератором группируем слова, другим группируем линии в страницы, в конце собираем это все в списки страниц
Применяем в конструкторе
Java:
public TextComponent(String unlocalizedText) {
    text = I18n.format(unlocalizedText);
    pages = buildPagesForFont(Minecraft.getMinecraft().fontRenderer);
}
==== 2 ====
Апи и отрисовка
Главный класс апи содержит статический метод, позволяющий добавлять главы
Java:
public static void addChapter(String name, List<IChapterComponent> content) {
    book.put(name, flatMapToPages(content.stream()));
}
Единственное что он делает - подготавливает страницы и пихает их в мапу
Подготовка происходит таким образом:
Сначала компоненты flatMap-юся в страницы
1554915496588.png


Потом страницы по две объединяются в пары(потому то на одном развороте две страницы), в качестве пары используется PageContainer
1554916308175.png


Потом создаются связи между соседними разворотами
1554916152315.png


И возвращается первый контейнер
github/EnderMagic/BookApi#flatMapToPages

Чтобы листать страницы нужно просто переходить по ссылкам контейнеров влево и вправо

Гуи - github/EnderMagic/GuiScreenEMBook
Хранит текущий контейнер страниц
Основное действо происходит в drawScreen
Java:
private static int bookFullWidth = 256;
private static int bookFullHeight = 192;


public void drawScreen(int mouseX, int mouseY, float partialTicks) {
    GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
    mc.getTextureManager().bindTexture(BOOK_TEXTURES);
    int i = (width - bookFullWidth) / 2;
    int j = (height - bookFullHeight) / 2;
    drawTexturedModalRect(i, j, 0, 0, bookFullWidth, bookFullHeight);//Сначала отрисовывается фон

    drawPage(i + 20, j + 15, currentPage.page1, mouseX, mouseY);//Потом страницы
    drawPage(i + bookFullWidth / 2 + 4, j + 15, currentPage.page2, mouseX, mouseY);


    GlStateManager.disableRescaleNormal();
    RenderHelper.disableStandardItemLighting();
    GlStateManager.disableLighting();
    GlStateManager.disableDepth();
    super.drawScreen(mouseX, mouseY, partialTicks);//Потом кнопки
    RenderHelper.enableGUIStandardItemLighting();
    GlStateManager.enableRescaleNormal();
}

private void drawPage(int i, int j, IPage page, int mouseX, int mouseY) {
    GlStateManager.pushMatrix();

    GlStateManager.translate(i, j, 0);
    page.elements().forEach(e -> e.render(mouseX - i, mouseY - j));//Отрисовываем все элементы

    GlStateManager.popMatrix();
}
Элементы считают началом координат верхний левый угол страницы

1554918213242.png

Области страниц для отрисовки компонентов​

Также, некоторые элементы могут быть кликабельны(например, ссылки на главы в содержании)
Java:
    protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException {
        super.mouseClicked(mouseX, mouseY, mouseButton);
        int i = (width - bookFullWidth) / 2;
        int j = (height - bookFullHeight) / 2;
        if (mouseX < i + bookFullWidth / 2)
            performClickOnPage(mouseX, mouseY, i + 20, j + 15, currentPage.page1);
        else
            performClickOnPage(mouseX, mouseY, i + bookFullWidth / 2 + 4, j + 15, currentPage.page2);
    }
Очень хочется как-нить вынести (i + 20, j + 15) и (i + bookFullWidth / 2 + 4, j + 15), координаты верхних левых углов левой и правой страницы, но как это сделать по нормальному до сих пор придумать не удалось

Чтобы листать страницы сделал 3 кнопки навигации
Java:
buttonBack = addButton(new BackButton(0, width / 2 - 11, j + 160));

buttonNextPage = addButton(new NextPageButton(1, i + 215, j + 160, true));
buttonPreviousPage = addButton(new NextPageButton(2, i + 18, j + 160, false));
При клике по кнопке происходит смена страницы
Java:
protected void actionPerformed(GuiButton button) {
    if(button instanceof PageButton)
        setCurrentPage(((PageButton) button).goToPage.get());
}
PageButton - базовый класс кнопки, которая имеет Supplier<Optional<PageContainer>>
В наследниках это свойство устанавливается через конструктор
Java:
public NextPageButton(int button, int x, int y, boolean isForward) {
    super(button, x, y, 0, isForward ? bookFullHeight : bookFullHeight + 13,
          isForward ? () -> GuiScreenEMBook.instance.currentPage.right : () -> GuiScreenEMBook.instance.currentPage.left);
}
Используется именно Supplier, потому что GuiScreenEMBook.instance.currentPage меняется при пролистывании

При изменении страницы вызывается updateButtons, который устанавливает видимость кнопок в зависимости от доступности переходов
Java:
private void updateButtons() {
    buttonPreviousPage.visible = currentPage.left.isPresent();
    buttonNextPage.visible = currentPage.right.isPresent();
    buttonBack.visible = currentPage != BookApi.mainPage();
}
==== 3 ====
Выравнивание

Потребность в выравнивании появилась при создании компонента картинки. У картинки есть подпись, которая должна быть под картинкой и отцентрована по ширине.

Формула центрирования такая:

x = pageWidth/2 - elementWidth/2

Первая часть формулы означает, что элемент будет размещен в центре страницы. Вторая часть означает, что в качестве точки отсчета координат элемента(точки крепления) используется середина элемента.

Поэтому эту формулу можно обобщить до вида
x = pageWidth * uv_like_x_координата - точка_крепления_элемента

Поэтому структурному элементу добавляю свойство fixPoint, определяющее точку крепления элемента и свойства width и height.
Java:
default Vec2i fixPoint() {
    return new Vec2i(0, 0);
}
По дефолту в качестве fix point используется условно верхний левый угол(условно ,потому что метод render можно реализовать как угодно, в том числе рисовать че-то в отрицательных относительных координатах)

Также нужно обновить drawPage, чтобы применять fix point
Java:
private void drawPage(int i, int j, IPage page, int mouseX, int mouseY) {
    GlStateManager.pushMatrix();
    GlStateManager.translate(i, j, 0);
    
    page.elements().forEach(e -> {
        GlStateManager.translate(-e.fixPoint().x, -e.fixPoint().y, 0);
        e.render(mouseX - i, mouseY - j);
        GlStateManager.translate(e.fixPoint().x, e.fixPoint().y, 0);

    });

    GlStateManager.popMatrix();
}
Для удобства установки значения pageWidth * uv_like_x_координата сделал несколько методов:
Java:
public class Alignment {
    public static int min(double v,int dimension){return (int) (dimension * v);}
    public static int max(double v,int dimension){return (int) (dimension * (1 + v));}
    public static int center(double v,int dimension){return (int) (dimension * (0.5 + v));}

    public static int left(double v){return min(v, BookApi.pageWidth);}
    public static int right(double v){return max(v, BookApi.pageWidth);}
    public static int centerX(double v){return center(v, BookApi.pageWidth);}

    public static int top(double v){return min(v, BookApi.pageHeight);}
    public static int bottom(double v){return max(v, BookApi.pageHeight);}
    public static int centerY(double v){return center(v, BookApi.pageHeight);}
}
min,max,center - универсальные(для любой координаты) функции смещения
остальные - реализации для конкретных осей
Графически можно изобразить так:
1555267141391.png


Теперь, чтобы сделать текст выровненный по x по центру, по y в самом внизу нужно написать:
Java:
new TextLine(label, centerX(0)/*по центру страницы*/, bottom(0)) {
    @Override
    public Vec2i fixPoint() {
        return new Vec2i(width() / 2, height());/*точка фиксации по x в центре элемента*/
    }
}
1555266416415.png


Как мы видим, текст правильно позиционируется. Правда, картинка рендерится только куском, но это уже материал для одной из следующих заметок)

На этом заметка заканчивается, надеюсь, вам понравилось)
Автор
hohserg
Скачивания
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок

Другие ресурсы пользователя hohserg

Последние обновления

  1. Третья заметка!

    Рассказываю о позиционировании элементов гуи
  2. Вторая заметка!

    Рассказываю про апи и гуи
  3. Обновление введения

    Немного обновил введение, добавил creadits
Сверху