Разное

Java рисование: Java Swing — пример рисования динамически

как подружить Java AWT Stroke и 10 приказ Минэкономразвития РФ / Хабр

Мы тут в ИТМО занимаемся созданием всяких ГИС на заказ. И вот пришел к нам заказчик и попросил сделать демку с отображением генерального плана города и с некоторой аналитикой по ней.

Сам генеральный план — это куча геоданных в формате Shapefile, содержащих различные виды зон и объектов. Плюс поверх этого мы еще должны были рисовать всякие кадастровые участки, здания и много чего еще. И при этом отображать это как в браузере (про это я еще отдельную статью напишу, как нарисовать в браузере на карте 200+ тысяч объектов), так и рендерить отдельные схемы в пнгшки для дальнейшей печати.

И все бы ничего — взяли стандартные средства для работы с изображениями, отрендерили в текстуру по шаблону сгенерированный текст, пунктирчиком разметили границы участков, но тут всплыла такая штука как ЗОУИТ — Зоны с особыми условиями использования территории. Грубо говоря — зоны, в которых строить или нельзя вообще, или можно но с ограничениями и согласованиями. Это могут быть всякие зоны защиты инженерных сетей, водоохранные зоны и т.п.

И вот тут-то началась засада. У каждого ЗОУИТа есть свой стиль для отображения, описанный в 10 приказе Минэкономразвития (вообще порадовало, что оказывается есть цельный отдельный приказ, где прописано все в деталях, от RGB цветов линий до названий и значений полей с данными). И стили эти все достаточно непростые.

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

Под катом — описание того, как сделать свой собственный Stroke, позволяющий рисовать произвольные фигуры вдоль пути.

Сперва вкратце пробежимся по тому, как в AWT выглядит рисование объектов:

  • Берем объект Graphics — его можно получить как у окна (да, десктоп на джаве давно мертв, я в курсе), так и у, например, изображения, чтобы рендерить в текстуру. Это будет наше полотно для рисования.

  • Создаем объект типа Shape — это могут быть всякие круги-линии-полигоны и прочие геометрические примитивы, заполняем его координатами. Это то, ЧТО мы будем рисовать.

  • У нашего Graphics выставляем поля типа font, color, stroke, paint — это то, КАК мы будем рисовать.

  • Зовем один из двух основных методов — fill закрасит наш шейп, draw нарисует его контур.

Этого достаточно чтобы нарисовать что-то простое. При этом вся магия стилизации нашего изображения находится в пункте 3. И конкретно за рисование контуров отвечает класс Stroke.

Про BasicStroke

Из коробки у нас есть единственный класс BasicStroke с несколькими конструкторами, умеющий рисовать сплошные и пунктирные линии одним цветом. Наиболее понятно (для меня) параметры конструктора и возможности рисования расписаны в этой статье. Например вот разные варианты пунктира для разного варианта dashArray:

Почему-то много где приводят эти цифры, но мало где их объясняют. На деле это просто последовательность закрашенных и пустых пикселей. Т.е. [21, 9, 3, 9] означает «нарисуй 21 пиксель, затем пропусти 9, нарисуй 3, пропусти 9, начни с начала».

Тут тоже скрыт небольшой подводный камень. Вот вы задаете массив {10, 10} и ожидаете, что пунктир и пропуски у вас будут одинаковой длины… однако на изображении видите, что пунктир явно длиннее чем пустое место. Такое поведение может быть вызвано неправильным значением параметра endCap, который задает, как именно должны заканчиваться штрихи. Вариантов доступных три:

В итоге если задать что-то отличное от CAP_BUTT, к штриху добавится еще несколько пикселей на его окончание.

С помощью BasicStroke и dashArray можно рисовать различные сложные последовательности пунктира. И большинство туториалов заканчиваются фразой в духе «ну вот этого вам точно хватит на 99% случаев рисования, поэтому углубляться дальше не будем».

Ну а вот мы попали в тот самый 1 процент. Нам вдоль пути надо рисовать не только пунктир, но еще и различные символы в разрывах. В том самом 10 приказе встречаются самые разные варианты: кружочки, крестики, полоски в одну сторону от линии, полоски в обе стороны от линии, какие-то повернутые галочки и чего еще только нет. В общем, нам нужен Stroke, который умеет рисовать не только линию. но и произвольный Shape раз в N пикселей. Повернутый на правильный угол.

Как устроены классы Stroke

Реализуем свой собственный класс, наследник Stroke. Этот интерфейс определяет единственный метод createStrokedShape. Суть в том, что в AWT по сути есть одна-единственная «настоящая» операция рисования — это заливка области, заданной Shape’ом. Рисование контуров тоже сделано через заливку.

Stroke берет геометрию и превращает ее в фигуру, площадь которой надо залить. Т.е. в простейшем случае линия превращается в многоугольник нужной толщины, построенный вокруг нее, который потом заливается сплошным цветом. И наш метод должен брать геометрический объект (в нашем случае — линию границы ЗОУИТ) и возвращать Shape, который можно будет залить нужным цветом и получить итоговый контур.

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

Несколько неплохих примеров есть в статье http://www.java2s.com/Code/Java/2D-Graphics-GUI/CustomStrokes.htm. Обратите внимание на занятный вид штриха, искажающего форму объекта и имитирующего «рукописные кривули»:

