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

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

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

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

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

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

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

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

1554666217002.png

1554666240309.png

Credits:
Спасибо @Dahaka за обсуждение некоторых вопросов
Спасибо @Agravaine :j за годную идею о реализации страничек текста, хотя она не используется

==== 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_координата - точка[I]крепления[/I]элемента

Поэтому структурному элементу добавляю свойство 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)/[I]по центру страницы[/I]/, bottom(0)) {
    @Override
    public Vec2i fixPoint() {
        return new Vec2i(width() / 2, height());/[I]точка фиксации по x в центре элемента[/I]/
    }
}
1555266416415.png


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

====4====
Рендер картинок

В этой заметке речь пойдет о странице ImagePage. Страница состоит из ImageView и TestLine для подписи
Код:
private final ResourceLocation texture;
private final String label;

@Override
public List<IStructuralGuiElement> elements() {
    return ImmutableList.of(
        new ImageView(texture, new Rectangle(5, 5, BookApi.pageWidth - 10, BookApi.pageHeight - 20)),
        new TextLine(label, centerX(0), bottom(0)) {
            @Override
            public Vec2i fixPoint() {
                return new Vec2i(width() / 2, height());
            }
        }
    );
}
ImageView должна рисовать картинку texture в пределах прямоугольника - второго аргумента.
На момент третьей заметки ее рендер выглядел так:
Код:
@Override
public void render(int mouseX, int mouseY) {
    mc().getTextureManager().bindTexture(texture);
    drawTexturedModalRect(rectangle.getX(), rectangle.getY(), 0, 0, rectangle.getWidth(), rectangle.getHeight());
}
На скрине из прошлой заметке вы могли видеть, что картинка рисуется только куском. Это означает, что неправильно выставлены u/v для вершин.
Воспользуемся drawScaledCustomSizeModalRect:
Java:
drawScaledCustomSizeModalRect(rectangle.getX(), rectangle.getY(),
                0, 0, // u,v
                1, 1, // uw,vh
                rectangle.getWidth(), rectangle.getHeight(), // w,h
                1, 1);//tileSizes
Последние два аргумента позволяют замостить картинкой. Нам нужна только одна копия картинки в прямоугольнике, поэтому передает 1,1

Это делает почти то, что нам нужно
1560867421663.png

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

Для этого нам нужны размеры картинки.
Чтобы получить правильные размеры произвольной картинки, заданной ResourceLocation, нам нужно получить ее AtlasSprite:
Код:
this.atlas = Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite(texture.toString());

Делаем кастомный метод рисования вписанной текстуры
public static void drawInscribedCustomSizeModalRect(int x, int y, int width, int height, float textureWidth, float textureHeight)
В нем нужно вычислить новые размеры прямоугольника, которые будут соответствовать вписанной текстуре
Java:
//Для начала определим отношение сторон текстуры
float ratio = textureWidth / textureHeight;
//После можем описать новую ширину
int newWidth = min(width, (int) (ratio * height));
//И новую высоту
int newHeight = min(height, (int) (1f / ratio * width));
Вызов функции Math#min нужен чтобы ограничить новые размеры размерами прямоугольника
1560869910364.png

Теперь осталось скорректировать позицию x,y
Java:
int ny = y + (height - newHeight) / 2;
int nx = x + (width - newWidth) / 2;
1560870062769.png

Теперь текстурка красиво вписывается в прямоугольник

После рендера текста остается его цвет.
Поэтому мы обнуляем устанавливаем цвет на белый.
1560870379075.png


Полный код
Java:
@Override
public void render(int mouseX, int mouseY) {
    GlStateManager.color(1, 1, 1, 1);
    mc().getTextureManager().bindTexture(texture);
    drawInscribedCustomSizeModalRect(rectangle.getX(), rectangle.getY(),
                                     rectangle.getWidth(), rectangle.getHeight(),
                                     atlas.getIconWidth(), atlas.getIconHeight());
}

public static void drawInscribedCustomSizeModalRect(int x, int y, int width, int height, float textureWidth, float textureHeight) {
    float ratio = textureWidth / textureHeight;
    int newWidth = min(width, (int) (ratio * height));
    int newHeight = min(height, (int) (1f / ratio * width));

    int ny = y + (height - newHeight) / 2;
    int nx = x + (width - newWidth) / 2;

    drawScaledCustomSizeModalRect(nx, ny, 0, 0,
                                  1, 1,
                                  newWidth, newHeight,
                                  1, 1);
}


На этом заметка заканчивается, надеюсь, вам понравилось)
  • 1560870372076.png
    1560870372076.png
    30.9 KB · Просмотры: 149
Автор
hohserg
Скачивания
11
Просмотры
1,880
Первый выпуск
Обновление
Оценка
5.00 звёзд 1 оценок

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

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

  1. Четвертая заметка!

    Рассказываю о рендере картинок Немного изменено оформление
  2. Третья заметка!

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

    Рассказываю про апи и гуи

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

Отличный гайд!
~~~
А можно использовать эти наработки в других проектах?
hohserg
hohserg
Разумеется, если лицензия вашего проекта совместима с GNU GPL v3.0, по которой опубликован код Ender'sMagic, пример которого используется в туторе
Сверху