В нашей версии игры «Breakout» должны присутствовать полноценные уровни с большим количеством кирпичиков. При этом мы хотим, чтобы эти уровни были сконфигурированы таким образом, чтобы:
могли поддерживать любое количество строк и/или столбцов;
имели твердые (неразрушаемые) кирпичи;
поддерживали несколько вариантов раскраски кирпичей;
информация о структуре уровня хранилась во внешних (текстовых) файлах.
На этом уроке мы кратко пробежимся по коду объекта игрового уровня, который будет задействован в управлении большим количеством кирпичей. Однако для начала необходимо описать объект, представляющий непосредственно сам кирпич.
Создание кирпичей
Для этого создадим компонент под названием игровой объект (англ. «game object»), который будет играть роль базового представления объектов внутри игры. Представленный компонент хранит в себе данные о состоянии описываемых им игровых объектах, например: координаты положения, размер, скорость, цвет, вращение, способность к разрушению (да/нет), а также изображение спрайта в виде переменной типа Texture2D.
Каждый объект игры будет представлен классом GameObject или производным от него классом.
#Класс GameObject
Заголовочный файл — game_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 32 |
#ifndef GAMEOBJECT_H #define GAMEOBJECT_H #include <glad/glad.h> #include <glm/glm.hpp> #include "texture.h" #include "sprite_renderer.h" // Контейнерный объект, хранящий все состояния, относящиеся к отдельно взятой игровой сущности class GameObject { public: // Состояние объекта glm::vec2 Position, Size, Velocity; glm::vec3 Color; float Rotation; bool IsSolid; bool Destroyed; // Состояние рендера Texture2D Sprite; // Конструкторы GameObject(); GameObject(glm::vec2 pos, glm::vec2 size, Texture2D sprite, glm::vec3 color = glm::vec3(1.0f), glm::vec2 velocity = glm::vec2(0.0f, 0.0f)); // Отрисовка спрайта virtual void Draw(SpriteRenderer &renderer); }; #endif |
Файл реализации — game_object.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include "game_object.h" GameObject::GameObject() : Position(0.0f, 0.0f), Size(1.0f, 1.0f), Velocity(0.0f), Color(1.0f), Rotation(0.0f), Sprite(), IsSolid(false), Destroyed(false) { } GameObject::GameObject(glm::vec2 pos, glm::vec2 size, Texture2D sprite, glm::vec3 color, glm::vec2 velocity) : Position(pos), Size(size), Velocity(velocity), Color(color), Rotation(0.0f), Sprite(sprite), IsSolid(false), Destroyed(false) { } void GameObject::Draw(SpriteRenderer &renderer) { renderer.DrawSprite(this->Sprite, this->Position, this->Size, this->Rotation, this->Color); } |
Уровень в игре «Breakout» состоит исключительно из кирпичей, поэтому можно его прям так и представлять: набором кирпичей. Поскольку для описания кирпича в виде игрового объекта требуются параметры состояния похожие на те, которые мы перечислили выше, то будем описывать каждый кирпич уровня в виде объекта класса GameObject.
Объявление класса GameLevel выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class GameLevel { public: // Состояние уровня std::vector<GameObject> Bricks; // Конструктор GameLevel() { } // Загрузка уровня из файла void Load(const char *file, unsigned int levelWidth, unsigned int levelHeight); // Рендеринг уровня void Draw(SpriteRenderer &renderer); // Проверка, что уровень пройден (все разрушаемые кирпичи уничтожены) bool IsCompleted(); private: // Инициализация уровня void init(std::vector<std::vector<unsigned int>> tileData, unsigned int levelWidth, unsigned int levelHeight); }; |
В виду того, что уровень должен загружаться из внешнего (текстового) файла, потребуется придумать какую-то структуру, которая задаст представление игрового уровня. Вот пример того, как может выглядеть игровой уровень в текстовом файле:
1 1 1 1 1 1
2 2 0 0 2 2
3 3 4 4 3 3
Его данные хранятся в виде, очень похожем на матрицу, где каждое число представляет тип кирпича, а сами числа разделены пробелом. Затем уже непосредственно в коде уровня мы сопоставим каждому числу определенное свойство, например:
Число 0: Нет кирпича, пустое пространство внутри уровня.
Число 1: Твердый кирпич, т.е. кирпич, который не может быть разрушен.
Число > 1: Разрушаемый кирпич; каждое последующее число отличается только цветом.
Если бы мы обработали вышеприведенный уровень классом GameLevel, то результат выглядел бы следующим образом:
Для генерации уровня из загружаемого файла в классе GameLevel используется две функции. Сначала, при помощи функции GameLevel::Load(), все числа уровня загружаются в двумерный вектор, а затем, с помощью функции GameLevel::init(), происходит их обработка (чтобы создать все игровые объекты):
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 |
void GameLevel::Load(const char *file, unsigned int levelWidth, unsigned int levelHeight) { // Очистка старых данных this->Bricks.clear(); // Загрузка из файла unsigned int tileCode; GameLevel level; std::string line; std::ifstream fstream(file); std::vector<std::vector<unsigned int>> tileData; if (fstream) { while (std::getline(fstream, line)) // построчное чтение файла уровня { std::istringstream sstream(line); std::vector<unsigned int> row; while (sstream >> tileCode) // чтение каждого элемента уровня, разделенного пробелом row.push_back(tileCode); tileData.push_back(row); } if (tileData.size() > 0) this->init(tileData, levelWidth, levelHeight); } } |
Переменная загруженных данных tileData
передается в функцию init() для инициализации игрового уровня:
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 44 45 46 47 |
void GameLevel::init(std::vector<std::vector<unsigned int>> tileData, unsigned int lvlWidth, unsigned int lvlHeight) { // Вычисляем размеры unsigned int height = tileData.size(); unsigned int width = tileData[0].size(); float unit_width = lvlWidth / static_cast<float>(width); float unit_height = lvlHeight / height; // Инициализация тайлов уровня на основе tileData for (unsigned int y = 0; y < height; ++y) { for (unsigned int x = 0; x < width; ++x) { // Проверка типа блока из данных уровня (2D-массива) if (tileData[y][x] == 1) // твердый { glm::vec2 pos(unit_width * x, unit_height * y); glm::vec2 size(unit_width, unit_height); GameObject obj(pos, size, ResourceManager::GetTexture("block_solid"), glm::vec3(0.8f, 0.8f, 0.7f) ); obj.IsSolid = true; this->Bricks.push_back(obj); } else if (tileData[y][x] > 1) { glm::vec3 color = glm::vec3(1.0f); // исходный цвет: белый if (tileData[y][x] == 2) color = glm::vec3(0.2f, 0.6f, 1.0f); else if (tileData[y][x] == 3) color = glm::vec3(0.0f, 0.7f, 0.0f); else if (tileData[y][x] == 4) color = glm::vec3(0.8f, 0.8f, 0.4f); else if (tileData[y][x] == 5) color = glm::vec3(1.0f, 0.5f, 0.0f); glm::vec2 pos(unit_width * x, unit_height * y); glm::vec2 size(unit_width, unit_height); this->Bricks.push_back( GameObject(pos, size, ResourceManager::GetTexture("block"), color) ); } } } } |
Функция init() перебирает каждое из загруженных чисел и добавляет столько объектов GameObject к вектору уровня GameLevel::Bricks, сколько чисел было обработано. Размеры каждого кирпича вычисляются автоматически (переменные unit_width
и unit_height
) в зависимости от общего количества кирпичей, в результате все они будут идеально вписаны в границы экрана.
Ниже представлены текстуры игровых объектов двух типов: разрушаемого блока и неразрушаемого (твердого) блока:
Правда тут есть один маленький трюк, а именно: эти текстуры выполнены полностью в градациях серого. Благодаря этому, в игровом коде мы можем аккуратно манипулировать их цветами, перемножая цвет текстуры с определенным цветовым вектором; точно так же, как мы это делали внутри SpriteRenderer
.
Класс GameLevel помимо всего прочего содержит и несколько других функций, таких как рендеринг всех неразрушаемых кирпичей или проверку на предмет того, все ли разрушаемые кирпичи были уничтожены.
#Класс GameLevel
Заголовочный файл — game_level.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 32 33 34 35 36 37 |
#ifndef GAMELEVEL_H #define GAMELEVEL_H #include <vector> #include <glad/glad.h> #include <glm/glm.hpp> #include "game_object.h" #include "sprite_renderer.h" #include "resource_manager.h" // Класс GameLevel содержит все кирпичи уровней игры, // а также функционал для загрузки/рендеринга информации с жесткого диска class GameLevel { public: // Состояние уровня std::vector<GameObject> Bricks; // Конструктор GameLevel() { } // Загрузка уровня из файла void Load(const char *file, unsigned int levelWidth, unsigned int levelHeight); // Рендеринг уровня void Draw(SpriteRenderer &renderer); // Проверка, пройден ли уровень (все разрушаемые кирпичи должны быть уничтожены) bool IsCompleted(); private: // Инициализация уровня данными из tileData void init(std::vector<std::vector<unsigned int>> tileData, unsigned int levelWidth, unsigned int levelHeight); }; #endif |
Файл реализации — game_level.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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
#include "game_level.h" #include <fstream> #include <sstream> void GameLevel::Load(const char *file, unsigned int levelWidth, unsigned int levelHeight) { // Очистка старых данных this->Bricks.clear(); // Загрузка из файла unsigned int tileCode; GameLevel level; std::string line; std::ifstream fstream(file); std::vector<std::vector<unsigned int>> tileData; if (fstream) { while (std::getline(fstream, line)) // построчное чтение файла уровня { std::istringstream sstream(line); std::vector<unsigned int> row; while (sstream >> tileCode) // чтение каждого элемента уровня, разделенного пробелом row.push_back(tileCode); tileData.push_back(row); } if (tileData.size() > 0) this->init(tileData, levelWidth, levelHeight); } } void GameLevel::Draw(SpriteRenderer &renderer) { for (GameObject &tile : this->Bricks) if (!tile.Destroyed) tile.Draw(renderer); } bool GameLevel::IsCompleted() { for (GameObject &tile : this->Bricks) if (!tile.IsSolid && !tile.Destroyed) return false; return true; } void GameLevel::init(std::vector<std::vector<unsigned int>> tileData, unsigned int levelWidth, unsigned int levelHeight) { // Вычисляем размеры unsigned int height = tileData.size(); unsigned int width = tileData[0].size(); // обратите внимание, что мы можем индексировать вектор, начиная с 0, поскольку данная функция вызывается только в том случае, если height > 0 float unit_width = levelWidth / static_cast<float>(width), unit_height = levelHeight / height; // Инициализация уровня данными из tileData for (unsigned int y = 0; y < height; ++y) { for (unsigned int x = 0; x < width; ++x) { // Проверяем тип блока по информации об уровне (2D-массив уровня) if (tileData[y][x] == 1) // твердый { glm::vec2 pos(unit_width * x, unit_height * y); glm::vec2 size(unit_width, unit_height); GameObject obj(pos, size, ResourceManager::GetTexture("block_solid"), glm::vec3(0.8f, 0.8f, 0.7f)); obj.IsSolid = true; this->Bricks.push_back(obj); } else if (tileData[y][x] > 1) // разрушаемый; теперь определяем его цвет { glm::vec3 color = glm::vec3(1.0f); // исходный цвет - белый if (tileData[y][x] == 2) color = glm::vec3(0.2f, 0.6f, 1.0f); else if (tileData[y][x] == 3) color = glm::vec3(0.0f, 0.7f, 0.0f); else if (tileData[y][x] == 4) color = glm::vec3(0.8f, 0.8f, 0.4f); else if (tileData[y][x] == 5) color = glm::vec3(1.0f, 0.5f, 0.0f); glm::vec2 pos(unit_width * x, unit_height * y); glm::vec2 size(unit_width, unit_height); this->Bricks.push_back(GameObject(pos, size, ResourceManager::GetTexture("block"), color)); } } } } |
Благодаря классу GameLevel у нас появляется возможность работать с любым количеством строк и столбцов, а пользователи могут легко создавать свои собственные уровни, изменяя соответствующие файлы.
Внутри игры
Будет здорово, если мы реализуем в игре поддержку нескольких уровней, поэтому давайте немного расширим класс игры, добавив вектор, содержащий переменные типа GameLevel. А также еще одну переменную, в которой будет храниться номер текущего активного уровня:
1 2 3 4 5 6 7 |
class Game { [...] std::vector<GameLevel> Levels; unsigned int Level; [...] }; |
Наша версия игры «Breakout» будет включать в себя в общей сложности 4 уровня:
Уровень №1: Standard (файл — one.lvl):
5 5 5 5 5 5 5 5 5 5 5 5 5 5 5
5 5 5 5 5 5 5 5 5 5 5 5 5 5 5
4 4 4 4 4 0 0 0 0 0 4 4 4 4 4
4 1 4 1 4 0 0 1 0 0 4 1 4 1 4
3 3 3 3 3 0 0 0 0 0 3 3 3 3 3
3 3 1 3 3 3 3 3 3 3 3 3 1 3 3
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
Уровень №2: A few small gaps (файл — two.lvl):
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 0 5 5 0 5 5 0 5 5 0 5 5 0 1
1 5 5 5 5 5 5 5 5 5 5 5 5 5 1
1 0 3 3 0 3 3 0 3 3 0 3 3 0 1
1 3 3 3 3 3 3 3 3 3 3 3 3 3 1
1 0 2 2 0 2 2 0 2 2 0 2 2 0 1
1 2 2 2 2 2 2 2 2 2 2 2 2 2 1
1 0 1 1 0 1 1 0 1 1 0 1 1 0 1
Уровень №3: Space invader (файл — three.lvl):
0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 2 0 0 0 0 0 0 0 2 0 0
0 0 0 2 0 0 0 0 0 2 0 0 0
0 0 0 5 5 5 5 5 5 5 0 0 0
0 0 5 5 0 5 5 5 0 5 5 0 0
0 5 5 5 5 5 5 5 5 5 5 5 0
0 3 0 1 1 1 1 1 1 1 0 3 0
0 3 0 3 0 0 0 0 0 3 0 3 0
0 0 0 0 4 4 0 4 4 0 0 0 0
Уровень №4: Bounce galore (файл — four.lvl):
1 2 1 2 1 2 1 2 1 2 1 2 1
2 2 2 2 2 2 2 2 2 2 2 2 2
2 1 3 1 4 1 5 1 4 1 3 1 2
2 3 3 4 4 5 5 5 4 4 3 3 2
2 1 3 1 4 1 5 1 4 1 3 1 2
2 2 3 3 4 4 5 4 4 3 3 2 2
Затем все текстуры и уровни игры инициализируются с помощью функции Game::Init():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
void Game::Init() { [...] // Загрузка текстур ResourceManager::LoadTexture("textures/background.jpg", false, "background"); ResourceManager::LoadTexture("textures/awesomeface.png", true, "face"); ResourceManager::LoadTexture("textures/block.png", false, "block"); ResourceManager::LoadTexture("textures/block_solid.png", false, "block_solid"); // Загрузка уровней GameLevel one; one.Load("levels/one.lvl", this->Width, this->Height / 2); GameLevel two; two.Load("levels/two.lvl", this->Width, this->Height / 2); GameLevel three; three.Load("levels/three.lvl", this->Width, this->Height / 2); GameLevel four; four.Load("levels/four.lvl", this->Width, this->Height / 2); this->Levels.push_back(one); this->Levels.push_back(two); this->Levels.push_back(three); this->Levels.push_back(four); this->Level = 0; } |
Теперь всё, что осталось сделать, — это непосредственно визуализация уровня. Для этого у текущего активного уровня вызывается функция Draw(), которая, в свою очередь, вызывает функцию GameObject::Draw() у каждого объекта:
1 2 3 4 5 6 7 8 9 10 11 12 |
void Game::Render() { if(this->State == GAME_ACTIVE) { // Отрисовка фона Renderer->DrawSprite(ResourceManager::GetTexture("background"), glm::vec2(0.0f, 0.0f), glm::vec2(this->Width, this->Height), 0.0f ); // Отрисовка уровня this->Levels[this->Level].Draw(*Renderer); } } |
Следующим шагом мы визуализируем сцену с вот таким милым фоновым изображением:
В результате получается красиво прорисованный уровень:
Ракетка для игрока
Таким же образом в нижней части сцены мы можем ввести и ракетку, которая будет контролироваться игроком. Ракетка допускает только горизонтальные перемещения, и всякий раз, когда она касается любого из краев сцены, её движение прекращается. А в качестве текстуры мы используем следующее изображение:
Объект ракетки должен содержать информацию о своем положении, размере и текстуре спрайта, поэтому имеет смысл определить ракетку через класс GameObject:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Начальный размер ракетки игрока const glm::vec2 PLAYER_SIZE(100.0f, 20.0f); // Начальная скорость перемещения ракетки игрока const float PLAYER_VELOCITY(500.0f); GameObject *Player; void Game::Init() { [...] ResourceManager::LoadTexture("textures/paddle.png", true, "paddle"); [...] glm::vec2 playerPos = glm::vec2( this->Width / 2.0f - PLAYER_SIZE.x / 2.0f, this->Height - PLAYER_SIZE.y ); Player = new GameObject(playerPos, PLAYER_SIZE, ResourceManager::GetTexture("paddle")); } |
В вышеприведенном фрагменте кода мы задали несколько констант, определяющих размер и скорость перемещения ракетки. В рамках функции инициализации игры Game::Init() мы вычисляем начальное положение ракетки внутри сцены. А также убеждаемся в том, чтобы её центр был выровнен с горизонтальным центром сцены.
Кроме того, необходимо добавить следующую строку кода в конец функции Game::Render():
1 |
Player->Draw(*Renderer); |
Если бы вы прямо сейчас запустили игру, то увидели бы не только сам уровень, но и забавную ракетку, выровненную по нижнему краю сцены. На данный момент она ничего не делает, поэтому мы собираемся углубиться в функцию Game::ProcessInput(), чтобы реализовать возможность горизонтального перемещения ракетки всякий раз, когда пользователь нажимает клавишу A
или D
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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 (this->Keys[GLFW_KEY_D]) { if (Player->Position.x <= this->Width - Player->Size.x) Player->Position.x += velocity; } } } |
Как вы можете видеть, мы перемещаем ракетку либо влево, либо вправо, в зависимости от того, какую клавишу нажал пользователь (обратите внимание, мы умножаем скорость на переменную разницы времени dt
). Если бы значение x-координаты ракетки было бы меньше 0
, то она переместилась бы за пределы левого края окна, поэтому мы перемещаем её влево только в том случае, если значение её x-координаты больше значения х-координаты левого края окна (0.0)
. Проделываем то же самое для случая, когда ракетка достигает правого края окна, при этом необходимо сравнивать положение правого края окна с правым краем ракетки (т.е. вычесть ширину ракетки из x-координаты правого края окна).
Если сейчас запустить игру, то мы получим ракетку, которую можно горизонтально перемещать по всему нижнему краю окна:
#Класс Game
Заголовочный файл — game.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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
#ifndef GAME_H #define GAME_H #include <glad/glad.h> #include <GLFW/glfw3.h> #include "game_level.h" // Представляем текущее состояние игры enum GameState { GAME_ACTIVE, GAME_MENU, GAME_WIN }; // Начальный размер ракетки игрока const glm::vec2 PLAYER_SIZE(100.0f, 20.0f); // Начальная скорость перемещения ракетки игрока const float PLAYER_VELOCITY(500.0f); // Класс Game содержит все относящиеся к игре состояния и необходимый функционал. // Объединяем все данные, связанные с игрой, в один класс для простого доступа к каждому из компонентов class Game { public: // Состояние игры GameState State; bool Keys[1024]; unsigned int Width, Height; std::vector<GameLevel> Levels; unsigned int Level; // Конструктор Game(unsigned int width, unsigned int height); // Деструктор ~Game(); // Инициализация состояния игры (загрузка всех шейдеров/текстур/уровней) void Init(); // Игровой цикл void ProcessInput(float dt); void Update(float dt); void Render(); }; #endif |
Файл реализации — game.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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
#include "game.h" #include "resource_manager.h" #include "sprite_renderer.h" #include "game_object.h" // Данные, относящиеся к состоянию игры SpriteRenderer *Renderer; GameObject *Player; Game::Game(unsigned int width, unsigned int height) : State(GAME_ACTIVE), Keys(), Width(width), Height(height) { } Game::~Game() { delete Renderer; delete Player; } void Game::Init() { // Загрузка шейдеров ResourceManager::LoadShader("shaders/sprite.vs", "shaders/sprite.frag", nullptr, "sprite"); // Конфигурирование шейдеров glm::mat4 projection = glm::ortho(0.0f, static_cast<float>(this->Width), static_cast<float>(this->Height), 0.0f, -1.0f, 1.0f); ResourceManager::GetShader("sprite").Use().SetInteger("image", 0); ResourceManager::GetShader("sprite").SetMatrix4("projection", projection); // Установка специфичных для рендеринга элементов управления Renderer = new SpriteRenderer(ResourceManager::GetShader("sprite")); // Загрузка текстур ResourceManager::LoadTexture("textures/background.jpg", false, "background"); ResourceManager::LoadTexture("textures/awesomeface.png", true, "face"); ResourceManager::LoadTexture("textures/block.png", false, "block"); ResourceManager::LoadTexture("textures/block_solid.png", false, "block_solid"); ResourceManager::LoadTexture("textures/paddle.png", true, "paddle"); // Загрузка уровней GameLevel one; one.Load("levels/one.lvl", this->Width, this->Height / 2); GameLevel two; two.Load("levels/two.lvl", this->Width, this->Height / 2); GameLevel three; three.Load("levels/three.lvl", this->Width, this->Height / 2); GameLevel four; four.Load("levels/four.lvl", this->Width, this->Height / 2); this->Levels.push_back(one); this->Levels.push_back(two); this->Levels.push_back(three); this->Levels.push_back(four); this->Level = 0; // Конфигурирование игровых объектов glm::vec2 playerPos = glm::vec2(this->Width / 2.0f - PLAYER_SIZE.x / 2.0f, this->Height - PLAYER_SIZE.y); Player = new GameObject(playerPos, PLAYER_SIZE, ResourceManager::GetTexture("paddle")); } void Game::Update(float dt) { } 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 (this->Keys[GLFW_KEY_D]) { if (Player->Position.x <= this->Width - Player->Size.x) Player->Position.x += velocity; } } } void Game::Render() { if(this->State == GAME_ACTIVE) { // Отрисовка фона Renderer->DrawSprite(ResourceManager::GetTexture("background"), glm::vec2(0.0f, 0.0f), glm::vec2(this->Width, this->Height), 0.0f); // Отрисовка уровня this->Levels[this->Level].Draw(*Renderer); // Отрисовка ракетки Player->Draw(*Renderer); } } |
GitHub / Часть №4: Создание уровней в игре «Breakout» на C++/OpenGL — Исходный код