Поскольку глупо было бы изобретать велосипед и делать рисование пунктиров самостоятельно, если это уже есть в стандартной библиотеке, сделаем так:

  1. Нарисуем наш Shape стандартным BasicStroke с нужным типом пунктира

  2. Добавим к этому Shape наши выступающие кусочки и всякие загогулины

  3. Вернем комбинированный Shape

Под катом полный код метода createStrokedShape()

Spoiler
override fun createStrokedShape(shape: Shape): Shape {
        // По сути копия исходного шейпа, которую мы будем рисовать чтобы знать последнюю точку
        val newshape = GeneralPath() 
        // Тут мы будем хранить все вставленные нами дополнительные шейпы
        val innerShape = GeneralPath()
        val i: PathIterator = shape.
getPathIterator(null) val coords = FloatArray(6) // Аккумулятор расстояния с момента последней вставки нашего дополнительного шейпа var pixels = 0.0 // Пройдем по сегментам переданного нам Shape while (!i.isDone()) { val type: Int = i.currentSegment(coords) when (type) { // Пустой сегмент - ничего не делаем PathIterator.SEG_MOVETO -> { newshape.moveTo(coords.get(0), coords.get(1)) } PathIterator.SEG_LINETO -> { // Вычислим длину сегмента val startX = newshape.currentPoint.x val startY = newshape.currentPoint.y val endX = coords.get(0).toDouble() val endY = coords.get(1).toDouble() val line = Line2D.Double(startX, startY, endX, endY) val segmentLength = Math.sqrt(Math.pow(endX - startX, 2.0) + Math.
pow(endY - startY, 2.0)) // Если сегмент короче чем расстояние между вставками шейпа - ничего не делаем if (pixels + segmentLength < step) { pixels += segmentLength } else { // Накопилось достаточное расстояние с момента вставки предыдущего шейпа, вставми новую копию var offset = Math.max(0.0, step - pixels) while (offset < segmentLength) { // Вычисляем точку на текущем сегменте, в котороую надо вставить шейп val stepScale = offset / segmentLength val stepTransform2 = AffineTransform.getTranslateInstance(startX, startY) stepTransform2.scale(stepScale, stepScale) stepTransform2.translate(-startX, -startY) val stepLine = stepTransform2.createTransformedShape(line) as Path3D.
Double val lastPoint = getLastPoint(stepLine) // Берем наш шейп, поворачиваем и перемещаем его в эту точку val transformed = generateShapeForInsertion(line, lastPoint!!) // Добавляем шейп к результирующему шейпу innerShape.append(transformed, false) offset += step } pixels = segmentLength - offset + step } newshape.lineTo(coords.get(0), coords.get(1)) } PathIterator.SEG_QUADTO -> { newshape.quadTo(coords.get(0), coords.get(1), coords.get(2), coords.get(3)) } PathIterator.SEG_CUBICTO -> { newshape.curveTo(coords.get(0), coords.get(1), coords.get(2), coords.get(3), coords.get(4), coords.get(5)) } PathIterator.
SEG_CLOSE -> newshape.closePath() } i.next() } // Составим результат из newShape (сейчас содержащего по сути копию входного шейпа) // и добавленных нами шейпов вдоль него val rz = Area(mainStroke.createStrokedShape(newshape)) rz.add(Area(sideStroke.createStrokedShape(innerShape))) return rz }

Начинаем мы с двух пустых шейпов. В один будем складывать копию входного шейпа (наверное можно было бы обойтись и без нее и как-то вытаскивать координаты из итератора), во вторую будем складывать добавленные нами шейпы, которые будем рисовать вдоль линии.

Идем итератором по сегментам исходного шейпа, накапливаем расстояние. Как только накопилось достаточно — находим точку, в которую надо вставить дополнительный шейп, создаем афинное преобразование для перемещения и поворота и добавляем его к sideStroke шейпу.

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

Полный код класса можно увидеть в нашем репозитории в GitHub

В итоге можно рендерить всякие вот такие вот линии, которые очень нравятся чиновникам и архитекторам:

Познаем основы Java и создаем крестики-нолики — Разработка на vc.ru

18 022 просмотров

Немного о Java

Java — это не только язык, это целая экосистема, включающая в себя средства разработки, платформу для запуска готовых приложений, огромный свод документации и активное сообщество. Одним из преимуществ Java на начальном этапе была кросс-платформенность (принцип — «написано один раз — запускается везде»).

Дело в том, что программа на Java исполняется не на прямую процессором компьютера, а виртуальной машиной Java (JVM). Это позволяет абстрагироваться от многих нюансов конкретных платформ. Программу, написанную на Java, можно без изменений кода запускать на Windows, Linux, MacOS и других операционных системах (если, конечно, программа не использует специфичные для ОС функции).

Кто застал начало 2000х, наверное помнит огромное количество мобильных телефонов (тогда еще они не были смартфонами), на каждом телефоне была по сути своя маленькая ОС, но при этом почти на каждом можно было запустить Java игру или приложение.

На сегодняшний день Java по-прежнему входит в топ языков для изучения, а Java как платформа — в топ используемых технологий в мире IT и смежных областях.

Создание проекта, первые шаги

Сегодня мы начнем изучать Java, причем сразу с примера игры «Крестики-нолики».

