Быстрый старт с Java: «лопни шарик»

Знакомимся с графической библиотекой Swing
7 минут20492

Мы продолжаем изучать Java и переходим к знакомству с графической библиотекой Java, которая называется Swing. Она позволяет легко и быстро создавать оконные приложения в стиле операционной системы. Это третья статья цикла «Быстрый старт с Java». Если  вы не знакомы с основами языка, рекомендуем предварительно прочесть две предыдущие статьи: «Быстрый старт с Java: начало» и «Быстрый старт с Java: крестики-нолики».

В этом посте мы рассмотрим применение Swing в процессе написания графической игры «Лопни шарик». Снова поговорим о классах и объектах, а также разберёмся с практическим применением механизма наследования.

Hello, Swing!

В первой статье демонстрировался класс “Hello, world!”, выводящий строку с приветствием в консоль. А сейчас мы напишем класс, который «приветствует мир», создавая простейшее оконное приложение.

import javax.swing.JFrame;
 
class HelloSwing extends JFrame {
    public static void main(String[] args) {
        new HelloSwing(); // создаём объект-окно
    }
 
    HelloSwing() {
        setTitle("Hello, Swing!");  // заголовок окна
        setDefaultCloseOperation(EXIT_ON_CLOSE); // при закрытии
        setSize(500, 300); // размеры окна
        setLocationRelativeTo(null); // позиция на экране
        setVisible(true); // сделать видимым
    }
}

Запустив приведённый выше код, мы увидим пустое окно с текстом “Hello, Swing!” в заголовке. Оно будет подчиняться всем обычным манипуляциям: перемещение, изменение размера. Его можно закрыть, как любое другое, нажав “крестик” в правой верхней части окна.

Создать такое, написав минимум кода, мы смогли благодаря классу JFrame из библиотеки javax.swing и механизму наследования.

Класс JFrame содержит всё необходимое для создания окна приложения и управления им. Упрощённо мы можем назвать его “классом-окном”. Используя ключевое слово extends, мы связываем с ним наш класс в отношениях «предок-потомок». Наш класс является «потомком», а точнее – «наследником». И он получает «в наследство» все ресурсы JFrame: поля, методы и так далее.

Поэтому, когда мы создаём объект на основании нашего класса в методе main(), на экране возникает окно. Только оно невидимое, имеет нулевой размер. Однако, как мы помним, сразу после создания объекта выполняется метод-конструктор. Имя конструктора в точности повторяет имя класса. Наш конструктор содержит вызов пяти методов, унаследованных от JFrame. Они вставляют текст в заголовок окна, определяют действия при закрытии, его размеры и положение на экране. И, наконец, делают окно видимым. Подробное описание этих методов можно найти в документации.

Рисуем “случайные” шарики

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

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

Чтобы создать область для рисования, «холст», мы опишем внутренний класс Canvas, сделав его наследником класса JPanel. В этом случае появится возможность переопределить метод paint() класса-предка. В нём мы разместим свой код, сразу после вызова метода предка: super.paint(g). Аннотация @Override поможет не допустить ошибку в сигнатуре переопределяемого метода.

В конструкторе создадим объект canvas, назначив ему белый фон и заданные размеры. Затем добавим созданные объект в наше окно, с помощью метода add(). Метод pack() скорректирует размеры окна в соответствии с canvas.

import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Dimension;
 
public class RandomBalls extends JFrame {
 
    final String TITLE_OF_PROGRAM = "Random balls";
    final int WINDOW_WIDTH = 650;
    final int WINDOW_HEIGHT = 650;
 
    public static void main(String[] args) {
        new RandomBalls();
    }
 
    public RandomBalls() {
        setTitle(TITLE_OF_PROGRAM);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
 
        Canvas canvas = new Canvas();
        canvas.setBackground(Color.white);
        canvas.setPreferredSize(
            new Dimension(WINDOW_WIDTH, WINDOW_HEIGHT));
 
        add(canvas);
        pack();
        setLocationRelativeTo(null);
        setResizable(false);
        setVisible(true);
    }
 
    class Canvas extends JPanel {
        @Override
        public void paint(Graphics g) {
            super.paint(g);
            // рисуем окружности
        }
    }
}

Приведённый выше код дополним импортом класса Random, полем random и командой создания объекта random в конструкторе.

import java.util.Random; // импорт класса Random
Random random; // поле класса RandomBalls
random = new Random(); // создание объекта в конструкторе

