Minecraft и REPL: взаимодействие

от автора

История одной оптимизации во время мастер-класса Clojure Minecraft

В июне компания JUXT приняла участие в ClojureD, ежегодной замечательной конференции по Clojure в Берлине. В течение дня проводился ряд семинаров, на которых рассказывалось о конкретной идее или инструменте и предлагалось поработать с ними. Наша группа решила посетить семинар «Изменим мир (Minecraft) с помощью кода», который проводили Арне Брассер (Arne Brasseur), Ариэль Алекси (Ariel Alexi) и Фелипе Баррос (Felipe Barros). В этом посте рассказывается о том, как мы использовали полученные на семинаре знания для создания изображений в игре и оптимизировали код с помощью https://clojured.de/media/gallery/gallery-2022/nggallery/page/5)

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

Проект Witchcraft предоставляет удобный API для взаимодействия с серверами Minecraft на базе Bukkit. Именно он и использовался на семинаре.

Пробуем сами

Продемонстрировав свои навыки работы с Minecraft на основе REPL, Арне предоставил нам возможность самостоятельно попробовать и увидеть, что же у нас получится. После того как все было настроено и установлено, мы смогли запустить сервер, выполнить подключение к нему с помощью клиентского интерфейса и зайти в REPL с помощью CIDER. В репозитории воркшопа есть четыре пространства имен, которые демонстрируют различные способы использования Witchcraft при организации работы сервера и мира Minecraft.

Одной из наиболее интересных функций, предоставляемых Witchcraft, является nearest-material, которая находит материал Minecraft, в наибольшей степени соответствующий заданному цвету RGB. Witchcraft предоставляет файл соответствий между материалами (*основные строительные единицы в игре, представляющие собой разнообразные блоки, предметы, ресурсы и детали, которые составляют игровой мир Minecraft) и наиболее характерными цветами, присутствующими в текстуре этого материала.

Для определения наиболее подходящего материала (или блока) в Minecraft, который будет наиболее близко соответствовать заданному значению RGB цвета, функция nearest-material вычисляет расстояние между этим цветом и цветами, которые являются наиболее характерными для текстур блоков в игре. 

Уилл Кейн (Will Caine) предложил блестящую идею считывать пиксели изображения и отображать их в мире Minecraft, используя данную функцию для определения наиболее подходящих материалов. Проведя небольшое исследование, нам удалось настроить библиотеку ImageIO на чтение нашего изображения, и вскоре у нас были значения RGB для каждого пикселя в файле!

Это выглядит как достаточно интересный проект, в рамках которого использовалась обработка изображений и их преобразование в игровой мир Minecraft. Значения RGB для каждого пикселя изображения могут быть использованы при определении наилучших материалов или блоков, которые должны быть размещены в Minecraft, чтобы создать визуальное представление этого изображения. Этот процесс может создавать уникальные и креативные искусственные структуры или объекты в игре на основе фактических изображений.

Генерация изображений в мире Minecraft

В игре Minecraft мир имеет ограничение по высоте — всего 319 блоков, от основания снизу до верхней грани. Это означает, что игровой мир ограничен по вертикали, и игроки могут строить или перемещаться только до вершины карты. Верхняя граница мира часто называется «sky limit» (пределом неба).

Если вы хотите внести изображение в игровой мир Minecraft, вам придется адаптировать его размеры под это ограничение. Поскольку высота мира определена 319 блоками, изображение должно быть уменьшено или масштабировано так, чтобы оно вписалось в заданную высоту. Это может потребовать изменения размеров и пропорций изображения, чтобы оно корректно отображалось в игровом мире Minecraft без нарушения вышеуказанных условий.

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