Итак, поехали. Надеюсь как установить java SDK ты уже разобрался. Мы будем писать код в IDE IntelliJ IDEA, но если у вас какая-то другая, например Eclipse, то разницы большой не будет.

Итак, создаем новый проект: нажимаем «create new project», выбираем java и щелкаем «next» до окна, где требуется ввести имя проекта, вводим TicTacToe (крестики-нолики). В некоторых случаях на этапе создания потребуется выбрать шаблон проекта, тогда смело выбирай что-либо похожее на JavaConsoleApplication.

После этого нажимаем «Finish». Idea немного подумает и сгенерирует нам проект с классом Main, в котором определена функция main().

Давайте разберемся, что здесь что. Слева открыто окно структуры проекта «Project», как мы видим в папке src в пакете com.company находится единственный java-файл нашей программы с именем Main. Справа показано его содержимое. Тут надо сделать небольшое отступление, дело в том, что в Java почти все представлено классами. В том числе и файлы программы описывают классы, причем имя файла должно совпадать с классом, который описывается в этом файле (например, у нас файл Main.java описывает класс Main). Пусть слово «класс» не смущает на первом этапе. Пока лишь отмечу, что для глубокого изучения Java так или иначе придется познакомиться с объектно-ориентированным подходом. В двух словах, класс можно воспринимать как шаблон, идею, а экземпляры класса — как реализацию этой идеи. Экземпляры класса называются его объектами. Например, вы можете представить идею стола (нечто, на что можно класть предметы), однако конкретных экземпляров такого стола огромное множество (на одной ножке, на четырех, круглые, квадратные, из разных материалов).

Примерно так соотносятся классы и объекты в объектно-ориентированном программировании.

Внутри нашего класса Main описана функция main(), в Java с этой функции начинается исполнение программы, это точка входа в наше приложение. Сейчас там написан только автоматический комментарий (комментарии в Java начинаются с двух символов //). Попробуем кое-что добавить в наш код и проверить работоспособность приложения. Внутри функции main() допишем две строки:

Встроенная функция println() просто выводит на экран текстовую информацию. Запустим наше приложение (нажимаем shift-F10 или зеленый треугольник). Внизу, во вкладке run появится вывод нашей программы:

Функция main() отработала и закончилась, вместе с ней закончилась наша программа.

В игре пользователю конечно захочется взаимодействовать с программой более продвинутым способом, поэтому нам понадобится окно. Набираем внутри функции main() следующие строки:

Смысл большинства строк понятен из комментариев к ним, отдельно отмечу строку window. setLayout() — здесь устанавливается менеджер расположения, который будет применяется к компонентам, добавляемым в наше окно. Менеджер BorderLayout может располагать новые компоненты относительно сторон света (North(верх), West(слева), East(справа), South(низ)), Center (центр)). По умолчанию он располагает компоненты по центру. Подробнее с менеджерами расположения можно познакомиться в документации.

Теперь, если запустить нашу программу, мы увидим окно:

Пока в этом окне ничего нет. Создадим свой компонент, который и будет отрисовывать графику игры.

Свой компонент для рисования

Очевидно, что рисовать в консоли у нас не получится, нужен какой-то компонент для более продвинутого взаимодействия с пользователем. Для этой цели создадим еще один класс, назовем его TicTacToe. Щелкаем правой клавишей мыши на имени пакета приложения (в данном случае это com.company)

И в появившемся меню выбираем пункт «New» → «Java Class». В окне создания класса набираем его имя «TicTacToe» и нажимаем «Enter».

У нас в проекте появился еще один класс. В главное окно можно добавлять только объекты класса JComponent, кроме того, нам нужна область для рисования. Поэтому наследуем наш класс TicTacToe от JComponent. Ой сколько непонятных слов! Сейчас постараюсь пояснить.

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

С JComponent то же самое — данный класс реализует идею некоторого графического компонента пользовательского интерфейса. Такой компонент можно добавить в окно и он умеет как-то себя отрисовывать. Например, класс JButton — наследник класса JComponent, это компонент, который выглядит, как кнопка и умеет показывать анимацию клика мышкой.

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

Итак дописываем extends JComponent в строку описания класса:

Слово extends говорит о том, что наш класс TicTacToe расширяет (наследует) класс JComponent.

У всех компонентов есть метод paintComponent(), который отвечает за их отрисовку. В параметры этого метода приходит объект Graphics, с помощью которого мы и будем рисовать то, что нам необходимо. Давайте переопределим метод paintComponent так, чтобы он рисовал окружность (это необязательно, но для проверки, что у нас все работает как надо, будет хорошим тоном это сделать).

Переопределим метод paintComponent() в классе TicTacToe следующим образом:

метод setColor() объекта graphics, как очевидно из названия, устанавливает цвет, которым мы будем рисовать, а метод drawOval(x, y, w, h) — в общем случае рисует овал с координатами центра x, y, шириной — w и высотой h. В данном случае рисуется окружность, так как ширина и высота заданы одинаковые — 100. Замечу, что экранные координаты отсчитываются от левого верхнего угла. То есть 0 по вертикали находится вверху.

Чтобы проверить, как выглядит наш объект класса TicTacToe надо создать его экземпляр и добавить в главное окно в качестве дочернего компонента. Создание новых объектов в Java осуществляется с помощью ключевого слова new.

