На данный момент у нас есть уровень, содержащий несколько кирпичей разного типа и цвета и подвижную ракетку, управляемую игроком. Единственное, чего здесь не хватает, — это мяча. Наша задача состоит в том, чтобы позволить мячу сталкиваться со всеми кирпичами уровня до тех пор, пока каждый из них не будет разрушен, но всё это при условии, что мяч не выпадет за нижний край экрана.
В дополнение к основным компонентам игрового объекта, объект мяча должен иметь радиус и дополнительную логическую переменную, указывающее на то, зафиксирован (переменная Stuck
) ли мяч на ракетке игрока или ему разрешено свободное перемещение. В начале игры мяч находится на ракетке, затем игрок нажимает какую-нибудь клавишу на клавиатуре, и мяч приходит в движение.
Для описания мяча мы создадим производный от класса GameObject класс BallObject, добавив в него еще несколько свойств:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class BallObject : public GameObject { public: // Состояние мяча float Radius; bool Stuck; BallObject(); BallObject(glm::vec2 pos, float radius, glm::vec2 velocity, Texture2D sprite); glm::vec2 Move(float dt, unsigned int window_width); void Reset(glm::vec2 position, glm::vec2 velocity); }; |
Конструктор класса BallObject инициализирует не только свои собственные переменные, но также и переменные базового класса GameObject. Класс BallObject содержит функцию движения BallObject::Move(), которая позволяет мячу перемещаться с учетом его скорости. Она также проверяет, достиг ли он какого-либо из краев сцены, и если — да, то меняет направление скорости мячика:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
glm::vec2 BallObject::Move(float dt, unsigned int window_width) { // Если мяч не зафиксирован ракеткой, if (!this->Stuck) { // то перемещаем мяч this->Position += this->Velocity * dt; // Проверка, находится ли мяч за пределами границ окна; если – да, то изменяем его скорость и восстанавливаем правильное положение if (this->Position.x <= 0.0f) { this->Velocity.x = -this->Velocity.x; this->Position.x = 0.0f; } else if (this->Position.x + this->Size.x >= window_width) { this->Velocity.x = -this->Velocity.x; this->Position.x = window_width - this->Size.x; } if (this->Position.y <= 0.0f) { this->Velocity.y = -this->Velocity.y; this->Position.y = 0.0f; } } return this->Position; } |
В дополнение к изменению направления скорости мяча, мы также должны переместить его обратно в случае, если он достиг края сцены; мяч может двигаться только тогда, когда он не зафиксирован.
Примечание: У нас отсутствует код, позволяющий мячу отскакивать от нижнего края, поскольку игрок заканчивает игру (или теряет жизнь) при достижении мячом нижнего края сцены. Однако позже нужно будет реализовать данную логику где-то в коде игры.
#Класс BallObject
Заголовочный файл — ball_object.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#ifndef BALLOBJECT_H #define BALLOBJECT_H #include <glad/glad.h> #include <glm/glm.hpp> #include "game_object.h" #include "texture.h" // Класс BallObject получен из класса GameObject. // Помимо необходимой информации о состоянии мяча в этом классе присутствуют некоторые дополнительные функции class BallObject : public GameObject { public: // Состояние мяча float Radius; bool Stuck; // Конструкторы BallObject(); BallObject(glm::vec2 pos, float radius, glm::vec2 velocity, Texture2D sprite); // Перемещаем мяч, удерживая его в пределах границ окна (за исключением нижнего края); возвращаем новую позицию glm::vec2 Move(float dt, unsigned int window_width); // Возвращаем мяч в исходное состояние с заданным положением и скоростью void Reset(glm::vec2 position, glm::vec2 velocity); }; #endif |
Файл реализации — ball_object.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#include "ball_object.h" BallObject::BallObject() : GameObject(), Radius(12.5f), Stuck(true) { } BallObject::BallObject(glm::vec2 pos, float radius, glm::vec2 velocity, Texture2D sprite) : GameObject(pos, glm::vec2(radius * 2.0f, radius * 2.0f), sprite, glm::vec3(1.0f), velocity), Radius(radius), Stuck(true) { } glm::vec2 BallObject::Move(float dt, unsigned int window_width) { // Если мяч не зафиксирован ракеткой, if (!this->Stuck) { // то перемещаем мяч this->Position += this->Velocity * dt; // Затем проверяем, находится ли он за пределами границ окна, и если да, то изменяем его скорость и восстанавливаем правильное положение if (this->Position.x <= 0.0f) { this->Velocity.x = -this->Velocity.x; this->Position.x = 0.0f; } else if (this->Position.x + this->Size.x >= window_width) { this->Velocity.x = -this->Velocity.x; this->Position.x = window_width - this->Size.x; } if (this->Position.y <= 0.0f) { this->Velocity.y = -this->Velocity.y; this->Position.y = 0.0f; } } return this->Position; } // Сбрасываем мяч в стартовое положение (если мяч находится за пределами границ окна) void BallObject::Reset(glm::vec2 position, glm::vec2 velocity) { this->Position = position; this->Velocity = velocity; this->Stuck = true; } |
Теперь давайте добавим мяч в игру. Точно так же, как и в случае с ракеткой, мы создаем указатель на объект класса BallObject и определяем две константы, необходимые для инициализации мяча. Использовать мы будем следующую текстуру:
Код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Начальная скорость мяча const glm::vec2 INITIAL_BALL_VELOCITY(100.0f, -350.0f); // Радиус мяча const float BALL_RADIUS = 12.5f; BallObject *Ball; void Game::Init() { [...] glm::vec2 ballPos = playerPos + glm::vec2(PLAYER_SIZE.x / 2.0f - BALL_RADIUS, -BALL_RADIUS * 2.0f); Ball = new BallObject(ballPos, BALL_RADIUS, INITIAL_BALL_VELOCITY, ResourceManager::GetTexture("face")); } |
Затем мы должны обновлять положение мяча каждый кадр, вызывая функцию Ball->Move() внутри функции Game::Update():
1 2 3 4 |
void Game::Update(float dt) { Ball->Move(dt, this->Width); } |
Кроме того, поскольку мяч изначально находится на ракетке в неподвижном (зафиксированном) состоянии, то мы должны дать игроку возможность привести его в движение. За это будет отвечать клавиша Пробел. Для этого мы немного изменим функцию Game::ProcessInput():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
void Game::ProcessInput(float dt) { if (this->State == GAME_ACTIVE) { float velocity = PLAYER_VELOCITY * dt; // Перемещаем ракетку if (this->Keys[GLFW_KEY_A]) { if (Player->Position.x >= 0.0f) { Player->Position.x -= velocity; if (Ball->Stuck) Ball->Position.x -= velocity; } } if (this->Keys[GLFW_KEY_D]) { if (Player->Position.x <= this->Width - Player->Size.x) { Player->Position.x += velocity; if (Ball->Stuck) Ball->Position.x += velocity; } } if (this->Keys[GLFW_KEY_SPACE]) Ball->Stuck = false; } } |
В вышеприведенном фрагменте кода, если пользователь нажимает пробел, то переменная Stuck
устанавливается в false
. Обратите внимание, что пока мяч находится на ракетке в неподвижном состоянии, то помимо перемещения самой ракетки, вместе с ней необходимо перемещать и мяч.
Наконец, добавляем код визуализации мяча:
1 2 3 4 5 6 7 8 |
void Game::Render() { if (this->State == GAME_ACTIVE) { [...] Ball->Draw(*Renderer); } } |
В результате получается мяч, который вначале игры неразрывно следует за ракеткой и начинает свое самостоятельное движение тогда, когда мы нажимаем пробел. Мяч также должным образом отскакивает от левого, правого и верхнего краев сцены, но, как мы видим, он еще не реагирует на столкновения с кирпичами:
GitHub / Часть №5: Добавление мяча в игру «Breakout» на C++/OpenGL — Исходный код
Чтобы это исправить, потребуется создать одну или несколько функций, которые проверяют, столкнулся ли объект мяча с любым из кирпичей уровня, и если — да, то кирпич уничтожается. На этих, так называемых, функциях обнаружения столкновений мы и сосредоточимся на следующем уроке.