Классическая игра

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

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

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

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

Читайте нас в TelegramVK и Яндекс.Дзен


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

Предыдущая статьяКак создать приложение Todo на React
Следующая статья15 часто используемых методов массивов JavaScript