Например, если у нас есть класс Стол и мы хотим создать объект этого класса (настоящий конкретный стол), то мы должны написать что-то такое: стол = new Стол(). Здесь «стол» имя, по которому мы будем обращаться к нашему объекту (взаимодействовать с ним), а Стол — имя класса, объект которого мы создаем.

Замечу сразу, что вместо «стол» мы могли написать любое имя, например «fdgdgdgfd», но программисты обычно стараются давать «говорящие» имена объектам, чтобы код было легче читать. Чтобы создать экземпляр класса TicTacToe мы можем также написать game = new TicTacToe(), а потом добавить его в окно методом add().

Теперь код класса Main выглядит вот так:

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

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

Создание игрового поля

Вернемся к классу TicTacToe. Для начала необходимо нарисовать игровое поле, состоящее из девяти клеточек. Для этого давайте нарисуем две горизонтальные и две вертикальные линии на нашем поле. Чтобы это сделать, воспользуемся методом drawLine(x1,y1,x2,y2) объекта Graphics, который приходит к нам в метод paintComponent() в качестве параметра. Метод drawLine() рисует линию от точки с координатами x1,y1 до точки x2,y2. Давайте подумаем как нарисовать игровое поле.

Если мы разобьем высоту поля на три (у нас же три клетки в ряду), то получим высоту одной клетки (назовем ее dh). Узнать высоту всего компонента можно методом getHeight(). Значит, мы должны нарисовать первую горизонтальную линию от точки 0,dh до точки w, dh, где w — ширина поля. Но это только одна горизонтальная линия, вторую рисуем также, но координаты будут уже: начало — 0, 2*dh, конец w, 2*dh. По аналогии, если высота поля равна h, а ширина одной клетки равна dw, то вертикальные линии рисуются в координатах d, 0 — d, h и dw*2, 0 — dw*2, h.

Теперь давайте немного поговорим о переменных. Если помните — в алгебре за буквой могло скрываться какое-то значение, например выражение x = 2*a, подразумевало, что на место буквы а можно подставить любое значение и вычислить x.

Примерно то же самое происходит с переменными в программировании. Имя переменной (идентификатор) сопоставлен с некоторым значением и «хранит» его «в себе» (на самом деле, с объектами классов все несколько сложнее, там мы имеем дело со ссылками, но это пока за рамками данного материала).

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

Здесь int — означает тип данных «целое число». Выражение int a = 10 объявляет переменную с именем a и задает ей сразу значение 10. В нашем примере создаются четыре переменных, значения w и h получаются из методов самого компонента TicTacToe, а dw и dh вычисляются. Обратите внимание, что при делении w / 3 получается целый тип данных. В Java, как и во многих других языках, деление целого на целое дает в результате целое. При этом дробная часть просто отбрасывается (округления нет). Заметьте, что здесь не используется слово «new», так как создаются не объекты, а переменные простых (скалярных) типов данных, в данном случае типа int.

Мы могли бы уже написать код для рисования всех линий, но мы же программисты, а программисты любят все упрощать, правда для этого они пишут много дополнительного кода. Представим, что у нас было бы поле не 3 на 3 клетки а, например, 15х15. Как бы мы его разлиновали? Вручную набирать код для рисования 28 линий это уж слишком.

К счастью, во всех языках программирования (привет ассемблер) есть конструкции, позволяющие повторить заданное число раз тот или иной участок кода — циклы.

Разберем, как автоматизировать рисование линий, заметим, что все горизонтальные линии содержат одни и те же значения координат по горизонтали (от начала до конца ширины игрового поля), при этом их координаты по вертикали различаются на dh. У первой линии высота dh, у второй 2*dh, и так далее.

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

Попробуем рисовать линии с помощью цикла, в классе TicTacToe создадим свой метод с названием drawGrid(), он будет у нас отвечать за рисование линий сетки игрового поля:

Еще раз пробежимся по коду. Первые четыре строки метода — необходимые нам значения ширины, высоты игрового поля и ширины, высоты одной ячейки. Цикл начинается с ключевого слова for, в скобках после него указывается переменная, которая будет счетчиком (у нас она еще и объявляется сразу int i = 1), условие при ложности которого цикл прервется и выражение изменяющее переменную-счетчик (i++ увеличивает i каждую итерацию цикла на единицу).

Внутри цикла каждую итерацию рисуются очередные горизонтальная и вертикальная линии поля.

Добавим вызов нашего метода drawGrid() в метод отрисовки всего компонента paintComponent():

Запускаем программу и видим разрисованное поле:

Скажи мне, куда ты кликнул?

Итак, наше игровое поле выглядит готовым к игре, но теперь нам надо узнать в какой из квадратов кликнул пользователь. Для этого давайте немного настроим наш компонент TicTacToe, чтобы он смог принимать события от мыши. Во-первых, нам необходимо включить получение таких событий. Делается это с помощью метода enableEvents(), но где его вызвать?

Конечно можно было бы добавить его вызов в наш метод drawGrid() или даже в paintComponent(), но эти методы по логике работы игры будут вызываться каждый раз, когда мы захотим что-то нарисовать. А включить события надо лишь один раз. Где бы найти метод, который вызывается у компонента единожды, например при его создании?

На самом деле такой метод есть у каждого класса и называется он конструктором. Именно конструктор вызывается при попытке создания нового объекта. Конструкторов может быть несколько, он так же как обычный метод может принимать параметры, но вот возвращаемого значения у него нет. Конструктор класса имеет то же имя, что и класс. Создадим конструктор в классе TicTacToe:

Как видим — ничего сложного, просто еще один метод. А как же наш компонент создавался до этого? Ведь в классе Main мы его уже создавали. Помните, game = new TicTacToe()? Тут тоже никакой магии — если конструктор не задан явно, используется конструктор по умолчанию.

Именно здесь мы включим получение событий от мыши:

Хорошо! Получение событий мы включили, а где же мы их будем получать? В методе processMouseEvent() конечно, именно он будет срабатывать каждый раз, когда указатель мыши каким-либо образом взаимодействует с нашим игровым полем.

Приведу на всякий случай полный код класса TicTacToe на текущий момент:

Мозг игры

Ну не то чтобы уж мозг, но некоторую начинку нам создать придется. Итак, давайте подумаем, как хранить состояние игры? Неплохо бы было хранить состояние каждой клетки игрового поля, для этой цели хорошо подойдет двумерный массив целых чисел размером 3х3. Создается он просто int[][] field = new int[3][3].

Массив это уже целый объект, на который выделяется память в отдельной области (куче), поэтому тут мы используем слово new. Создадим в классе TicTacToe новый метод под названием initGame(), он будет отвечать у нас за сброс игры к начальному состоянию, именно здесь мы будем «очищать» игровое поле.

Для хранения состояния ячейки поля создадим три константы со значениями 0, 10 и 200. Ноль будет соответствовать пустой ячейке, 10 — крестику, а 200 — нолику. Первоначально заполним массив нулями.

Теперь добавим немного кода в метод processMouseEvent(), который отвечает за клики мышью на игровом поле. Также заведем логическую переменную isXturn, которая будет показывать чей сейчас ход.

Пробежимся по коду. Переход от координат к индексам довольно прост: мы делим текущую координату на размер одной ячейки и получаем сколько целых ячеек укладывается до текущей (если совсем непонятно, то поясню: разделить на w/3 это то же самое, что умножить на 3/w).

В 42й строке кода стоит условный оператор (также называемый ветвлением), пора с ним познакомиться. Если условие в скобках истинно (в нашем случае если поле пустое), то мы заходим «внутрь» условного оператора (строки 43-46), если же условие ложно (ячейка уже занята), то мы пройдем дальше. Что же происходит если кликнутая ячейка пуста?

В 44й строке после «=» стоит еще один интересный оператор — тернарный, он дает возможность записать в строку ветвление, если в результате него присваивается значение. Записывают его так: isXturn? — это проверка, чей сейчас ход (ходит крестик, если значение «истина»), далее следует определенная нами константа FIELD_X, именно она будет результатом выражения, если isXturn — true.

После FIELD_X стоит двоеточии и константа FIELD_O — ее значение станет результатом выражения, если ход «нолика». После изменения значения в ячейке массива, меняем очередность хода: isXturn =! isXturn изменит значение переменной на противоположное. В конце всех действий — вызываем перерисовку компонента, так как теперь нужно нарисовать крестик или нолик, там где его не было раньше.

Теперь осталось научиться рисовать крестики или нолики. Создадим два метода: drawX() и draw0(). На данном этапе мы уже умеем рисовать линии и круги, поэтому обойдусь комментариями в коде:

Комментарии в коде достаточно очевидны. Коротко поясню, что крестик мы рисуем как пересечение двух линий из угла в угол ячейки, а нолик — как овал чуть вытянутый по вертикали. Теперь у нас есть методы, рисующие крестик и нолик по заданным индексам ячейки поля. Как же мы будем рисовать процесс игры? Пробежимся еще раз по коду. Игроки кликают мышкой на наш компонент, при этом срабатывает метод processMouseEvent(), в котором мы определяем, какое событие произошло, пуста ли ячейка, в которую кликнули и вызываем перерисовку компонента (repaint()). На момент перерисовки в массиве field содержатся актуальные данные о поставленных крестиках и ноликах, остается пробежаться циклами по всему массиву и если встречается нолик — рисовать нолик, а если крестик — рисовать крестик. Поехали, создаем метод drawXO(). Именно в нем будем «просматривать» массив:

Осталось вызвать данный метод в методе painComponent():

Теперь, если запустить нашу программу можно понаслаждаться постановкой крестиков и ноликов:

Определяем победителя

Все бы хорошо, но сейчас игра никак не отслеживает свое состояние, то есть крестики и нолики успешно ставятся, но выигрыш никак не определяется. Придется еще немного потрудиться! Как же нам определить, что игра закончилась? Давайте присмотримся к массиву field. Если в ряду будут стоять одни крестики, значит, там значения 10, 10, 10.

Если нолики — 200, 200, 200. Эврика! Давайте проверять сумму всех ячеек в ряду по горизонтали и вертикали, а также по двум диагоналям. Создаем еще один метод checkState(), он будет каждый ход проверять сложившуюся на поле ситуацию и возвращать -1, если ходов не осталось, 3*FIELD_X если выиграли крестики и 3*FIELD_O, если выиграли нолики, в случае продолжения игры — метод пусть вернет 0.

Основные комментарии даны в коде. Элементы, стоящие на главной диагонали вычисляются просто — их индексы равны ([0][0], [1][1] и т. д.). Побочная диагональ содержит элементы с индексами [0][N-1], [1][N-2] и так далее (N — длина массива).

