🎯 Цель работы:

Освоить событийно-ориентированный подход в разработке программ на пример создания click-игры

📋 Задание

Разработать игру “Лови шарик”

Минимальные требования

🏗️ Событийно-ориентированное программирование

Событие (Event) - это действие пользователя или изменение состояния системы.

Например, Клик мыши, Нажатие клавиши, Изменение размера окна, уведомление от таймера

JavaFX использует событийную модель, в которой

java
// 1. Анонимный класс
button.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Кнопка нажата");
}
});

// 2. Лямбда-выражение (Java 8+)
button.setOnAction(event -> {
System.out.println("Кнопка нажата");
});

// 3. Ссылка на метод
button.setOnAction(this::handleButtonClick);
  MOUSE_CLICKED      // Клик (нажатие + отпускание)
  MOUSE_PRESSED      // Нажатие кнопки мыши
  MOUSE_RELEASED     // Отпускание кнопки мыши
  MOUSE_MOVED        // Движение мыши
  MOUSE_DRAGGED      // Перетаскивание
  MOUSE_ENTERED      // Курсор вошёл в компонент
  MOUSE_EXITED       // Курсор вышел из компонента

Архитектура MVVM (Model-View-ViewModel) как автоматизация обработки событий

Модель (Model) — данные и бизнес-логика

Представление (View) — UI, отображение данных

Модель представления (ViewModel) — посредник между Model и View

MVVM дает автоматическое обновление UI через binding, т.е. НЕ нужно вручную (писать код) обновлять каждый элемент.

Этот механизм основан на Привязках (Binding) - объектах, реализующих интерфейс Рroperty.

Как работают привязки

// Создаём зависимость: A зависит от B
A.bind(B);

// Теперь при изменении B автоматически меняется A
B.set(10); // A автоматически становится 10
A.set(5);  // ОШИБКА! Нельзя менять связанное свойство

Используйте события для:

Используйте привязки для:

Последовательность разработки

Подзадача 1 Прыгающий шарик и обработка событий

  1. Создайте новый JavaFX проект, в View создайте панель gamePane <Pane fx:id="gamePane" prefWidth="800" prefHeight="600"> и шарик <Circle fx:id="ball" radius="30" fill="red"/>
  2. В контроллере GameController.java установите шарик в середину панели
    public class GameController {
     @FXML private Pane gamePane;
     @FXML private Circle ball;
        
     public void setGame() {
         // Шарик в центре панели
         ball.setCenterX(gamePane.getWidth() / 2);
         ball.setCenterY(gamePane.getHeight() / 2);
     }
    }
    
  3. Добавьте обработку нажатий на шарик в контроллер:
     @FXML
     private void handleMouseClick(MouseEvent event) {
         // Проверка попадания
         double dx = event.getX() - ball.getCenterX();
         double dy = event.getY() - ball.getCenterY();
         double distance = Math.sqrt(dx * dx + dy * dy);
            
         if (distance <= ball.getRadius()) {
             // При попадании перемещение шарика
             moveBallToRandomPosition();
         }
     }
        
     private void moveBallToRandomPosition() {
         double newX = ball.getRadius() + 
                      random.nextDouble() * (gamePane.getWidth() - 2 * ball.getRadius());
         double newY = ball.getRadius() + 
                      random.nextDouble() * (gamePane.getHeight() - 2 * ball.getRadius());
            
         ball.setCenterX(newX);
         ball.setCenterY(newY);
     }
    
  4. Добейтесь, чтобы при клике на шарик он перемещался в случайное место

Подзадача 2 Отображаемый счетчик состояний

Добавить счётчик, который увеличивается при попадании и отражается. Заменить обычную переменную-счетчик score на специальный объект IntegerProperty

В контроллер включите:

public class GameController {
    // ... другие поля
    
    // Вместо: private int score = 0;
    private IntegerProperty score = new SimpleIntegerProperty(0);
    
    public void initialize() {
               
        // ОДИН РАЗ устанавливаем привязку
        scoreLabel.textProperty().bind(
            javafx.beans.binding.Bindings.concat("Счет: ", score)
        );
    }
    
    @FXML
    private void handleMouseClick(MouseEvent event) {
       
       
           if (distance <= ball.getRadius()) {
            // Просто меняем значение - UI обновится САМ
            score.set(score.get() + 1);
            
            moveBallToRandomPosition();
        }
    }
}

Текстовая метка scoreLabel должна быть добавлена во View для отображения счета(объект Label)

Подзадача 3 Автоматизация движения шарика

