2D игра*
Выполнять это задание необязательно, оно добавлено для тех, кто планирует писать свою игру.
В этой главе мы напишем простую игру. На правом крае экрана появляются враги и с увеличивающейся скоростью летят в сторону игрока. Игрок, нажимая на пробел, должен перепрыгивать врагов.
Исходники игры можно скачать здесь
Прототип
Для начала сделаем форк приложения, созданного в предыдущих главах. Переходим в его и жмём
Дождёмся, пока репозиторийй скопируется
Репозиторий скопировался
Чтобы поменять название, зайдите в настройки репозитория Settings
Название меняется в разделе General
Теперь откроем этот репозиторий в Idea
Пока что дерево проекта выглядит так
Добавим в пакет panels
класс PanelGame
- PanelGame.java
package panels;
import controls.Label;
import controls.MultiLineLabel;
import io.github.humbleui.jwm.Window;
import io.github.humbleui.skija.Canvas;
import misc.CoordinateSystem2i;
/**
* Панель игры
*/
public class PanelGame extends Panel {
/**
* Заголовок с информацией
*/
private final MultiLineLabel infoLabel;
/**
* Заголовок подсказки
*/
private final Label hintLabel;
/**
* Конструктор панели
*
* @param window окно
* @param drawBG нужно ли рисовать подложку
* @param backgroundColor цвет фона
* @param padding отступы
*/
public PanelGame(Window window, boolean drawBG, int backgroundColor, int padding) {
super(window, drawBG, backgroundColor, padding);
// создаём заголовок
infoLabel = new MultiLineLabel(window, false, backgroundColor, 0,
7, 7, 6, 0, 1, 1,
"Информация", true, true);
// создаём заголовок
hintLabel = new Label(window, false, backgroundColor, 0,
7, 7, 0, 0, 3, 1,
"Для прыжка нажмите ПРОБЕЛ", true, true);
}
/**
* Метод под рисование в конкретной реализации
*
* @param canvas область рисования
* @param windowCS СК окна
*/
@Override
public void paintImpl(Canvas canvas, CoordinateSystem2i windowCS) {
// рисуем заголовок информации
infoLabel.paint(canvas, windowCS);
// рисуем заголовок подсказки
hintLabel.paint(canvas, windowCS);
}
}
Пока что в ней просто два заголовка. Один выводит подсказку, второй будет выводить информацию о текущем состоянии игры.
Из панелей прошлого задания нам понадобятся только Panel
и GridPanel
. Поэтому
удалим остальные. Из пакета dialogs
нам будет нужна только PanelInfo
. Поэтому
также удалим класс PanelSelectFile
. Ещё нам не пригодится классы Task
и Point
из пакета app
. Их тоже удалим.
Idea предупредит нас, что удаляемые нами классы где-то задействованы. Жмём Delete Anyway
Теперь у нас два класса с ошибками
Очистим класс Application от всех лишних элементов.
- Application.java
package app;
import controls.InputFactory;
import dialogs.PanelInfo;
import io.github.humbleui.jwm.*;
import io.github.humbleui.jwm.skija.EventFrameSkija;
import io.github.humbleui.skija.Canvas;
import io.github.humbleui.skija.Surface;
import misc.CoordinateSystem2i;
import panels.PanelGame;
import java.io.File;
import java.util.Calendar;
import java.util.Date;
import java.util.function.Consumer;
import static app.Colors.*;
/**
* Класс окна приложения
*/
public class Application implements Consumer<Event> {
/**
* Режимы работы приложения
*/
public enum Mode {
/**
* Основной режим работы
*/
WORK,
/**
* Окно информации
*/
INFO
}
/**
* окно приложения
*/
private final Window window;
/**
* отступ приложения
*/
public static final int PANEL_PADDING = 5;
/**
* радиус скругления элементов
*/
public static final int C_RAD_IN_PX = 4;
/**
* кнопка изменений: у мака - это `Command`, у windows - `Ctrl`
*/
public static final KeyModifier MODIFIER = Platform.CURRENT == Platform.MACOS ? KeyModifier.MAC_COMMAND : KeyModifier.CONTROL;
/**
* время последнего нажатия клавиши мыши
*/
Date prevEventMouseButtonTime;
/**
* флаг того, что окно развёрнуто на весь экран
*/
private boolean maximizedWindow;
/**
* Панель информации
*/
private final PanelInfo panelInfo;
/**
* Текущий режим(по умолчанию рабочий)
*/
public static Mode currentMode = Mode.WORK;
/**
* Панель игры
*/
private final PanelGame panelGame;
/**
* Конструктор окна приложения
*/
public Application() {
// создаём окно
window = App.makeWindow();
// панель информации
panelInfo = new PanelInfo(window, true, DIALOG_BACKGROUND_COLOR, PANEL_PADDING);
// панель игры
panelGame = new PanelGame(window, true, DIALOG_BACKGROUND_COLOR, PANEL_PADDING);
// задаём обработчиком событий текущий объект
window.setEventListener(this);
// задаём заголовок
window.setTitle("Java 2D");
// задаём размер окна
window.setWindowSize(900, 900);
// задаём его положение
window.setWindowPosition(100, 100);
// задаём иконку
switch (Platform.CURRENT) {
case WINDOWS -> window.setIcon(new File("src/main/resources/windows.ico"));
case MACOS -> window.setIcon(new File("src/main/resources/macos.icns"));
}
// названия слоёв, которые будем перебирать
String[] layerNames = new String[]{
"LayerGLSkija", "LayerRasterSkija"
};
// перебираем слои
for (String layerName : layerNames) {
String className = "io.github.humbleui.jwm.skija." + layerName;
try {
Layer layer = (Layer) Class.forName(className).getDeclaredConstructor().newInstance();
window.setLayer(layer);
break;
} catch (Exception e) {
System.out.println("Ошибка создания слоя " + className);
}
}
// если окну не присвоен ни один из слоёв
if (window._layer == null)
throw new RuntimeException("Нет доступных слоёв для создания");
// делаем окно видимым
window.setVisible(true);
}
/**
* Обработчик событий
*
* @param e событие
*/
@Override
public void accept(Event e) {
// если событие кнопка мыши
if (e instanceof EventMouseButton) {
// получаем текущие дату и время
Date now = Calendar.getInstance().getTime();
// если уже было нажатие
if (prevEventMouseButtonTime != null) {
// если между ними прошло больше 200 мс
long delta = now.getTime() - prevEventMouseButtonTime.getTime();
if (delta < 200)
return;
}
// сохраняем время последнего события
prevEventMouseButtonTime = now;
}
// кнопки клавиатуры
else if (e instanceof EventKey eventKey) {
// кнопка нажата с Ctrl
if (eventKey.isPressed()) {
if (eventKey.isModifierDown(MODIFIER))
// разбираем, какую именно кнопку нажали
switch (eventKey.getKey()) {
case W -> window.close();
case H -> window.minimize();
case DIGIT1 -> {
if (maximizedWindow)
window.restore();
else
window.maximize();
maximizedWindow = !maximizedWindow;
}
case DIGIT2 -> window.setOpacity(window.getOpacity() == 1f ? 0.5f : 1f);
}
else
switch (eventKey.getKey()) {
case ESCAPE -> {
if (currentMode.equals(Mode.WORK)) {
window.close();
// завершаем обработку, иначе уже разрушенный контекст
// будет передан панелям
return;
}
}
case TAB -> InputFactory.nextTab();
}
}
}
// если событие - это закрытие окна
else if (e instanceof EventWindowClose) {
// завершаем работу приложения
App.terminate();
// закрытие окна
} else if (e instanceof EventWindowCloseRequest) {
window.close();
} else if (e instanceof EventFrameSkija ee) {
Surface s = ee.getSurface();
paint(s.getCanvas(), new CoordinateSystem2i(0, 0, s.getWidth(), s.getHeight())
);
} else if (e instanceof EventFrame) {
// запускаем рисование кадра
window.requestFrame();
}
switch (currentMode) {
case INFO -> panelInfo.accept(e);
case WORK -> panelGame.accept(e);
}
}
/**
* Рисование
*
* @param canvas низкоуровневый инструмент рисования примитивов от Skija
* @param windowCS СК окна
*/
public void paint(Canvas canvas, CoordinateSystem2i windowCS) {
// запоминаем изменения (пока что там просто заливка цветом)
canvas.save();
// очищаем канвас
canvas.clear(APP_BACKGROUND_COLOR);
// рисуем панели
panelGame.paint(canvas, windowCS);
canvas.restore();
// рисуем диалоги
switch (currentMode) {
case INFO -> panelInfo.paint(canvas, windowCS);
}
}
}
В классе CoordinateSystem2d
заменим PanelLog.warning()
на System.out.println()
/**
* Масштабировать СК пропорционально
*
* @param s коэффициент
* @param center центр масштабирования
*/
public void scale(double s, Vector2d center) {
// если центр масштабирования находится вне СК
if (!checkCoords(center)) {
System.out.println("центр масштабирования находится вне области");
return;
}
...
Теперь очистим Unit-тесты
- Application.java
import org.junit.Test;
/**
* Класс тестирования
*/
public class UnitTest {
/**
* Первый тест
*/
@Test
public void test1() {
assert true;
}
/**
* Второй тест
*/
@Test
public void test2() {
assert true;
}
}
Запустим игру
Класс игры
Напишем теперь класс игры. Создадим пакет games
и добавим в него класс Game
. Т.к. игра у нас может быть всего одна, то логику работы этого класса
мы построем на основе паттерна проектирования Singleton
(синглтон).
Для того чтобы создать синглтон, нужно создать закрытое статическое поле с объектом самого класса, а также
закрытый конструктор. Этот конструктор вызывается из статического метода get()
в котором если
поле, хранящее объект этого класса, не инициализировано, то создаётся новый объект класса, сохраняется
в это поле и возвращается. Если поле уже инициализировано, то оно просто возвращается.
- Game.java
package game;
import io.github.humbleui.skija.Canvas;
import misc.CoordinateSystem2d;
import misc.CoordinateSystem2i;
import java.util.*;
/**
* Синглтон класс игры
*/
public class Game {
/**
* СК игры
*/
private final CoordinateSystem2d ownCS;
/**
* Результат игры
*/
private double score;
/**
* Последняя полученная СК окна
*/
private CoordinateSystem2i lastWindowCS;
/**
* Предыдущее время обработки
*/
private static Date prevTime;
/**
* Флаг, остановлена ли игра
*/
private boolean paused;
/**
* сама игра
*/
private static Game thisGame;
/**
* Конструктор игры
*/
private Game() {
// инициализируем СК игры
ownCS = new CoordinateSystem2d(-50, -300, 1000, 1000);
// перезапускаем игру
restart();
}
/**
* Получить ссылку на игру
*
* @return синглтон игры
*/
public static Game getGame() {
if (thisGame == null)
thisGame = new Game();
return thisGame;
}
/**
* Перезапустить игру
*/
public void restart() {
// результат равен 0
score = 0;
// игра запущена
paused = false;
}
/**
* Получить время с прошлого вызова
*
* @return сколько времени прошло с момента прошлого вызова этого метода
*/
private static double getDeltaTime() {
// если время ещё ни разу не сохранялось
if (prevTime == null) {
// сохраняем его
prevTime = Calendar.getInstance().getTime();
// возвращаем 0
return 0;
}
// получаем текущие дату и время
Date now = Calendar.getInstance().getTime();
// получаем, сколько времени прошло с прошлого запуска в миллисекундах
long delta = now.getTime() - prevTime.getTime();
// сохраняем время запуска
prevTime = now;
// возвращаем время в секундах
return (double) delta / 1e3;
}
/**
* Обработка игры
*/
public void process() {
// если игра на паузе
if (paused)
// останавливаем её обработку
return;
// рассчитываем, сколько времени прошло с прошлой обработки
double dt = getDeltaTime();
}
/**
* Получить результат
*
* @return результат
*/
public double getScore() {
return score;
}
/**
* Рисование задачи
*
* @param canvas область рисования
* @param windowCS СК окна
*/
public void paint(Canvas canvas, CoordinateSystem2i windowCS) {
// сохраняем область рисования
canvas.save();
// восстанавливаем область рисования
canvas.restore();
// сохраняем СК экрана
lastWindowCS = windowCS;
}
/**
* Завершение игры
*/
private void gameOver() {
// останавливаем игру
paused = true;
}
}
Часть методов пока что ничего не делают, это просто заглушки, чтобы потом их дописать.
Почти вся инициализация перенесена в метод restart()
, чтобы потом было проще перезапускать игру.
Обычно в играх разносят логику рисования и логику обработки модели, на основе которой рисование выполняется. Это делается для того, чтобы обработка могла выполняться асинхронно по отношению к рисованию. Асинхронность подразумевает то, что в программе создаётся два процесса, один из которых рисует так часто, как только может, а второй раз в заданный период обрабатывает изменения мира.
В данной игре для простоты мы разнесём логику рисования и обработки, но будем запускать их последовательно в методе рисования панели.
Теперь перейдём в класс PanelGame
и добавим ему поле игры
/**
* Игра
*/
public static final Game game = Game.getGame();
и добавим вызов р игры
/**
* Метод под рисование в конкретной реализации
*
* @param canvas область рисования
* @param windowCS СК окна
*/
@Override
public void paintImpl(Canvas canvas, CoordinateSystem2i windowCS) {
// рисуем заголовок информации
infoLabel.paint(canvas, windowCS);
// рисуем заголовок подсказки
hintLabel.paint(canvas, windowCS);
// обрабатываем игру
game.process();
// рисуем игру
game.paint(canvas, windowCS);
}
Пока что ничего не изменилось
Для работы с игровыми объектами нам понадобится умножение вектора на скаляр. Для этого в классе
Vector2d
допишем метод mult()
/**
* Умножение вектора на число
*
* @param s число
* @return новый вектор
*/
public Vector2d mult(double s) {
return new Vector2d(s * x, s * y);
}
Теперь в пакете game
пропишем игровой объект GameObject
. От него потом мы будем наследоваться в классах игрока и врагов.
- GameObject.java
package game;
import io.github.humbleui.skija.Canvas;
import io.github.humbleui.skija.Paint;
import io.github.humbleui.skija.RRect;
import misc.CoordinateSystem2d;
import misc.CoordinateSystem2i;
import misc.Vector2d;
import misc.Vector2i;
/**
* Игровой объект
*/
public class GameObject {
/**
* Положение
*/
public Vector2d pos;
/**
* Скорость
*/
public Vector2d speed;
/**
* Ускорение
*/
public Vector2d acc;
/**
* Размер
*/
private final float size;
/**
* Цвет
*/
private final int color;
/**
* Конструктор игрового объекта
*
* @param pos положение
* @param speed скорость
* @param acc ускорение
* @param size размер
* @param color цвет
*/
public GameObject(Vector2d pos, Vector2d speed, Vector2d acc, float size, int color) {
this.pos = pos;
this.speed = speed;
this.acc = acc;
this.size = size;
this.color = color;
}
/**
* Метод обработки
* <p>
* При наследовании необходимо вызывать обработку предка super.process(dt)
*
* @param dt изменение времени
*/
public void process(double dt) {
pos.add(speed.mult(dt));
speed.add(acc.mult(dt));
}
/**
* Рисование
*
* @param canvas область рисования
* @param windowCS СК окна
* @param ownCS СК игры
*/
public void paint(Canvas canvas, CoordinateSystem2i windowCS, CoordinateSystem2d ownCS) {
try (Paint paint = new Paint().setColor(color)) {
Vector2i windowPos = windowCS.getCoords(pos, ownCS);
canvas.drawRRect(RRect.makeXYWH(
windowPos.x - size / 2,
windowCS.getMax().y - windowPos.y - size / 2,
size, size, 4), paint);
}
}
}
Теперь добавим простейшие классы игрока и врагов
- Player.java
- Enemy.java
package game;
import misc.Misc;
import misc.Vector2d;
/**
* Класс игрока
*/
public class Player extends GameObject {
/**
* Конструктор игрового объекта
*
* @param pos положение
* @param speed скорость
* @param acc ускорение
* @param size размер
*/
public Player(Vector2d pos, Vector2d speed, Vector2d acc, float size) {
super(pos, speed, acc, size, Misc.getColor(150, 50, 200, 50));
}
}
package game;
import misc.Misc;
import misc.Vector2d;
/**
* Класс противника
*/
public class Enemy extends GameObject {
/**
* Конструктор противника
*
* @param pos положение
* @param speed скорость
* @param acc ускорение
* @param size размер
*/
public Enemy(Vector2d pos, Vector2d speed, Vector2d acc, float size) {
super(pos, speed, acc, size, Misc.getColor(200, 150, 50, 50));
}
}
Игрок
Добавим теперь поле игрока в класс игры Game
:
/**
* Игрок
*/
private Player player;
Также добавим константу размера игрока
/**
* Размер игрока
*/
private static final float PLAYER_SIZE = 40;
Теперь инициализируем игрока в методе restart
...
// создаём игрока, на которого действует гравитация по оси X
player = new Player(
new Vector2d(0, 0), new Vector2d(0, 0), new Vector2d(0, -9000), PLAYER_SIZE
);
...
Также добавим рисование игрока:
/**
* Рисование задачи
*
* @param canvas область рисования
* @param windowCS СК окна
*/
public void paint(Canvas canvas, CoordinateSystem2i windowCS) {
// сохраняем область рисования
canvas.save();
// рисуем игрока
player.paint(canvas, windowCS, ownCS);
// восстанавливаем область рисования
canvas.restore();
// сохраняем СК экрана
lastWindowCS = windowCS;
}
Теперь на экране появился игрок
Чтобы игрок начал прыгать, добавим метод up()
в класс игры, который будет просто сообщать игроку
вертикальную скорость.
Для этого сначала введём соответствующую константу
/**
* Начальная скорость игрока при прыжке
*/
private static final float PLAYER_JUMP_SPEED = 3000;
а потом пропишем метод up()
/**
* Прыжок
*/
public void up() {
if (player.pos.y == 0)
player.speed.y = PLAYER_JUMP_SPEED;
}
Также пропишем метод обработки игрока procssPlayer()
и вызовем его из process()
/**
* Обработка игры
*/
public void process() {
// если игра на паузе
if (paused)
// останавливаем её обработку
return;
// рассчитываем, сколько времени прошло с прошлой обработки
double dt = getDeltaTime();
// обработка игрока
processPlayer(dt);
}
/**
* Обработка игрока
*
* @param dT сколько времени прошло, с прошлой обработки в секундах
*/
private void processPlayer(double dT) {
// обрабатываем игрока
player.process(dT);
// если игрок должен "уйти под землю"
if (player.pos.y <= 0) {
// обнуляем его положение
player.pos.y = 0;
// обнуляем скорость
player.speed = new Vector2d(0, 0);
}
}
Теперь вызовем его из класса Application
...
else if (e instanceof EventKey eventKey) {
// кнопка нажата с Ctrl
if (eventKey.isPressed()) {
if (eventKey.isModifierDown(MODIFIER))
// разбираем, какую именно кнопку нажали
switch (eventKey.getKey()) {
case W -> window.close();
case H -> window.minimize();
}
else
switch (eventKey.getKey()) {
case W, SPACE -> PanelGame.game.up();
case ESCAPE -> window.close();
}
}
}
...
Прыжок заработал
Враги
Добавим константы врагов
/**
* Размер противника
*/
private static final float ENEMY_SIZE = 40;
/**
* Ускорение врагов
*/
private static final float ENEMY_ACC = 5.0f;
/**
* Ускорение врагов
*/
private static final float ENEMY_START_SPEED = 300.0f;
Теперь добавим переменные врагов
/**
* Враги
* хранятся в LinkedList, потому что часто удаляются и добавляются
*/
private final List<Enemy> enemies = new LinkedList<>();
/**
* Скорость у новых врагов при появлении
*/
private float newEnemySpeed;
В restart()
добавим инициализацию врагов
...
// скорость врага берётся со знаком минус, т.к. враги
// формируются на правом краю экрана и движутся влево,
// т.е. против оси X
newEnemySpeed = -ENEMY_START_SPEED;
// очищаем список врагов
enemies.clear();
...
Допишем геттер
/**
* Получить скорость нового врага
*
* @return скорость нового врага
*/
public float getNewEnemySpeed() {
return Math.abs(newEnemySpeed);
}
Также пропишем обработку врагов
/**
* Обрабатываем врагов
*
* @param dT время, прошедшее с предыдущей обработки в секундах
*/
private void processEnemies(double dT) {
// составляем список врагов, которых уже можно удалить, потому что
// удалять элементы из того же списка, который читаем нельзя
List<Enemy> enemiesToRemove = new ArrayList<>();
// перебираем врагов
for (Enemy enemy : enemies) {
// если враг выходит за СК игры
if (!ownCS.checkCoords(enemy.pos)) {
// добавляем его в список на удаление
enemiesToRemove.add(enemy);
}
}
// удаляем всех игроков
enemies.removeAll(enemiesToRemove);
// запускаем обработку всех оставшихся врагов
for (Enemy enemy : enemies)
enemy.process(dT);
// довольно редко срабатывающий рандом
if (Math.random() < 0.01)
// добавляем нового врага
addEnemy();
// увеличиваем скорость нового врага на
newEnemySpeed -= ENEMY_ACC * dT;
}
/**
* Добавить врага
*/
private void addEnemy() {
if (lastWindowCS == null)
return;
if (!enemies.isEmpty() && enemies.get(enemies.size() - 1).pos.x > ownCS.getSize().x / 2) {
return;
}
enemies.add(new Enemy(
new Vector2d(ownCS.getMax().x - 20, 0),
new Vector2d(newEnemySpeed, 0),
new Vector2d(ENEMY_ACC, 0),
ENEMY_SIZE
));
}
Добавим обработку игроков в process()
/**
* Обработка игры
*/
public void process() {
// если игра на паузе
if (paused)
// останавливаем её обработку
return;
// рассчитываем, сколько времени прошло с прошлой обработки
double dt = getDeltaTime();
// обработка игрока
processPlayer(dt);
processEnemies(dt);
}
Теперь нам осталось добавить рисование врагов
/**
* Рисование задачи
*
* @param canvas область рисования
* @param windowCS СК окна
*/
public void paint(Canvas canvas, CoordinateSystem2i windowCS) {
// сохраняем область рисования
canvas.save();
// рисуем игрока
player.paint(canvas, windowCS, ownCS);
// рисуем врагов
for (Enemy enemy : enemies) {
enemy.paint(canvas, windowCS, ownCS);
}
// восстанавливаем область рисования
canvas.restore();
// сохраняем СК экрана
lastWindowCS = windowCS;
}
Теперь враги появляются время от времени и движутся со всё более высокой скоростью.
Панель информации
Переименуем теперь класс PanelInfo
в PanelGameInfo
и немного модифицируем его. Панель информации
из прошлого приложения просто скрывалась при клике по кнопке, а эта панель будет выполнять
те действия, которые будут переданы в анонимном классе onClick
- PanelGameInfo.java
package dialogs;
import app.Application;
import controls.Button;
import controls.MultiLineLabel;
import io.github.humbleui.jwm.*;
import io.github.humbleui.skija.Canvas;
import misc.CoordinateSystem2i;
import misc.Vector2i;
import panels.Panel;
import static app.Colors.BUTTON_COLOR;
/**
* Панель управления
*/
public class PanelGameInfo extends Panel {
/**
* Отступы в панели управления
*/
private static final int CONTROL_PADDING = 5;
/**
* Кнопка принять
*/
private final Button accept;
/**
* заголовок
*/
private final MultiLineLabel infoLabel;
/**
* текст заголовка делаем статическим, чтобы можно было менять его
* из любого места, каждый экземпляр панели информации будет обращаться
* к этому полю при рисовании, но логика работы программы
* не предполагает создания нескольких экземпляров, так что всё ок
*/
private static String labelText;
/**
* Обработчик нажатия кнопки ОК на панели
*/
Runnable onClick;
/**
* Панель управления
*
* @param window окно
* @param drawBG флаг, нужно ли рисовать подложку
* @param color цвет подложки
* @param padding отступы
*/
public PanelGameInfo(Window window, boolean drawBG, int color, int padding,
Runnable onClick) {
super(window, drawBG, color, padding);
// добавление вручную
infoLabel = new MultiLineLabel(window, false, backgroundColor, CONTROL_PADDING,
1, 2, 0, 0, 1, 1, "",
true, true);
accept = new Button(
window, false, BUTTON_COLOR, CONTROL_PADDING,
1, 2, 0, 1, 1, 1, "ОК",
true, true);
accept.setOnClick(onClick);
this.onClick = onClick;
}
/**
* Вывести информацию
*
* @param text текст
*/
public static void show(String text) {
// задаём новый текст
labelText = text;
// переключаем вывод приложения на режим информации
Application.currentMode = Application.Mode.INFO;
}
/**
* Обработчик событий
*
* @param e событие
*/
@Override
public void accept(Event e) {
// вызываем обработчик предка
super.accept(e);
// событие движения мыши
if (e instanceof EventMouseMove ee) {
accept.checkOver(lastWindowCS.getRelativePos(new Vector2i(ee)));
// событие нажатия мыши
} else if (e instanceof EventMouseButton) {
if (!lastInside)
return;
Vector2i relPos = lastWindowCS.getRelativePos(lastMove);
accept.click(relPos);
// перерисовываем окно
window.requestFrame();
// обработчик ввода текста
} else if (e instanceof EventKey ee) {
if (ee.isPressed()) {
// получаем код клавиши
Key key = ee.getKey();
// перебираем варианты
switch (key) {
// если esc
case ESCAPE -> onClick.run();
// если enter
case ENTER -> onClick.run();
}
}
}
}
/**
* Метод под рисование в конкретной реализации
*
* @param canvas область рисования
* @param windowCS СК окна
*/
@Override
public void paintImpl(Canvas canvas, CoordinateSystem2i windowCS) {
infoLabel.text = labelText;
accept.paint(canvas, windowCS);
infoLabel.paint(canvas, windowCS);
}
}
Мы будем показывать панель информации, когда игрок проиграл.
Для начала нам нужно написать в классе Vectord2d
метод, рассчитывающий расстояние между двумя точками.
/**
* Получить расстояние между векторами
*
* @param a первый вектор
* @param b второй вектор
* @return расстояние
*/
public static double distance(Vector2d a, Vector2d b) {
double dx = b.x - a.x;
double dy = b.y - a.y;
return Math.sqrt(dx * dx + dy * dy);
}
Теперь в классе игры Game
в обработке игрока processPlayer()
добавим проверку, не сталкивается ли он с
тем или иным врагом
...
// перебираем врагов
for (Enemy enemy : enemies)
// если есть контакт с врагом
if (Vector2d.distance(player.pos, enemy.pos) < (PLAYER_SIZE + ENEMY_SIZE) / 2)
// оканчиваем игру
gameOver();
// вычитаем из результата пройденное расстояние самым быстрым врагом
// просто само расстояние как функция от скорости отрицательное
// поэтому для увеличения положительного результата мы
// вычетаем его
score -= newEnemySpeed * dT;
...
Также добавим вывод информации на панель в методе gameOver()
/**
* Завершение игры
*/
private void gameOver() {
// останавливаем игру
paused = true;
// выводим панель информации
PanelGameInfo.show("Игра окончена\nВаш результат: " + String.format("%.1f", score));
}
Панель информации работает
Теперь нам осталось только дописать вывод информации во время игры, для этого будем задавать текст заголовка прямо в рисовании панели
/**
* Метод под рисование в конкретной реализации
*
* @param canvas область рисования
* @param windowCS СК окна
*/
@Override
public void paintImpl(Canvas canvas, CoordinateSystem2i windowCS) {
// задаём текст заголовка
infoLabel.text = String.format(
"Пройдено %.1f\n Скорость: %.1f", game.getScore(), game.getNewEnemySpeed()
);
// рисуем заголовок информации
infoLabel.paint(canvas, windowCS);
// рисуем заголовок подсказки
hintLabel.paint(canvas, windowCS);
// обрабатываем игру
game.process();
// рисуем игру
game.paint(canvas, windowCS);
}
Всё готово
Исходники игры можно скачать здесь