Часть кода с 142й по 160ю строку отвечает за подсчет суммы значений в ячейках по вертикальным и горизонтальным рядам: каждую «большую» итерацию по i фиксируется вертикальный (горизонтальный) ряд с индексом i и запускается малый цикл с перебором всех ячеек в данном ряду (цикл по j).

Кроме того, проверяется наличие на поле хотя бы одной не занятой ячейки (hasEmpty=true), это нужно чтобы определить ситуацию, когда все ячейки заняты, но никто не выиграл (ничья). Наконец, если нигде ранее не произошел выход из метода мы проверяем значение hasEmpty, если пустые ячейки есть — возвращаем 0, а если нет, то -1 (ничья).

Осталось использовать данный метод. Немного подправим обработку нажатий.

Здесь добавилось получение результата из метода checkState() и его обработка. Метод showMessageDialog() показывает сообщение с заданным текстом и заголовком. В ветвлении проверяем, какое значение вернул метод checkState() и показываем соответствующие сообщения (или продолжаем игру, если результат 0).

На этом данный туториал подходит к концу. За не столь долгое время нам удалось создать игру с графическим интерфейсом и захватывающим геймплеем: ).

Задание на дом: наша игра все-таки ленивая, и почему-то не зачеркивает выигрышный ряд. Подумайте, как можно его зачеркнуть, ведь рисовать линии вы уже умеете (подсказка, обратите внимание на метод checkState() — там уже сделана почти вся работа по нахождению выигрышного ряда).

С помощью нашего шестимесячного курса «Профессия: Разработчик» вы научитесь писать в Java не только это! 👉 Узнать подробности!

Лабораторная работа 11: Графика Java

Лабораторная работа 11: Графика Java

Начало работы

  1. Чтение фона на графике
  2. Snarf проект labs/lab11 в Eclipse
  3. Добавьте awb.jar в путь сборки
  4. Откройте MyDrawing. java и запустите как апплет без внесение каких-либо изменений в код прямо сейчас!

  5. Просмотр апплета MyDrawing с помощью программы просмотра апплетов. Все вы увидите пустой белый холст и два поля IntField для ввода координаты х и у. Эти координаты определяют положение вашего рисунка на холсте.

Сделать

Следующие элементы должны быть реализованы , чтобы получить кредит на лабораторию завершение.

  1. Инициализируйте и добавьте две кнопки bDraw и бОчистить. Кнопки должны быть помечены как «Рисовать» и «Очистить» соответственно и выполнить следующие действия:

    • Кнопка bDraw: Ваше действие Выполнила подпрограмма уже содержит код для ввода пользователя x и y начальные координаты, которые установлены на (0,0) по умолчанию. Добавьте код в передать значения x и y в подпрограмму Draw.

    • Кнопка bОчистить: добавьте код в действие Выполняется для очистки холста. Вы можете легко сделать это, окраска всего полотна в белый цвет. Глядя на GDemo код может помочь вам понять, как это сделать.

  2. Подпрограмма Draw объявлена ​​следующим образом:

    public void Draw (int x, int y)

    Подпрограмма принимает на вход два целых числа, которые представляют координаты x и y положения рисунка на холсте.

    Вы можете рисовать все, что хотите, если это «имеет смысл». За Например, вы можете нарисовать что-то простое, например, цветок в примере ниже. или создать узор из геометрических фигур, но не стоит просто нагромождать случайные фигуры на холсте. Вы должны использовать несколько разных форм (например, линии, овалы, прямоугольники и т.д.) и цвета. Убедитесь, что при разработке ваш рисунок вы используете общие координаты x и y, переданные в подпрограмму Draw. Java Graphics API содержит спецификации методов для рисование фигур. Может быть проще добавлять фигуры по одной, компилируя и тестирование вашего апплета в промежутке, или набросать ваши формы и координаты на сначала бумага.

    ПОМНИТЕ : Система координат для Java графика начинается с левого верхнего угла и идет вправо и вниз!

Пример

Ниже приведен пример возможной реализации MyDrawing апплет. При нажатии кнопки Draw изображение рисуется в координаты по умолчанию (0,0), холст выглядит следующим образом:

Если введены координаты (100,100) и кнопка Draw нажимается, рисуется вторая копия картинки:

Получение кредита

Полный балл будет присужден за превышение минимальных требований — для например, создание особенно сложного и/или замысловатого рисунка, или реализовать одно из предложенных ниже предложений.


ПРИМЕЧАНИЕ: Если вы решите кредитная работа, добавьте краткое описание того, что вы сделали в указанном месте (по «Дополнительная работа:») в файле MyDrawing.html.

  • Добавьте дополнительные кнопки и подпрограммы для рисования нескольких изображений (не несколько форм!).

  • Добавить дополнительную кнопку (и, возможно, подпрограмму) на плитку холст с несколькими копиями вашей картины.

  • Добавьте дополнительную кнопку (и, возможно, подпрограмму), чтобы перевернуть и/или повернуть изображение.

  • Добавить дополнительные кнопки и подпрограммы для изменения цвета на твоей картинке.

  • Добавить дополнительные кнопки и подпрограммы в растянуть/уменьшить/изменить размер изображения.

  • Добавьте дополнительные поля IntField для принятия пользовательского ввода за пределами координаты.

