Если вы искали обучающее руководство по воссозданию классической игры “Пинг-понг”, поздравляю  —  вы его нашли! Для реализации этой цели воспользуемся библиотекой 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 с двумя указанными классами. Она представляет собой простую структуру классов системы, их свойства, методы и взаимосвязи. 

Рис.1 Диаграмма класса UML с двумя классами, используемыми в игре “Пинг-понг” 

4.1 Объект player

Класс Player создает объекты для обоих игроков. Для каждого из них обязательно задаются уникальные значения position (положение) и y-direction (направление по оси y).Что же касается width (ширины), height (высоты) и boundary (границы), то они могут быть статическими переменными, поскольку их значения одинаковы для всех объектов. Рисунок 2 отображает принцип расположения игроков и границы их движения:

Рис.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

Комментарий к рисунку 6. Объект player перемещается в опорной точке (значение свойства position). Следовательно, он должен остановиться на высоте экрана, вычитаемой из b и h. Так мы гарантируем, что прямоугольник остановится, когда его нижняя сторона достигнет границы экрана. 

Ссылка на полный вариант кода для класса Player.  

Telegram-канал по теме
NOP::Nuances of ProgrammingСтатьи и переводы для разработчиков — каждый день
Подписаться

4.2 Объект ball

Класс Ball очень похож на класс Player. При этом допускается движение мяча как по оси x, так и y. Рисунок 7 отображает изменение значений положения и направления мяча по мере движения:

Рис. 7 Свойства объекта ball 

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 градусов. 

Рис. 10 

Ниже представлены

  •  формула перевода градусов в радианы: 
  • формула для поворота вектора v на θ радиан: 

Точки, вычисляемые по данным формулам, используются для следующей проверки: если одна из них находится между угловыми точками player, то мяч столкнулся с игроком (рис 11):  

Рис. 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. За управлением вторым игроком отвечает компьютер. 

Полный вариант кода игры предоставлен по данной ссылке. 

Читайте также:


Перевод статьи Nicolai B. Andersen: Create the Classic Ping-pong Game With Java

Мы в Telegram — подписывайтесь
NOP::Nuances of ProgrammingСтатьи и переводы для разработчиков — каждый день
Подписаться
NOP::HumorИТ-юмор и мемы о боли разработчиков
Подписаться
NOP::Recruiter RemoteУдалённые ИТ-вакансии с вилками
Подписаться
Предыдущая статьяКак создать приложение Todo на React
Следующая статья15 часто используемых методов массивов JavaScript