Что сделать

  1. Использовать таймер для обновления положения шарика и его отображения

  2. Привязать шарик к координатам, отделив модель игры от контроллера

Как сделать

Чтобы обновлять картинку, надо знать когда это делать, т.е. нужен объект, который “кликает по шарику” Без таймера шарик стоит на месте. С таймером каждые 0.05 секунды:

Как сделать таймер

Timer/TimerTask

// создали таймер
timer = new Timer("GameTimer", true);
// создали задачу для таймера
TimerTask task = new TimerTask() {
    
// метод run() вызывается каждые delay мс для перемещния шарика
   
    public void run() {

        if(gameActive.get()) {
            moveBallRandomly();
        }

    }

};
// создали расписание запуска задачи
timer.schedule(task, delay, period);

Варианты индивидуальных заданий

  1. Сделайте так, чтобы шарик “убегал” от курсора мыши. Когда мышь приближается к шарику на расстояние меньше 100 пикселей, шарик должен двигаться в противоположную сторону. Подсказка: Используй setOnMouseMoved и меняй направление скорости.
  2. При попадании по шарику он должен менять цвет на случайный. Используй Color.rgb() для генерации случайного цвета.
  3. Каждые 5 попаданий увеличивайте скорость шарика на 10%. Сделай так, чтобы игрок видел текущую скорость на экране.
  4. Если игрок попадает по шарику несколько раз подряд (без промахов), давай дополнительные очки
  5. Добавьте таймер на 60 секунд. Игрок должен набрать как можно больше очков за это время. Когда время закончится, покажи надпись “Игра окончена!”.
  6. При наведении мыши на шарик (MOUSE_ENTERED без нажатия) замедляйте его движение в 2 раза. Когда мышь выходит из компонента - скорость возвращается.
  7. Сделайте так, чтобы раз в 10 секунд шарик становится полупрозрачным на 3 секунды. Попасть по нему в это время сложнее, но даёт в 2 раза больше очков.
  8. Добавьте по двойному клику смену траектории движения шарика со случайной на волнообразную и обратно. Формула: y = 300 + 100 * Math.sin(x/50)
  9. Сделайте так, чтобы при зажатой левой кнопке мыши шарик притягивался к курсору. Отпустил кнопку - шарик оттолкнётся
  10. Сделай возможность остановить и возобновить игру

Результаты работы программы (Работоспособность)

  1. Автоматическое движение шарика и обновление счета при попадании по шарику
  2. Остановка игры при двойном клике на шарик (игра останавливается, но не перезапускается, счет не сбрасывается)
  3. Отображение счета и статуса состояния игры (активна/неактивна), возможность начать новую игру в UI (изменить игровые параметры).
  4. Отражение от границ поля (реагирует на изменение поля), не попадает на элементы UI
  5. Модель игры включает игровую механику (правила движения шарика), правила игры Gameplay (система очков, уровень сложности), состояние игры (Данные и Свойства)

Структура проекта

GameApplication (extends Application)

├── GameModel (данные игры)
│   ├── scoreProperty: IntegerProperty
│   ├── gameActiveProperty: BooleanProperty
│   ├── ballSpeedProperty: IntegerProperty (интервал в мс)
│   └── ballPosition: Point2D.DoubleProperty
│   └── timer: Timer
│   └── gameLogic методы
├── GameView (UI)
│   ├── gamePane: Pane (игровое поле)
│   ├── ball: Circle (Shape компонент)
│   │   ├── fill: RadialGradient (для объема)
│   │   ├── stroke: цвет обводки
│   │   └── centerX/Y: привязка к свойствам модели
│   ├── scoreLabel: Label (привязка к scoreProperty)
│   └── statusLabel: Label (привязка к gameActiveProperty)
└── GameController (логика и обработка)
│   ├── eventHandlers
│   ├── gameModel: GameModel
└── 

Чек-лист верификации кода проекта заданной архитектуре MVVM

Model:

View:

Controller:

✅ Критерии оценивания

Критерий Балл Описание  
  Работоспособность 1 см.Результаты работы
  Правильность 1 Модель игры выделена в отдельный класс и не связана с view, Обработка не менее 2 событий мыши, Связывание данных через Property и Binding, таймер корректно остановлен
  Механика движения 1 Шарик “убегает”/ имеет траекторию / изменяет внешний вид по событию / индивидуальный вариант
  Индивидуальность 1 включена дополнительная опция механики, уровня сложности или других правил игры (не UI и клики)
  Соблюдение дедлайна 1 Сдача вовремя