В приведенном ниже примере показано, что происходит, когда вы нажимаете кнопку «Плитка». Кнопка:

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

Дополнительный дополнительный кредит

Реализуйте рекурсивную графическую структуру, например

Треугольники Серпинского:

или H-деревья:

Отправка

Отправьте файлы MyDrawing. java и MyDrawing.html, используя имя задания lab11 с Eclipse.

Вы указали свое имя в своих файлах???


Комментарии?
Последнее изменение: среда, 16 ноября, 09:26:38 EST 2005 г.

типов операций) – Блог Энди Балаама

Серия: операции, изображения, непрозрачность

Я хочу переделать пользовательский интерфейс рабочего стола моей игры Rabbit Escape, чтобы он был более удобным и красивым, поэтому я по-новому взглянул на игровой цикл рендеринг графики на холст в пользовательском интерфейсе Java 2D (Swing).

Подробнее об изображениях читайте в следующем посте.

В частности, насколько быстро это может быть и каких ошибок мне следует избегать при этом?

Результаты

  • Большие окна (намного) медленнее
  • Изменение размера изображений на лету происходит очень медленно, даже если они каждый раз имеют один и тот же размер
  • Отрисовка маленьких изображений выполняется быстро, но рисование больших изображений происходит медленно
  • Быстрое рисование прямоугольников
  • Текст рисуется быстро
  • Быстрое рисование виджетов Swing перед холстом
  • Создание шрифтов «на лету» — это немного медленно

Код

Вы можете найти полный код (написанный на Kotlin) на gitlab. com/andybalaam/java-2d-performance.

По сути, мы создаем JFrame и Canvas и говорим им не слушать перерисовки (т.е. мы контролируем их отрисовку).

 значение приложение = JFrame()
app.ignoreRepaint = истина
Вал Холст = Холст ()
canvas.ignoreRepaint = true 

Затем мы добавляем любые кнопки в JFrame, а холст последним (чтобы он отображался позади):

 приложение.добавить(кнопка)
app.add(canvas) 

Теперь мы делаем холст с двойной буферизацией и получаем для него буферное изображение:

 app.isVisible = true
canvas.createBufferStrategy(2)
valbufferStrategy = canvas.bufferStrategy
val bufferedImage = GraphicsEnvironment
    .getLocalGraphicsEnvironment()
    .defaultScreenDevice
    .defaultКонфигурация
    .createCompatibleImage(config.width, config.height) 

Затем в тесном цикле мы рисуем изображение в буфере:

 val g2d = bufferedImage.createGraphics()
пытаться
{
    g2d.color = цвет фона
    g2d.fillRect(0, 0, config. width, config.height)

    ... сюда идут различные операции рисования ... 

, а затем меняем местами буферы:

 val graphics = bufferStrategy.drawGraphics
    пытаться {
        Graphics.drawImage (буферизованное изображение, 0, 0, ноль)
        если (!bufferStrategy.contentsLost()) {
            bufferStrategy.show()
        }
    } в конце концов {
        графика.распоряжаться()
    }
} в конце концов {
    g2d.dispose()
} 

Результаты

Базовая линия: несколько прямоугольников

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

Мой тестовый компьютер — это Intel Core 2 Duo E6550 2,33 ГГц с 6 ГБ ОЗУ и графической картой GeForce GT 740 (я понятия не имею, используется ли он здесь — полагаю, что нет). Я использую Ubuntu 18.04.1 Linux, OpenJDK Java 1.8.0_191 и Kotlin 1.3.20-релиз-116. (Я ожидаю, что результаты были бы идентичными, если бы я использовал Java, а не Kotlin. )

Я провел все тесты в двух размерах окна: 1600×900 и 640×480. 640×480 было неприлично быстро для всех тестов, но 1600×900 боролось с некоторыми задачами.

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

 g2d.color = Color(
    ранд.nextInt(256),
    ранд.nextInt(256),
    ранд.nextInt(256)
)
g2d.fillRect(
    rand.nextInt(config.width/2),
    rand.nextInt(config.height/2),
    rand.nextInt(config.width/2),
    rand.nextInt(config.height/2)
) 

В маленьком окне базовая линия (20 прямоугольников) работала со скоростью 553 кадра в секунду. В большом окне он работал со скоростью 87 кадров в секунду.

Никакой статистики по этим номерам я не делал, потому что мне лень. Не стесняйтесь делать это правильно и дайте мне знать о результатах — я с радостью обновлю статью.

Меньше прямоугольников

Когда я уменьшил количество прямоугольников, чтобы меньше рисовать, я заметил небольшое улучшение производительности. В маленьком окне рисование 2 прямоугольников вместо 20 увеличило частоту кадров с 553 до 639. , но в этих результатах много шума, и другие прогоны были намного ближе. В большом окне такое же уменьшение увеличило частоту кадров с 87 до 92. Это не сильное ускорение, показывающее, что рисование прямоугольников происходит довольно быстро.

Добавление изображений фиксированного размера

Рисование предварительно масштабированных изображений выглядит следующим образом:

 g2d.drawImage(
    изображение,
    rand.nextInt(config.width),
    rand.nextInt(config.height),
    нулевой
) 

