[Java] Обработка клавиатуры, рисование/поворот картинок, Flappy Bird
В этот раз мы:
- изучим как обрабатывать нажатия клавиатуры
- изучим как рисовать/поворачивать картинки
- используем и то, и то в своей версии игры Flappy Bird
Ниже инструкция для случая если вы делаете с чистого листа, если же у вас уже есть прототип сделанный на прошлом уроке - просто проверяйте что очередной шаг так или иначе у вас уже сделан, либо не сделан и тогда добавьте это (например про обработку клавиатуры или рисование картинки).
1) Создайте пустой проект и так же как в прошлый раз с шариками - подготовьте проект-шаблон:
import javax.swing.*;
import java.awt.*;
public class BirdPanel extends JPanel { // наследуемся от базовой панели
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
}
}
import javax.swing.*;
public class Main {
public static void main(String[] args) {
BirdPanel panel = new BirdPanel();
// Создаем окно
JFrame frame = new JFrame();
frame.add(panel); // добавляем в окно панель
frame.setSize(640, 480); // делаем окно нужного размера
frame.setVisible(true); // делаем его видимым (ОБЯЗАТЕЛЬНО В САМЫЙ ПОСЛЕДНИЙ МОМЕНТ - когда панель уже добавлена)
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); // при закрытии окошка нужно завершить выполнение программы
while (true) {
frame.repaint(); // говорим окну что ему нужно рисоваться заново постоянно (это в свою очередь провоцирует отрисовку панели)
}
}
}
2) Посмотрите на код, он должен компилироваться и запускаться - пустое окно. Этот код вам должен быть понятен, если есть по нему непонятки что где - спросите.
3) Создайте класс Bird
:
- Поля
x
,y
- координаты птицы в данный момент (сразуdouble
, ведь это важно когда мы хотим двигать объект медленно и плавно) - Конструктор который инициализирует эти поля в переданные значения
- Метод
void paint(Graphics g)
, который отрисовывает птицу по его координатам - пусть пока что это будет эллипс размера \(5\)
4) В main
функции создайте объект класса Bird
(пусть она будет в центре окошка) и передайте его как аргумент в конструктор BirdPanel
(чтобы панель знала про существование птицы и смогла ее отрисовать)
5) В BirdPanel
:
- Добавьте поле которое будет хранить ссылку на птицу (объект класса
Bird
) - Добавьте в конструкторе аргумент типа
Bird
, сохраните этот аргумент в только что созданное поле - В методе
paintComponent
сразу послеsuper.paintComponent(g)
мы хотим вызвать у нашей птицы метод “нарисуй себя”
6) Запустите программу и убедитесь что птица нарисовалась
Теперь рисование картинки!
Нам нужно сохранить картинку на диске. При запуске программы (точнее при создании объекта птицы) считать эту картинку в объект “Картинка” - он будет хранится в поле картинка внутри птицы, и отрисовать ее через какой-то вызов аля g.drawImage(...);
!
7) Скачайте эту картинку (или любую другую) - нажмите по ней правой кнопкой и там Save image as...
(чтобы сохранить изображение):
8) Откройте в проводнике папку куда она скачалась, скопируйте ее (Ctrl+C
)
9) В IDEA нажмите сверху слева по папке src
правой кнопкой -> Open In
-> Explorer
10) Вы видите папку src
в проводнике - в ней лежат ваши исходники, создайте рядом с папкой src
папку data
- положите туда копию скачанной картинки (Ctrl+V
)
11) Убедитесь что в IDEA вы видите эту папку data
и там лежит картинка
12) Теперь давайте научимся хранить эту картинку в нашей птице - добавьте для хранения картинки новое поле BufferedImage image;
13) В конструкторе птицы это поле надо инициализировать - положить в него считанную с диска картинку - this.image = ImageIO.read(new File("data\\bird.png"));
- получается вызвали конструктор через new Bird(...)
- создали птицу - она сразу считала с диска картинку
14) Кроме красных ошибок про то что нужно импортировать классы ImageIO
и File
- вы увидите красную ошибку под read
15) Эта ошибка - Unhandled exception: java.io.IOException
- дело в том что чтение с диска может кинуть ошибку “не получилось найти картинку по такому пути” или еще что-то может пойти не так, это так называемая Input-Output (т.е. IO) ошибка - IOException
16) Нажмите Alt+Enter
-> Add exception to method signature
- теперь если такая ошибка случится - то конструктор оповестит об этом того кто вызвал конструктор
17) А значит теперь эта же IOException
ошибка появилась в Main
- в месте вызова этого конструктора new Bird(...)
- сделайте здесь то же самое, теперь если эта ошибка случиться - программа прервет исполнение и эта ошибка будет напечатана в консоль
18) Давайте это проверим! Испортите путь к картинке (например добавьте туда абракадабры) в конструкторе птицы и запустите - что вы увидели? Ошибка IIOException: Can't read input file!
как раз и говорит что файл не получилось считать - по данному пути ничего не было найдено!
19) Исправьте путь обратно и добавьте вывод в консоль размер картинки сразу после считывания (у картинки есть методы getWidth()
и getHeight()
) и запустите - ошибка должна исправиться, а в консоли вы увидите размер картинки в пикселях
20) Теперь давайте наконец нарисуем картинку в методе paint
у птицы: g.drawImage(this.image, x, y, null);
(четвертый аргумент игнорируйте - это не важно)
21) Заметьте что птица нарисовалась смещенной относительно эллипса а не центрированно, почему так? Если нет идей - прочитайте (через гугл переводчик) документацию - нажмите мышкой на drawImage
-> Ctrl+Q
22) Поняв почему так происходит - поправьте это
Теперь обработка нажатия клавиатуры!
Не любой объект умеет обрабатывать нажатия клавиатуры. Давайте научим это делать панель.
Для начала она должна заявить что у нее есть соответствующий навык и даже справочка имеется! В этой справочке-сертификате по сути гордо написано гордое заявление “я являюсь сертифицированным слушателем событий происходящих с кнопками - таких слушателей называют KeyEventDispatcher”.
Любой кто взял на себя обязательство быть KeyEventDispatcher
по сути берет на себя обязательство реализовать метод public boolean dispatchKeyEvent(KeyEvent e)
. Этот метод будет вызываться когда будут нажаты/отпущены кнопки, т.е. когда с ними будет происходить какое-то событие. А вот что мы в этом методе будем делать - наше дело, мы можем реагировать на эти нажатия как хотим!
23) Возьмите в панели обязательство обрабатывать события происходящие с кнопками - для этого сразу после наследования от JPanel
(т.е. после extends JPanel
) надо написать implements KeyEventDispatcher
(т.е. реализует этот “интерфейс”, т.е. реализует все методы которые в нем заявлены - в данном случае один - dispatchKeyEvent(...)
)
24) Сразу вся строка подсветилась красным: Class 'BirdPanel' must either be declared abstract or implement abstract method 'dispatchKeyEvent(KeyEvent)' in 'KeyEventDispatcher'
, т.е. IDEA сразу говорит нам что наш класс должен implement abstract method ‘dispatchKeyEvent(KeyEvent)’ - т.е. мы должны реализовать этот метод - это очень просто и быстро сделать если в этой красной строке нажать Alt+Enter
-> Implement methods
25) IDEA сделала нам заготовку метода dispatchKeyEvent
, давайте добавим туда System.out.println("EVENT HAPPENED!");
26) Запустим программу и нажмем кнопку… ничего не сработало, консоль пуста. Потому что мы должны не только быть сертифицированным слушателем нажатий, но еще и устроиться на работу - подключиться к прослушиванию клавиатуры
27) Зарегистрируйте в main
-функции нашу панель как сертифицированного слушателя:
KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager(); // менеджер по трудоустройству слушателей клавиатуры
manager.addKeyEventDispatcher(panel); // подключаем нашу панель к прослушиванию клавиатуры
28) Запустите вновь и нажмите кнопку на клавиатуре - должно работать! Интересно, почему-то очень много раз появляется запись в консоли!
29) Давайте изучим что за KeyEvent e
передается нашему слушателю клавиатуры. У этого KeyEvent e
есть два ценных метода - e.getID()
(говорит что за событие произошло - например кнопку нажали, или кнопку отпустили) и e.getKeyCode()
(говорит с какой кнопкой произошло это событие):
@Override
public boolean dispatchKeyEvent(KeyEvent e) {
System.out.println("EVENT HAPPENED!");
// С помощью e.getID() можно выяснить что за событие случилось? нажали кнопку? или отпустили? или какой-то typed? как думаете что это?
String typeOfEvent = "unknown";
if (e.getID() == KeyEvent.KEY_PRESSED) {
typeOfEvent = "pressed";
} else if (e.getID() == KeyEvent.KEY_RELEASED) {
typeOfEvent = "released";
} else if (e.getID() == KeyEvent.KEY_TYPED) {
typeOfEvent = "typed";
}
// С помощью e.getKeyCode() можно выяснить а не пробел ли был нажат? аналогично можно для ENTER и других кнопок
String key = "unknown";
if (e.getKeyCode() == KeyEvent.VK_SPACE) {
key = "space";
} else {
key = "code#" + e.getKeyCode();
}
System.out.println("type=" + typeOfEvent + " keyCode=" + key);
return false;
}
30) Запустите и поэкспериментируйте, что за e.getID() == KeyEvent.KEY_TYPED
?
31) Сделайте так чтобы при нажатии по пробелу - птица взлетала вверх. А при нажатии на Enter - опускалась ниже
32) Подумайте - а как сделать так чтобы при зажатой кнопке пробела птица плавно взлетала вверх? Чтобы без рывка! Если не будет идей - спросите у меня и я подскажу
УльтраХардкорМод++
33) Сделайте Flappy Bird. Вам надо добавить при каждой отрисовке:
- добавить объект стена
Wall
- проверку “врезалась ли птица в стену”
- обновление координат птицы
- обновление координат стены
- проверку “не вышла ли стена за пределы панели”
- если вышла - заменить стену на новую - случайной высоты
- заменить одну стену - на список из стен (чтобы можно было на одном экране видеть три стены или даже больше)
- сделайте так чтобы птичка поворачивалась пропорционально скорости (см. ниже в Добавке)
Добавка
Если хотите поворачивать картинку - подсмотрите здесь
Если хотите обрабатывать мышку - подсморите здесь