К полям класса добавим массив цветов. Сейчас в нём только три цвета, но можно добавить ещё (константы находим в документации к классу Color).

final Color[] COLORS = {Color.red, Color.green, Color.blue};

Осталось добавить цикл в метод paint() после комментария. Изменяя цифры, можно варьировать количество окружностей и их размер. Приведённый ниже фрагмент нарисует 100 «случайных» окружностей: x и y – координаты левого верхнего угла квадрата, в который вписана окружность, а d – её диаметр. Цвета берутся из массива COLORS, каждая окружность имеет чёрную “обводку”.

for (int i = 0; i < 100; i++) {
    int d = random.nextInt(20) + 60;
    int x = random.nextInt(WINDOW_WIDTH - d);
    int y = random.nextInt(WINDOW_HEIGHT - d);
    Color color = COLORS[random.nextInt(COLORS.length)];
    g.setColor(color);
    g.fillOval(x, y, d, d);
    g.setColor(Color.black);
    g.drawOval(x, y, d, d);
}

Взаимодействуем с мышью

Добавим интерактивности, подключив взаимодействие с мышью. Потребуется импорт классов для обработки событий из библиотеки java.awt.event.

import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

Затем в конструкторе, после вызова метода setPreferredSize у объекта canvas, необходимо дописать следующий код.

canvas.addMouseListener(new MouseAdapter() {
    @Override
    public void mouseReleased(MouseEvent e) {
        canvas.repaint();
    }
});

Вызов метода addMouseListener() создаёт обработчик событий от мыши. Интересно, что параметром метода является не переменная, а исполняемый код. Фактически в скобках описывается анонимный класс, создаваемый наследованием от абстрактного MouseAdapter() и содержащий переопределённый метод mouseReleased(). Аннотация @Override помогает нам не ошибиться в имени метода. Метод будет вызываться всякий раз, когда мы будем отпускать (released) кнопку мыши. В теле метода одна строка кода, которая вызывает перерисовку канвы, а именно вызов paint() объекта canvas.

Запустите обновлённый код. Теперь по каждому щелчку мыши в окне программы картинка будет меняться. Как думаете, почему?

Используем «резиновый массив»

В процессе игры шарик должен исчезать после щелчка по нему мышью. В текущей версии кода это невозможно, так как наши шарики – всего лишь картинки. Поэтому их надо превратить в объекты, сохранить в списке, а затем взаимодействовать с ними.

Классический способ хранения набора однотипных переменных – массив. Но в нашей ситуации он не подходит, так как не позволяет добавлять и удалять элементы. Поэтому используем класс ArrayList, который подобен массиву, но при этом имеет методы для добавления и удаления элементов.

В качестве примера рассмотрим класс ArrayListExample, иллюстрирующий некоторые возможности ArrayList. Как видим, потребуется импорт из библиотеки java.util и создание объекта list, с указанием типа хранимых элементов (String).

import java.util.ArrayList;
 
class ArrayListExample {
	public static void main(String[] args) {
    	    ArrayList<String> list = new ArrayList<>();
    	    list.add("One");
    	    list.add("Two");
    	    list.add("Three");
    	    System.out.println(list);
    	    list.remove("Two");
    	    System.out.println(list);
	}
}

Сразу после создания списка он пуст. Затем мы добавляем в него три элемента и выводим в консоль. Затем удаляем средний элемент и снова выводим в консоль. Обратите внимание, как меняется размер списка. Теперь в нашем распоряжении настоящий “резиновый массив”. Более подробно о классе ArrayList можно прочесть в документации.

Превращаем шарики в объекты

Дополним наш код соответствующим импортом.

import java.util.ArrayList;

Объявим поле balls для хранения списка шариков.

ArrayList<Ball> balls;

Создадим соответствующий объект (подобно random) в конструкторе.

balls = new ArrayList<>();

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

class Ball {
    int x, y, d;
    Color color;
 
    Ball(int x, int y, int d, Color color) {
        this.x = x;
        this.y = y;
        this.d = d;
        this.color = color;
    }
 
    void paint(Graphics g) {
        g.setColor(color);
        g.fillOval(x, y, d, d);
        g.setColor(Color.black);
        g.drawOval(x, y, d, d);
    }
}

Класс Ball имеет четыре поля, согласно четырём параметрам шарика: координаты левого верхнего угла квадрата, куда он вписан, диаметр и цвет. Есть параметрический конструктор и также метод paint(), обеспечивающий отрисовку шарика.

Теперь напишем метод, который добавляет очередной объект-шарик (на основе класса Ball) в список. Его параметры (координаты, диаметр и цвет) получаем с помощью random, генерирующего случайные значения.

void addBall() {
    int d = random.nextInt(20) + 60;
    int x = random.nextInt(WIN_WIDTH - d);
    int y = random.nextInt(WIN_HEIGHT - d);
    Color color = COLORS[random.nextInt(COLORS.length)];
    balls.add(new Ball(x, y, d, color));
}

В конструктор впишем фрагмент, создающий при помощи цикла for нужное количество шариков. COUNT_BALLS необходимо добавить как финализированное поле с заданным значением.

for (int i = 0; i < COUNT_BALLS; i++) {
    addBall();
}

Цикл в переопределённом методе paint() класса Canvas, рисовавший случайные шарики, заменим на более простой, который работает со списком.

for (Ball ball : balls) {
    ball.paint(g);
}

Запустите обновлённый код. Видим знакомую картинку. Но теперь по каждому щелчку мыши меняться она не будет. Как думаете, почему? Сможете переписать код так, чтобы она менялась, как раньше?

Лопаем шарики

Теперь, когда шарики перестали быть просто картинками, а стали объектами, с ними можно что-то делать. Например, «лопать», кликая мышкой. Для этого добавим следующий метод.

void deleteBall(int x, int y) {
    for (int i = balls.size() - 1; i > -1; i--) {
        double dx = balls.get(i).x + balls.get(i).d/2 - x;
        double dy = balls.get(i).y  + balls.get(i).d/2 - y;
        double d = Math.sqrt(dx * dx + dy * dy);
        if (d < balls.get(i).d/2) {
            balls.remove(i);
            break;
        }
    }
}

Цикл for перебирает список, определяя: находится ли точка, заданная координатами в параметрах метода, внутри или снаружи шарика? Если внутри – шарик удаляется и перебор прекращается.

Остаётся добавить одну строку в метод mouseReleased(), перед методом перерисовки канвы. Это вызов deleteBall(), с передачей координат клика.

deleteBall(e.getX(), e.getY());

Запустите обновлённый код, задав достаточное количество шариков в поле COUNT_BALLS. При щелчке мышью по шарику он должен исчезать с экрана.

Идея игры

Имея  в распоряжении шарики-объекты, которые исчезают при клике по ним мышью, можно придумать и реализовать несколько игровых сценариев.

Например: на пустом игровом поле появляются «случайные» шарики. Удаляем их мышкой. После каждых 10 шариков скорость появления новых возрастает. Если на экране одновременно будет находиться 5 шариков – игра заканчивается. Код метода game(), представленный ниже – вариант такой реализации.

void game() {
    while (true) {
        addBall();
        if (balls.size() >= 5) {
            System.out.println("Game Over: " + counter);
            break;
        }
        canvas.repaint();
        counter++;
        if (counter % 10 == 0 && showDelay > 100) {
            showDelay -= 100;
        }
        try {
            Thread.sleep(showDelay);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Помимо добавления метода game(), необходимо внести и другие изменение. Во-первых, добавим счётчик создаваемых шаров counter и время задержки в миллисекундах showDelay как поля, задав им начальные значения.

int showDelay = 1000;
int counter = 0;

Во-вторых, уберём из конструктора фрагмент, создающий при помощи цикла for нужное количество шариков. В-третьих, в методе main() добавим вызов метода game() через точку, после команды создания объекта.

public static void main(String[] args) {
    new RandomBalls().game();
}

Запустите обновлённый код. На экране в случайных местах начнут появляться шарики разных размеров и цветов. «Лопайте» их, кликая мышкой. После каждых 10 шариков скорость их появления будет увеличиваться. Ваша задача – не допустить чтобы на экране одновременно находилось 5 шариков. Если это случится, то в консоль будет выведено сообщение об окончании игры.

Заключение

На всякий случай прилагаю мой telegram — @biblelamp. И, как и в предыдущих статьях, рекомендую почитать «Java-программирование для начинающих» Майка МакГрата и «Изучаем Java» Кэти Сьерра и Берта Бейтса.

Предыдущие статьи из серии «Быстрый старт с Java»:

Если язык Java вас заинтересовал — приглашаем на факультет Java-разработки. Если ещё не совсем уверены — посмотрите истории успеха наших Java-выпускников:

обучение javaкурсы javajava для начинающихигры на javaпрограммированиеjava
Нашли ошибку в тексте? Напишите нам.
Спасибо,
что читаете наш блог!