Когда я добавил 20 маленьких изображений (40×40 пикселей) для отрисовки в каждом кадре, производительность почти не изменилась. В маленьком окне прогон, показывающий 20 изображений в кадре (а также прямоугольник), на самом деле работал быстрее, чем без него (561 FPS против 553), предполагая, что разница незначительна, и мне следует сделать некоторую статистику. В большом окне версия с 20 изображениями работала с точно такой же скоростью (87 кадров в секунду).

Получается, что рисование маленьких изображений почти ничего не стоит.

Когда я перешел к большим изображениям (400×400 пикселей), маленькое окно замедлилось с 553 до 446 кадров в секунду, а большое окно замедлилось с 87 до 73 кадров в секунду, поэтому большие изображения явно имеют влияние, и нам нужно будет ограничить количество и размер изображений, чтобы поддерживать приемлемую частоту кадров.

Масштабирование изображений на лету

Вы можете масштабировать изображение на лету, когда рисуете на холсте. (Спойлер: не делайте этого!)

Мой код выглядит так:

 val s = config.imageSize
val x1 = rand.nextInt(config.width)
val y1 = rand.nextInt(config.height)
знач х2 = х1 + с
знач y2 = y1 + s
g2d.drawImage(
    немасштабированное изображение,
    х1, у1, х2, у2,
    0, 0, unscaledImageWidth, unscaledImageHeight,
    нулевой
) 

Обратите внимание, что используется форма drawImage с 10 аргументами. Вы можете быть уверены, что избежали этой ситуации, если воспользуетесь формой с 4 аргументами из предыдущего раздела.

Примечание: результирующее изображение имеет одинаковый размер каждый раз, когда , и документация Java подразумевает, что масштабированные изображения могут кэшироваться системой, но я видел огромное замедление при использовании формы drawImage с 10 аргументами. выше.

Изображения, масштабированные на лету, замедлили маленькое окно с 446 до 67 кадров в секунду (!), а большое окно — с 73 до 31 кадра в секунду, то есть один и тот же рендеринг занял в два раза больше времени.

Совет: убедитесь, что вы не используете одну из перегрузок drawImage, которая масштабирует изображения! Предварительно масштабируйте их самостоятельно (например, с помощью getScaledInstance, как я сделал здесь).

Отображение текста

Рисование текста на холсте следующим образом:

 g2d.font = Font("Courier New", Font.PLAIN, 12)
g2d.color = Цвет.ЗЕЛЕНЫЙ
g2d.drawString("FPS: $fpsLastSecond", 20, 20 + i * 14) 

оказал такое же влияние на отрисовку маленьких изображений, т. е. очень незначительно повлиял на производительность и в целом довольно быстро. Маленькое окно замедлилось с 553 до 581 FPS, а большое окно с 87 до 88.

Создание шрифта каждый раз (как показано выше) еще немного замедляло процесс, поэтому стоит вынести создание шрифта из игры цикл и делает это только один раз. Замедление только при создании шрифта составило от 581 до 572 FPS в маленьком окне и от 88 до 86 FPS в большом.

Виджеты Swing

Добавив виджеты Button в JFrame перед Canvas, я смог отобразить их впереди. Их рендеринг и фокусировка работали, как и ожидалось, и они никак не влияли на производительность.

То же самое было, когда я пытался добавить эти виджеты перед изображениями, отображаемыми на холсте (вместо прямоугольников).

Увеличиваем все до 11

Когда я добавил все, что тестировал, одновременно: прямоугольники, текст каждый раз с новым шрифтом, большие немасштабированные изображения и большое окно, частота кадров снизилась до 30 FPS. Это уже немного медленно для игры, и если бы у нас было больше изображений для рисования, это могло бы стать еще хуже. Однако, когда я предварительно масштабировал изображения, частота кадров увеличилась до 72 кадров в секунду, показывая, что Java способна запускать игры с приемлемой частотой кадров на моем компьютере, если мы будем осторожны в том, как мы ее используем.

Цифры

Маленькое окно (640×480)

Тест кадр/с
ничего 661
прямоугольник2 639
прямоугольники20 553
прямоугольники20 изображений2 538
прямоугольники20 изображений20 561
прямоугольники20 изображений20 больших изображений 446
прямоугольники20 изображений20 немасштабированных изображений 343
прямоугольники20 изображений20 больших изображений без масштабированияизображения 67
прямоугольники20 текст2 582
прямоугольники20 текст20 581
прямоугольники20 текст20 новый шрифт 572
прямоугольники20 кнопок2 598
прямоугольники20 кнопки20 612

Большое окно (1200×900)

Тест кадр/с
большой ничего 93
большие прямоугольники2 92
большие прямоугольники20 87
большие прямоугольники20 изображений2 87
большие прямоугольники20 изображений20 87
большие прямоугольники20 изображений20 больших изображений 73
большие прямоугольники20 изображений20 немасштабированных изображений 82
большие прямоугольники20 изображений20 больших изображений немасштабированные изображения 31
большие прямоугольники20 text2 89
большие прямоугольники20 текст20 88
большие прямоугольники20 текст20 новый шрифт 86
большие прямоугольники20 кнопок2 88
большие прямоугольники20 кнопок20 87
больших изображений20 кнопок20 больших изображений 74
больших прямоугольников20 изображений20 текста20 кнопок20 больших изображений новый шрифт 72
больших прямоугольников20 изображений20 текста20 кнопок20 больших изображений не в масштабедиизображения новый шрифт 30

Пожалуйста, ответьте

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *