содержание   вперед   назад

17. Программа анимации с графикой

Описание работы программы

В этой главе я хочу показать как сделать программу анимации с использованием простой графики. Снова вместо обсуждения общих принципов я покажу конкретную программу и просто объясню как она написана. Чтобы начать я предлагаю вам скачать, скомпилировать и запустить небольшую готовую программу, которую я назвал vkBM.jar. Прежде всего скачиваем zip-архив программы кликая ЗДЕСЬ. Вынимаем (копируем) все файлы из архива в какую-нибудь папку на компьютере. Затем надо отредактировать файл _compile.bat и указать в нем точный путь к папке вашего компилятора java. После этого запускаем bat-файл на исполнение и получаем все нужные классы. Затем можно запустить саму программу на исполнение, кликая на иконке файла _run.bat. Программа открывает окно посередине экрана и показывает в нем картинку с текстом правил игры. А на картинку накладывается другое окно с окнами ввода параметров и с кнопками [OK] и [Cancel]. Правила игры выписаны недостаточно четко, но если их знать заранее, то подсказка понятна. Я здесь попробую объяснить их более подробно, так как нам надо будет потом понимать что делает программа и каким кодом это описывается.

Итак, сначала надо отредактировать параметры показа, в первый раз можно даже все оставить как есть. Затем надо кликнуть кнопку [OK]. При этом окно программы моментально меняет свой вид и размеры. В нем теперь другая картинка, на которой изображена решетка разноцветных шаров с номерами на них. Шары пронумерованы слева направо и снизу вверх. Все неподвижно. Для запуска анимации надо нажать клавишу [Enter], это было в подсказке. Нажимаем. Все шары приходят в движение, сталкиваются между собой и о стенки ящика. Это движение может длиться бесконечно долго. Нажимаем клавишу пробела и движение замирает. Теперь мы можем посмотреть где находится шар с номером 1 и так далее. Если снова нажать клавишу [Enter], то движение продолжиться. Если надоело, то надо закрыть окно кликом крестика в правом верхнем углу. При этом программа моментально возвращается к первой форме с окнами ввода параметров. Можно изменить параметры и снова запустить анимацию. А если нет охоты, то кликаем кнопку [Cancel] и программа закрывается. Вот и все.

Разумно также сразу же обсудить что означают параметры. Первые два сверху вниз -- это ширина и высота ящика в пикселях. Следующие два -- это число шаров по горизонтали и вертикали, то есть число вертикальных и горизонтальных рядов прямоугольной сетки шаров. Следующий параметр имеет более сложный смысл -- это полная энергия шаров в начальный момент толчка, естественно в условных единицах. Энергия одного шара -- это произведение массы на квадрат скорости, деленное на 2, то есть фактически это квадрат скорости. Следующий параметр -- это число шаров, которым передается эта энергия. То есть начальная скорость каждого шара пропорциональна квадратному корню из полной энергии, деленной на число шаров. Скорость получают только шары с первыми номерами. Энергия удобна тем, что она не меняется при соударениях. Рассматриваются только абсолютно упругие соударения без изменения энергии. Ну и последний параметр -- это радиус шаров в пикселях. Цвет шаров определяется автоматически и случайным образом.

Разбор кода программы

На этом этапе вы уже запустили программу и посмотрели как она работает. Можно все классы записать в один jar-файл. Как это делается было написано в самом начале, точнее в главе 8. Сейчас приступим к разбору самого кода. Начальным файлом является файл vkBM.java. Его код очень простой. Класс vkBM имеет только один главный метод main(String[] args){new Begin();}, в котором записан запуск конструктора другого класса Begin. Сам класс Begin написан тут же. Он реализует интерфейс Runnable. Это значит, что он должен запускаться в новой нитке и иметь метод run(). Код написан так, что конструктор этого класса как раз и запускает новую нитку (процесс), а чтобы показать, что в ней запускается именно этот класс, в аргументе конструктора Thread записано служебное слово this. Таким образом, при запуске конструктора этого класса образуется новая нить с этим же классом, а по команде tr.start(); запускается метод run() этого класса, который записан сразу за конструктором. В этом методе определяется титульная строка окна программы, затем организуется бесконечный цикл while(true){ }, внутри которого определяется новый объект bf класса BrownFrame, в конструктор которого передается титульная строка окна. После того, как конструктор сработает, проверяется значение логической переменной MyPro.ok. Если оно равно true, то запускается метод game() данного объекта. В противном случае запускается метод close(); и код завершения работы программы с номером завершения 0. Все программы заканчивают запуском метода exit стандартного класса System.