(ns gen-image   (:require [lambdaisland.witchcraft :as wc]             [lambdaisland.witchcraft.palette :as palette])   (:import (javax.imageio ImageIO)            (java.io File)            (java.awt Color)))  (defn img2world   [filename coords mc-width]   (let [buff (. ImageIO (read (File. filename)))         img-width (.getWidth buff)         img-height (.getHeight buff)         rgbvec         (for [x (range 0 img-width)]           (for [y (range 0 img-height)]             (let [rgbint (.getRGB buff x y)                   color (Color. rgbint true)]               [(.getRed color) (.getGreen color) (.getBlue color)])))         scale-factor (/ img-width mc-width)         mc-height (quot img-height scale-factor)]     (for [x (range 0 mc-width)           y (range 0 mc-height)]       (wc/set-block        (-> coords            (update :x + x)            (update :y + y)            (assoc :material                   (palette/nearest-material                    (nth (nth rgbvec (* x scale-factor)) (* y scale-factor)))))))))

После совместной работы над решением нам наконец-то удалось сгенерировать изображение в мире Minecraft. Но мы столкнулись с проблемой: оно оказалось перевернутым вверх ногами!

--- (nth (nth rgbvec (* x scale-factor)) (* y scale-factor))) --- +++ (nth (nth rgbvec (* x scale-factor)) (* (- mc-height 1 y) scale-factor) +++

Обычно в компьютерной графике и изображениях координатная система имеет начало в верхнем левом углу изображения. Это означает, что координата (0, 0) находится в верхнем левом углу изображения, а увеличение координат происходит по направлению вниз (по оси Y) и вправо (по оси X). 

При чтении изображения в буфер ImageIO мы ожидали, что начало координат будет в нижнем левом углу, и соответственно они будут увеличиваться по направлению вверх (по оси Y) и вправо (по оси X).

После инверсии координат Y мы смогли корректно отобразить изображение в Minecraft. 

Оптимизация по скорости

Несмотря на то, что наше решение работало, генерация результирующего изображения происходила крайне медленно. Первой мыслью было то, что все эти повторные вызовы wc/set-block могут замедлять работу, поэтому мы рефакторизовали код, чтобы использовать wc/set-blocks для установки всех блоков сразу. Использование wc/set-blocks также имеет дополнительное преимущество — отменять сгенерированные изображения с помощью wc/undo! гораздо проще, поскольку при этом удаляется все изображение, а не только один сгенерированный блок за раз.

(defn img2world   [filename coords mc-width]   (let [buff (. ImageIO (read (File. filename)))         img-width (.getWidth buff)         img-height (.getHeight buff)         scale-factor (/ img-width mc-width)         mc-height (quot img-height scale-factor)]        (wc/set-blocks         (for [x (range 0 mc-width)               y (range 0 mc-height)]           (let [rgbint (.getRGB buff (* x scale-factor) (* y scale-factor))                 color (Color. rgbint true)                 rgb [(.getRed color) (.getGreen color) (.getBlue color)]]             [x (- mc-height 1 y) 0 (palette/nearest-material rgb)]))         {:anchor coords})))

Но реализация решения по-прежнему оставалась медленной. Не наступил ли у нас некий жесткий предел? Для выяснения реальной ситуации и определения, что происходит в коде, мы решили провести его профилирование. Tufte — это простой профилировщик для Clojure и ClojureScript, поэтому он и был использован для анализа производительности кода. Мы добавили зависимость в deps.edn сервера и написали код профилирования. Для использования Tufte необходимо выделить (идентифицировать) участки кода, которые необходимо профилировать. Оборачиваем их в маркер (p). После того как участок кода был обрамлен маркером p и помещен внутрь профилировочной формы, вызываем функцию profile и передаем ей этот участок кода в качестве аргумента и наблюдаем за результатами.

(ns gen-image   (:require ...             [taoensso.tufte :as tufte :refer (defnp p profile)])     ...)  (defn img2world   [filename coords mc-width]   (let [buff (p :new-buff (. ImageIO (read (File. filename))))         img-width (.getWidth buff)         img-height (.getHeight buff)         scale-factor (/ img-width mc-width)         mc-height (quot img-height scale-factor)]        (p :set-blocks (wc/set-blocks         (for [x (range 0 mc-width)               y (range 0 mc-height)]           (let [rgbint (p :get-rgb (.getRGB buff (* x scale-factor) (* y scale-factor)))                 color (p :new-color (Color. rgbint true))                 rgb (p :rgb-vec [(.getRed color) (.getGreen color) (.getBlue color)])]             [x (- mc-height 1 y) 0 (p :near-mat (palette/nearest-material rgb))]))         {:anchor coords}))))  (tufte/add-basic-println-handler! {:format-pstats-opts {:columns [:n-calls :min :max :mean :clock :total]}})  (profile  {}  (p :img2world (img2world "juxt-logo.png" {:x 0 :y 150 :z 0} 200)))
pId             nCalls        Min        Max       Mean      Clock  Total      :img2world           1    34.15s     34.15s     34.15s     34.15s    100%     :set-blocks          1    34.14s     34.14s     34.14s     34.14s    100%     :near-mat       15,400     1.47ms    28.95ms     2.18ms    33.62s     98%     :get-rgb        15,400   963.00ns     6.83ms     7.39μs   113.86ms     0%     :rgb-vec        15,400   124.00ns    42.28μs     1.14μs    17.49ms     0%     :new-buff            1     7.26ms     7.26ms     7.26ms     7.26ms     0%     :new-color      15,400    19.00ns    74.82μs   361.67ns     5.57ms     0%      Accounted                                                   1.70m    299%     Clock                                                      34.15s    100%

После выполнения профилирования кода с использованием Tufte, были получены результаты. Они показали, что 98% времени, затраченного на выполнение функции, приходится на nearest-material. nearest-material — это функция, которая, как описано выше, выполняет поиск ближайшего материала (в данном контексте, вероятно, блока или текстуры в игре Minecraft) на основе заданных входных данных (цвета в формате RGB). Она выполняет вычисления для определения ближайшего соответствия цвета материалу. Предполагается, что функция nearest-material часто вызывается с одними и теми же входными данными, так как входные и выходные значения этой функции ограничены небольшим диапазоном возможных значений. Поэтому, чтобы улучшить производительность функции nearest-material, предлагается использовать memoize

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

--- [x (- mc-height 1 y) 0 (p (palette/nearest-material rgb))] --- +++ (def memo-nearest-material (memoize palette/nearest-material)) ... [x (- mc-height 1 y) 0 (p (memo-nearest-material rgb))] +++
 pId             nCalls        Min        Max       Mean      Clock  Total      :img2world           1   322.64ms   322.64ms   322.64ms   322.64ms   100%     :set-blocks          1   317.40ms   317.40ms   317.40ms   317.40ms    98%     :near-mat       15,400   395.00ns     2.55ms    10.79μs   166.12ms    51%     :get-rgb        15,400   518.00ns    27.75μs   736.34ns    11.34ms     4%     :new-buff            1     5.10ms     5.10ms     5.10ms     5.10ms     2%     :rgb-vec        15,400    72.00ns    11.60μs   119.87ns     1.85ms     1%     :new-color      15,400    20.00ns    17.50μs    43.02ns   662.46μs     0%      Accounted                                                 825.10ms   256%     Clock                                                     322.76ms   100%

Сейчас мы можем сгенерировать изображение менее чем за треть секунды, по сравнению с 34 секундами, что является 100-кратным улучшением. Разумеется, при последующих вызовах изображение генерируется еще быстрее, так как отображения цвета и материала уже кэшированы.

В целом, для функции nearest-material оптимизация была успешной, но после нее стало видно новое “бутылочное горлышко” производительности, связанное с wc/set-blocks. Мы решили остановиться на данном этапе и, возможно, вернуться к ней позднее, если это будет необходимо.

Попробуйте сами

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

Спасибо Арне, Ариэлю и Фелипе за блестящий мастер-класс, организаторам ClojureD за отличную конференцию, а также компании JUXT за организацию нашего мероприятия.

Узнать больше про особенности Clojure — сферу разработки и основные фишки языка можно на открытом уроке курса «Clojure Developer».


ссылка на оригинал статьи https://habr.com/ru/articles/767248/