В этот раз мы:

  • изучим как обрабатывать нажатия клавиатуры
  • изучим как рисовать/поворачивать картинки
  • используем и то, и то в своей версии игры 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... (чтобы сохранить изображение):

Flabby bird

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
  • проверку “врезалась ли птица в стену”
  • обновление координат птицы
  • обновление координат стены
  • проверку “не вышла ли стена за пределы панели”
  • если вышла - заменить стену на новую - случайной высоты
  • заменить одну стену - на список из стен (чтобы можно было на одном экране видеть три стены или даже больше)
  • сделайте так чтобы птичка поворачивалась пропорционально скорости (см. ниже в Добавке)

Добавка

Если хотите поворачивать картинку - подсмотрите здесь

Если хотите обрабатывать мышку - подсморите здесь