Вероятно вы уже догадались, что MyPro.ok будет равно true если нажата клавиша [OK], и false если нажата клавиша [Cancel]. Вот и все с классом Begin. На самом деле он достаточно компактно делает две вещи: (1) организует новый процесс (нить), и в нем делает бесконечный цикл запуска основного окна программы, (2) запускает разные методы в зависимости от клика пользователем клавиш окна. Теперь мы видим, что вся главная работа делается именно в этом классе BrownFrame. Интересно, что в файле vkBM.java даже не понадобилось указывать ни одного пакета. Класс BrownFrame записан в новом файле BrownFrame.java. В этом файле уже записано больше кода. Он расширяет стандартный класс JFrame и реализует интерфейс KeyListener. Он импортирует некоторые методы стандартных классов и использует много разных переменных и объектов классов, которые тоже играют роль переменных.

Далее я не буду показывать отдельные строки кода, а буду ссылаться только на номера строк. Вам необходимо открыть файл BrownFrame.java в редакторе Notepad2 или любом другом, который показывает номера строк и вы сразу увидите о чем я говорю. Итак, начнем разбирать конструктор класса. Сначала все знакомо по предыдущим программам. В строках 15 и 16 задается внешний вид окна программы в варианте, разработанном авторами языка Java. В строке 17 задается иконка программы из файла. Аргументом метода setIconImage() является объект класса Image. Чтобы получить этот объект запускается конструктор класса ImageIcon с именем файла в аргументе, и к нему применяется метод getImage(), который и возвращает объект класса Image, соответствующий картинке из файла. Указанную сложную команду можно было бы написать в виде серии простых команд, но в языке Java вполне возможно писать сложные команды и по мере накопления опыта их тоже становится легко читать. Нужно постоянно следить за тем, чтобы все методы имели в аргументах объекты нужных типов (классов).

В той же строке задается титульная строка окна и к окну присоединяется детектор сигналов с клавиатуры, буквально "слушатель клавиатуры". Эта операция означает, что объявленный нами интерфейс будет реализован и все события клавиатуры будут обрабатываться должным образом. Строки 18-21 присоединяют дополнительно детектор сигналов от иконок окна. Но тут мы делаем по другому. Вместо того, чтобы объявить интерфейс окна, записать волшебное слово this и расписать интерфейс, мы записываем его сразу в аргументе команды. А именно, в аргументе мы записываем конструктор класса WindowAdapter и сразу записываем код этого конструктора, то есть сам интерфейс. В нем мы используем только один метод windowClosing(WindowEvent e), в котором меняем значение булевой переменной цикла и задаем команду dispose() чтобы принудительно убрать окно и очистить в памяти все ресурсы, связанные с ним. Иначе память засоряется и ее может не хватить. Этот код обеспечивает пользователю возможность закрыть окно кликом крестика в правом верхнем углу окна.

В строках 22-26 определяются размеры второй картинки, и, исходя из этих размеров, вычисляются размеры главного окна и его положение в центре экрана дисплея, исходя из размеров экрана. После этого инициируются переменные и задается команда сделать окно видимым. В этот момент на экране появляется окно. В строках 27-28 начинается графика. Для тех, кто сталкивается с графикой в Java впервые, нужно объяснить общие принципы. На самом деле есть несколько техник. Я использую ту, что записана, в коде. А именно, есть класс BufferedImage, который описывает несжатую картинку в виде матрицы пикселей. Конструктор этого класса имеет три аргумента (w,h,BufferedImage.TYPE_INT_RGB), где w и h -- это число пикселей в ширину и в высоту картинки, а третий аргумент -- это константа, задающая способ записи цветов пикселя. В частности константа BufferedImage.TYPE_INT_RGB означает, что пиксель будет описываться тремя цветами: красным, зеленым и синим, каждый цвет одним байтом с градацией от 0 до 255 и все три байта будут записываться в 4-х байтовое целое число. То есть картинка будет записана двумерным массивом целых чисел типа int. Есть команды упаковки цветов в целые числа и есть команды распаковки цветов из целых чисел. Четвертый байт не используется, он в принципе мог бы использоваться для записи прозрачности, но у нас он просто пустой, то есть картинка полностью не прозрачная.

