Если вы искали обучающее руководство по воссозданию классической игры “Пинг-понг”, поздравляю — вы его нашли! Для реализации этой цели воспользуемся библиотекой Processing и Java.
1. Скачивание и установка Processing
Переходим по ссылке, скачиваем Processing и выбираем версию в соответствии с платформой.
2. Что такое Processing?
Processing предоставляет графическую библиотеку и интегрированную среду разработки (IDE). Использует язык программирования Java.
3. Требования к игре
Требования к игре представлены в двух частях. Первая из них описывает требования к визуальному оформлению: изображение игроков, мяча и игрового пространства. Вторая часть знакомит с функциональными требованиями: поведение различных объектов и приложения.
3.1 Требования к визуальному оформлению игры
- Объект
player
(игрок) представляет собой прямоугольник. - Объект
ball
(мяч) изображен в виде круга. - Игра содержит 2 текстовых элемента, отображающих набранные очки каждого игрока.
3.1 Функциональные требования
- Объект
player
перемещается только по осиy
. - Объект
ball
перемещается по осямx
иy
. - При столкновении с объектом
player
объектball
отскакивает в противоположную сторону по осиx
. - При выходе за границы экрана по оси
y
объектball
меняет направление движения по осиy
на противоположное. - При выходе за границы экрана по оси
x
положение и направление объектаball
сбрасываются. - Объект
player
зарабатывает очко при каждом выходе объектаball
за границы экрана на противоположной стороне. - Первый
player
управляется с помощью клавиатуры, а второй — компьютера.
4. Объекты игры
Игра включает 2 разных класса: один представляет объект player
, другой — объект ball
. На рисунке 1 изображена диаграмма класса UML с двумя указанными классами. Она представляет собой простую структуру классов системы, их свойства, методы и взаимосвязи.
4.1 Объект player
Класс Player
создает объекты для обоих игроков. Для каждого из них обязательно задаются уникальные значения position
(положение) и y-direction
(направление по оси y
).Что же касается width
(ширины), height
(высоты) и boundary
(границы), то они могут быть статическими переменными, поскольку их значения одинаковы для всех объектов. Рисунок 2 отображает принцип расположения игроков и границы их движения:
4.1.1. Свойства Player
У класса должно быть 5 свойств (рис. 1), каждое из которых имеет свое назначение.
position
—PVector
. Определяет положение игрока по осямx
иy
.yDir
—float
. Определяет направление и скорость движения.w
—float
. Определяет ширину прямоугольника объекта.h
—float
. Определяет высоту прямоугольника.b
—float
. Определяет границы движения игрока.
Ниже представлена реализация свойств класса Player
(рис. 3):
class Player {
// Текущее положение
PVector position;
// Направление движения
float yDir;
// Размер
float w = 10;
float h = 25;
// Границы
float b = 15;
}
4.1.2. Методы Player
Рассмотрим 5 обязательных методов класса (рис. 1) и их назначение.
update()
— методvoid
. Обновляет поведение игрока, связанного с движением, проверкой границ и отрисовкой визуального объекта.setDirection(yDir)
— метод “сеттер”. Обновляет направление объектаplayer
по осиy
.getPosition()
— метод “геттер”. Возвращает положение объектаplayer
.getHeight()
— метод “геттер”. Возвращает высоту объектаplayer
.getWidth()
— метод “геттер”. Возвращает ширину объектаplayer
.
Классу Player
также требуется конструктор для установки начальных значений положения и направления по оси y
. Аргумент конструктора представлен значениями floats
для отображения отправной точки. Она используется в конструкторе с целью создания экземпляра нового объекта PVector
для положения игрока.
Такой подход гарантирует, что объекты player
не будут ссылаться на один и тот же объект PVector
в памяти. А такое могло бы произойти в случае получения несколькими объектами player
одного и того же объекта PVector
при инстанцировании.
Геттер для direction
позволяет установить направление объекта вне класса. Сеттеры для position
, height
и width
допускают считывание свойств объекта вне класса. Рассмотрим реализацию конструктора player
, геттеры и сеттеры (рис. 4):
class Player {
// ... Свойства.
// Конструктор Player
public Player(float x, float y, float yDir) {
this.position = new PVector(x, y);
this.yDir = yDir;
}
// Сеттеры
public void setDirection(float yDir) {
this.yDir = yDir;
}
// Геттеры
public PVector getPosition() {
return position;
}
public float getHeight() {
return h;
}
public float getWidth() {
return w;
}
}
В классе Player
указывается еще один последний метод — update()
. Ниже представлена его реализация (рис. 5):
class Player {
// ... Свойства.
// ... Конструктор Player.
// ... Сеттеры.
// ... Геттеры.
public void update() {
// Обновление положения player
position.y += yDir;
// Достиг ли игрок
// верхней границы?
if (position.y < b) {
// Остановка движения на верхней границе
position.y = b;
// Достиг ли игрок
// нижней границы?
} else if (position.y > height-b-h) {
// Остановка движения на нижней границе
position.y = height-b-h;
}
// Установка белого цвета для player
fill(255);
// Отрисовка прямоугольника
rect(position.x, position.y, w, h);
}
}
Поясним поведение данного метода:
- Строка 10. При положительном значении
ydir
она увеличиваетposition.y
игрока, а при отрицательномydir
— уменьшает. - Строка 14. Первая строка проверяет, является ли
position.y
меньше значенияb
, определяющего границы движения. Иначе говоря, достиг ли игрок верхней границы экрана. - Строка 16. Переустанавливает
position.y
игрока в значениеb
, препятствуя дальнейшему движению к верхней границе экрана. - Строка 19. По принципу строки 14 проверяет, достиг ли игрок нижней границы экрана. Подразумевается проверка
height
игрока и значенияb
, чтобы гарантировать остановку прямоугольника в правильном положении (рис. 6). - Строка 21. Сбрасывает
position.y
на нижней границе экрана, препятствуя выходу за пределы экрана по осиy
.
Комментарий к рисунку 6. Объект player
перемещается в опорной точке (значение свойства position
). Следовательно, он должен остановиться на высоте экрана, вычитаемой из b
и h
. Так мы гарантируем, что прямоугольник остановится, когда его нижняя сторона достигнет границы экрана.
Ссылка на полный вариант кода для класса Player
.
4.2 Объект ball
Класс Ball
очень похож на класс Player
. При этом допускается движение мяча как по оси x
, так и y
. Рисунок 7 отображает изменение значений положения и направления мяча по мере движения:
4.2.1. Свойства Ball
Согласно рисунку 1, класс Ball
обладает 4 свойствами. Познакомимся с ними.
position
—PVector
. Определяет положение мяча на осяхx
иy
.direction
—PVector
. Определяет направление и скорость движения мяча.d
—float
. Определяет диаметр окружности мяча.s
—float
. Определяет начальную скорость мяча.
Ниже представлена реализация свойств класса Ball
(рис. 8):
class Ball {
// Текущее положение
PVector position;
// Текущее направление
PVector direction;
// Диаметр окружности
float d = 15;
// Скорость мяча
float s = 5;
}
4.2.2. Методы Ball
По аналогии с классом Player
класс Ball
характеризуется наличием 5 методов.
update()
— методvoid
. Обновляет движение, границы и графику мяча.getPosition()
— метод “геттер”. Возвращает положение мяча.resetMovement()
— методvoid
. Сбрасывает положение мяча в центр экрана и переустанавливает направление на произвольное значение.setDirection(x)
— метод “сеттер”. Устанавливает направление мяча по осиx
.overlapsWith(player)
— логический метод. Возвращаетtrue
при столкновении с игроком.
Рассмотрим реализацию конструктора Ball
и методов getPosition()
, resetMovement()
, setDirection(x)
, update()
(рис. 9):
class Ball {
// ... Свойства.
// Конструктор Ball
public Ball() {
resetMovement();
}
// Геттеры
public PVector getPosition() {
return position;
}
// Сброс положения и рандомизация направления.
public void resetMovement() {
// Установка положения в центр экрана
position = new PVector(width/2, height/2);
// Получение произвольного значения скорости
float speed = random(-s, s);
// Установка направления по оси y на половинное значение скорости
// для гарантии ускоренного движения влево или
// вправо
direction = new PVector(speed, speed/2);
}
// Установка направления по оси x.
public void setDirection(float x) {
direction.x = x * speed;
}
// Обновление положения мяча, добавление границ
// и отрисовка графики.
public void update() {
// Добавление скорости
position.add(direction);
// Проверка, достиг ли мяч верхней
// или нижней границы экрана.
if (position.y < 0 || position.y > height) {
// Смена направления по оси y на противоположное
direction.y = -direction.y;
}
// Установка белого цвета заливки
fill(255);
// Отрисовка круга.
circle(position.x, position.y, d);
}
}
4.2.3 Методы столкновения
В классе Ball
осталось реализовать еще один последний метод — overlapsWith(player)
. Но прежде кратко рассмотрим математические понятия, лежащие в основе решения.
Рисунок 10 иллюстрирует мяч, нарисованный в системе координат. Опорная точка находится в исходном положении и равна свойству мяча position
.
Каждый вектор/стрелка на рисунке указывает на точку относительно опорной точки, используемой при проверке столкновения. Точки вычисляются по формуле перевода градусов в радианы и матрице поворота. Каждый вектор повернут на 45 градусов.
Ниже представлены
- формула перевода градусов в радианы:
- формула для поворота вектора v на θ радиан:
Точки, вычисляемые по данным формулам, используются для следующей проверки: если одна из них находится между угловыми точками player
, то мяч столкнулся с игроком (рис 11):
Комментарий к рисунку 11. Объект player
сталкивается с объектом ball
. Каждая угловая точка player
показана с учетом свойств position
, width
и height
.
На рисунке 12 строка 24 отображает реализацию формулы для перевода градусов в радианы, а строки 28–29 — реализацию матрицы поворота для поворота двухмерного вектора на 45 градусов.
Строки 33–34 показывают проверку, которая возвращает true
, если точки располагаются в пределах угловых точек player
.
Рис. 12. Реализация метода overlapsWith(player)
:
class Ball {
// ... Свойства.
// ... Конструктор Ball
// ... Геттеры
// ... Сброс положения и рандомизация направления.
// ... Смена метода направления.
// ... Обновление метода.
public boolean overlapsWith(Player player) {
// Получение положения player,
// ширины и высоты.
var p = player.getPosition();
var w = player.getWidth();
var h = player.getHeight();
// Вычисление радиуса.
var r = d/2;
// Цикл из 8 точек.
for (int i = 0; i < 8; i++) {
// Перевод градусов i * 45 в радианы.
var degree = (i * 45) * (PI/180);
// Вычисление точек x и y путем поворота вектора
// относительно положения 45 градусов
var x = r * cos(position.x + degree) + position.x;
var y = r * sin(position.y + degree) + position.y;
// Возвращение true, если точка в пределах обоих
// осей x и y.
if (p.x < x && x < p.x + w &&
p.y < y && y < p.y + h) return true;
}
// Если ни одна из точек не находится в пределах
// player, возвращается false.
return false;
}
}
Ссылка на полный вариант кода для класса Ball
.
5. Настройка потока управления Processing
Приложению требуются 3 метода Processing: setup()
, draw()
и keyPressed()
.
setup()
выполняется при запуске приложения.draw()
выполняется один раз в каждом блоке до остановки.keyPressed()
выполняется при нажатии клавиши.
5.1 Глобальные свойства
У игры должна быть пара глобальных свойств для ссылки на объекты player
и ball
, speed
(скорость) и score
(счет) игроков.
Рис. 13. Реализация глобальных свойств игры:
// Определение скорости движения
float speed = 3;
// Определение счета
float p1Score = 0;
float p2Score = 0;
// Определение переменных player
Player p1;
Player p2;
// Определение переменных ball
Ball ball;
// Определение последнего положения x
// мяча
float lastBallPositionX = 0;
5.2 Метод setup()
Метод setup()
применяется для инициализации экземпляров Player
и Ball
, а также установки размера экрана (рис. 14).
Рис. 14. Реализация метода setup()
:
// ... Глобальные свойства.
void setup() {
size(500, 500); // Установка размера экрана
// Создание экземпляров player
// по обеим сторонам экрана,
// без direction.y.
p1 = new Player(10, height/2, 0);
p2 = new Player(width-20, height/2, 0);
// Создание экземпляра ball
ball = new Ball();
}
5.3 Метод draw()
Метод draw()
нужен для повторяющейся логики игры. На рисунке 15 в строках 20–33 условная инструкции if
и else if
проверяет, находится ли мяч за границами экрана. Если да, то один из игроков зарабатывает очко, а положение и направление мяча сбрасываются. В строках 36–29 происходит проверка с результатом true
в случае столкновения одного из игроков с мячом, вследствие чего меняется его направление по оси x
. Строки 43–58 отображают реализацию принципа управления компьютером объекта player
(p2
), а строки 61–64 — реализацию текстового вывода очков игроков.
Рис. 15. Реализация метода draw()
:
// ... Глобальные свойства.
// ... Метод Setup.
void draw() {
// Установка черного фона экрана.
background(0);
// Обновление игроков.
p1.update();
p2.update();
// Обновление мяча.
ball.update();
// Получение положения мяча
PVector ballPosition = ball.getPosition();
// Находится ли мяч за границами
// экрана на левой стороне?
if (ballPosition.x < 0) {
// Присуждение P2 одного очка.
p2Score += 1;
// Сброс положения и направления мяча.
ball.resetMovement();
}
// Или, находится ли мяч за границами
// экрана на правой стороне?
else if (ballPosition.x > width) {
// Присуждение P1 одного очка.
p1Score += 1;
// Сброс положения и направления мяча.
ball.resetMovement();
}
// Сталкивается ли мяч с одним из игроков?
if (ball.overlapsWith(p1)) {
// Смена направления по оси x.
ball.setDirection(1);
}
if (ball.overlapsWith(p2)) {
// Смена направления по оси x.
ball.setDirection(-1);
}
// Перемещение P2 к мячу, если мяч
// движется по направлению к позиции P2
if (lastBallPositionX < ballPosition.x) {
// Получение позиции P2
PVector p2Position = p2.getPosition();
// Вычисление направления
float directionToBallY = ballPosition.y - p2Position.y;
// Ограничение значений между -1 and 1
directionToBallY = constrain(directionToBallY, -1, 1);
// Добавление скорости
directionToBallY *= speed;
// Установка направления P2
p2.setDirection(directionToBallY);
}
// Кэширование положения мяча x
// для следующей проверки
lastBallPositionX = ballPosition.x;
// Установка белого цвета заливки.
fill(255);
// Отрисовка счета игроков на каждой стороне.
text("P1 Score: " + p1Score, 10, 20);
text("P2 Score: " + p2Score, width-80, 20);
}
5.4 Метод KeyPressed()
Последний блок кода предназначен для функциональности, обрабатывающей события клавиатуры, которые перемещают объект p1
(рис. 16).
Рис. 16. Реализация метода keyPressed()
:
// ... Глобальные свойства.
// ... Метод Setup.
// ... Метод Draw.
void keyPressed() {
// Движение вверх
if (key == 'w') p1.setDirection(-speed);
// Движение вниз
else if (key == 's') p1.setDirection(speed);
}
6. Заключение
В статье игра “Пинг-понг” создается с помощью библиотеки Processing и Java. Она включает 2 класса Player
и Ball
. Первый представляет объекты player
, а второй — объект ball
.
Для настройки непрерывного поведения и структуры игры эти классы используются совместно внутри таких методов Processing, как setup()
, draw()
и keyPressed()
. Первый объект player
управляется клавиатурой с помощью s
и w
. За управлением вторым игроком отвечает компьютер.
Полный вариант кода игры предоставлен по данной ссылке.
Читайте также:
- Аннотации Java — основы
- Как правильно учиться Java-программированию: история одного тьютора
- Знакомство с Kotlin для Android за один день
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Nicolai B. Andersen: Create the Classic Ping-pong Game With Java