После того, как мы зарезервировали место под картинку и указали способ записи информации в нее, мы можем на ней рисовать. Пока что это просто белый лист нужных размеров. Чтобы рисовать нужно объявить объект класса Graphics, который имеет разные методы рисования. Команда gg = bi.getGraphics(); как раз связывает объект класса Graphics с нашей картинкой. Теперь все, что мы будем рисовать, будет автоматически появляться на нашей картинке. Но не на экране дисплея. Наша картинка будет рисоваться в памяти, то есть в буфере. А для того, чтобы рисовать на экране, нужно использовать стандартный метод paint(Graphics g), который своим аргументом имеет объект класса Graphics, но связанный уже с экраном дисплея. Все, что рисуется в методе paint() автоматически появляется на экране. А вызывает этот метод команда repaint();. Чтобы не отвлекаться давайте сразу посмотрим что у нас написано в методе paint(). Там у нас проверяется наличие буферной картинки, и если она существует, то определяются ее размеры и она рисуется в центре окна.

Получается так, что мы сначала все рисуем в памяти компьютера, то есть в буферную картинку, а потом уже готовую нарисованную картинку рисуем еще раз на экране монитора. Делается это для того, чтобы скрыть процесс рисования. Картинка, если ее рисовать сразу на экране монитора, будет появляться частями, и сам процесс рисования не всем нравится. А показать уже нарисованную картинку -- это быстро, и она появится практически мгновенно, глаз процесс рисования не заметит. Это особенно важно, когда новая картинка лишь чуть-чуть отличается от старой, как это происходит в кино. Можно реально видеть процесс движения. В противном случае картинка бы постоянно исчезала и появлялась вновь, то есть она бы мерцала, утомляя глаза и делая показ процесса движения невозможным. Вернемся назад в строку 28 нашего кода. Там написано, что класс Graphics имеет метод drawImage() с 4-мя аргументами. Первый аргумент -- это объект класса Image, второй и третий аргументы -- это координаты левого верхнего угла картинки относительно области графики, последний аргумент -- это объект, который реализует регистрацию картинки. Я сам его никогда не использовал и просто всегда пишу, что его нет. Итак в строке 28 мы поставили вторую картинку из файла на экран дисплея.

Далее, в строках 29-33 мы считываем значения параметров из файла, используя уже известные нам методы известного класса MyPro. Про этот класс и его методы написано в главе 10. Я напомню, что тут последовательно считываются три строки из файла. Первые две строки разделяются в массивы строк по разделительному знаку в виде вертикальной линии. И из одного элемента массива строк считывается число. После этого запускается объект уже известного нам класса inpForm. Этот класс был описан в главе 15. На экране появляются окна ввода параметров с предопределенными значениями. Как вы помните, этот класс сделан так, что его конструктор не делает объект видимым. Это надо сделать отдельной командой. Далее окна ввода блокируют работу программы, пока не будет нажата одна из клавиш [OK] или [Cancel]. Как только это произойдет объект больше не нужен и его можно ликвидировать, что и делает метод dispose(). В строках 34-36 делается следующее. Новое содержание окон записано в массив строк MyPro.tt. Этот массив копируется в массив txt. Далее весь массив снова собирается в одну строку с разделителем в виде вертикальной черты и записывается в файл во вторую строку. Специфика записи в файл состоит в том, что необходимо выполнить команду MyPro.ok=true;. Поэтому мы временно запоминаем текущее значение этой переменной в другой переменной, а потом снова его восстанавливаем.

Остальной код конструктора делается только в том случае, если нажата клавиша [OK]. В строках 38-40 из введенных параметров считываются новые размеры окна и это окно снова ставится посередине экрана. Вообще говоря, размеры экрана монитора можно было бы запомнить и не определять заново, но эта операция делается мгновенно, а код тоже очень быстро копируется, так что можно и так. Интересным моментом в этом коде является то, что он показывает как можно манипулировать размерами основного окна в процессе работы программы. Не обязательно ставить на экран новое окно, как обычно делается. В новом окне нам надо нарисовать ящик и решетку шариков в нем. Мы снова используем буферную картинку. В строке 42 задается цвет, затем рисуется прямоугольник заданного размера и заданного цвета (у нас на всю картинку). И этот прямоугольник сразу показывается на экране. Но тут может быть фокус. На рисование прямоугольника надо время и этот процесс идет в отдельной нитке. Поэтому разумно поставить команду Thread.yield();, которая задерживает данную нить до тех пор, пока не будут выполнены все процессы в других нитях. В этом случае сначала будет полностью нарисован прямоугольник и только после этого он будет показан. В строках 43-52 уже больше ничего не рисуется, а считываются параметры из входных данных, в частности числа из текста, задается массив цветов для шаров используя генератор случайных чисел. Задаются стартовые координаты шаров на картинке и задаются стартовые скорости для тех шаров, которые будут двигаться. При этом модуль скорости определяется из энергии для всех шаров одинаково, а направление задается значением угла снова с использованием генератора случайных чисел. В массиве r[ ] для каждого шара отведено 4 числа, первые 2 -- это текущая координата, вторые 2 -- это текущая скорость, то есть смещение координат за один шаг анимации.

Итак, с конструктором мы разобрались. Теперь надо понять что делают методы. Метод paint() мы уже обсуждали. Метод close() также понятен. Методы keyTyped(), keyPressed() и keyReleased() соответствуют интерфейсу KeyListener. Записав его в заголовке класса мы должны определить эти методы как реакцию на события клавитатуры. Для нас достаточно определить второй из этих методов для того, чтобы записать код нажатой клавиши в переменную key. Остался главный метод: сама игра, то есть game(). Код этого метода работает фактически в бесконечном цикле, потому что переменная okg=true. Она меняет свое значение только при закрытии окна, но после этого окно все равно полностью ликвидируется. Строки 71-73. Снова открывается буферная картинка, только меньшего размера. Снова рисуется прямоугольник, только белого цвета. А затем в цикле по всем шарам задается цвет каждого шара и рисуется круг командой gg.fillOval. У этой команды 4 целых аргумента, поэтому необходимо выполнить преобразование типа из вещественной переменной в целую. Первые два аргумента -- это координаты левого верхнего угла, последние два -- это ширина и высота прямоугольника, в который вписывается эллипс. У нас они совпадают, получаем круг. Так как я привык координаты шаров отсчитывать от центра, а вертикальные координаты -- от нижней границы, то необходим несложный пересчет координат. В строках 74-76 рисуются номера шаров. Для этого задается цвет. Задается фонт, затем он устанавливается. Затем из графики берется метрика фонта и используется для того, чтобы узнать длину нарисованной строки. Соответственно нам надо сдвинуть начало строки на половину ее длины, чтобы центр строки оказался в нужной координате.

После того, как буферная картинка нарисована ее надо переместить на экран командой repaint(); и сразу освободить ресурсы, связанные с отработанной графикой командой dispose(). Иначе ресурсов может не хватить. Далее надо обсудить тонкий момент. Дело в том, что в окнах ввода 7 параметров, в строке файла -- 8 параметров, на один параметр больше. Этим лишним параметром является время задержки, в течение которого процесс "спит", то есть ничего не делает. Реализуется это командой Thread.sleep(tp);, которую надо поставить в скобки try{ }catch( ){ }. Эти скобки описывают что надо делать, если команда не сможет выполниться. Но обычно она нормально выполняется, поэтому я поставил, что ничего делать не надо, но скобки необходимы, иначе компилятор не пропустит код. Если посмотреть в файл, то там задана задержка 10 миллисекунд, то есть очень маленькая. Но ее можно делать и больше. Если ее не делать, то процессор будет постоянно работать и он не сможет выполнить какие либо другие команды операционной системы. Нужно хотя бы на немного отдавать процессор для выполнения других команд. Впрочем я только что проверил, что если шаров не очень много (100), то процессор работает на программу 50% времени. Однако можно задавать очень много шаров, больше тысячи.

Итак мы нарисовали исходные положения шаров. Остальной код нужен для пересчета координат, но он работает только тогда, когда нажата класива [Enter], ее код 10 и переменная key == 10. Если нажата любая другая клавиша, то key имеет другое значение и пересчета не будет. То есть картинка все время перерисовывается, но результат все время один и тот же. Как следствие, мы не видим изменений, потому что мы не видим процесса рисования. Как я уже говорил, пересчет координат делается из условия упругого столкновения шаров. То есть если шар просто летит, то его координаты получают приращение, равное скорости. Если шар столкнулся с другим шаром, то вычисляется ось, проходящая через центры шаров. Компоненты скорости, перпендикулярные этой оси сохраняются, а компоненты вдоль оси переходят с одного шара на другой, то есть шары обмениваются скоростями. Это очень хорошо понять при лобовом столкновении движущегося шара с неподвижным. После столкновения движущийся шар останавливается, а неподвижный забирает всю его скорость. Тройные столкновения не учитываются, а парные учитываются по очереди. По этой причине при большой плотности шаров возможны артефакты, но при малой плотности шаров все хорошо.

Вот и все. Напоследок скажу, что эту задачу легко модернизировать. В рассмотренной версии все шары имеют одинаковую массу. Можно сделать шары разной массы, и даже часть шаров бесконечной массы, то есть полностью неподвижными, как стенки. Можно сделать границы ящика более сложной формы и многое другое. Я переписал математический блок этой задачи в виде команды своего языка ACL, после чего ее стало возможно программировать на языке ACL без потери скорости анимации. Программы такого типа входят в демонстрационный блок интерпретатора vkACL.jar, а картинку можно посмотреть на сайте программы.

содержание   вперед   назад


.